From 32080e5c3f9caacdf45800ecb1b005c2a6db9f6d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 May 2024 15:02:57 -1000 Subject: [PATCH 001/230] ApiEndpoints(): endpoints for common controller operations --- plugins/module_utils/common/endpoints.py | 88 ++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 plugins/module_utils/common/endpoints.py diff --git a/plugins/module_utils/common/endpoints.py b/plugins/module_utils/common/endpoints.py new file mode 100644 index 000000000..ef6554a55 --- /dev/null +++ b/plugins/module_utils/common/endpoints.py @@ -0,0 +1,88 @@ +# 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 logging +import re + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils + + +class ApiEndpoints: + """ + Endpoints for common API calls + + Usage + + endpoints = ApiEndpoints() + try: + endpoint = endpoints.features + except ValueError as error: + raise ValueError(error) from error + + rest_send = RestSend(self.ansible_module) + rest_send.path = endpoint.get("path") + rest_send.verb = endpoint.get("verb") + rest_send.commit() + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED ApiEndpoints()") + + self.conversion = ConversionUtils() + + self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" + self.endpoint_fm = f"{self.endpoint_api_v1}/fm" + + self.endpoint_features = f"{self.endpoint_fm}/features" + + self._init_properties() + + def _init_properties(self): + """ """ + self.properties = {} + + @property + def fm_features(self): + """ + - return feature manager features endpoint + - verb: GET + - path: /appcenter/cisco/ndfc/api/v1/fm/features + """ + endpoint = {} + endpoint["path"] = f"{self.endpoint_fm}/features" + endpoint["verb"] = "GET" + return endpoint + + @property + def fm_version(self): + """ + - return feature manager version endpoint + - verb: GET + - path: /appcenter/cisco/ndfc/api/v1/fm/about/version + """ + endpoint = {} + endpoint["path"] = f"{self.endpoint_fm}/about/version" + endpoint["verb"] = "GET" + return endpoint From 293b1ca1b66a0cb71073a455ce9bcd0b96367538 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 May 2024 15:09:11 -1000 Subject: [PATCH 002/230] ControllerFeatures(): Retrieve feature information from the controller --- .../common/controller_features.py | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 plugins/module_utils/common/controller_features.py diff --git a/plugins/module_utils/common/controller_features.py b/plugins/module_utils/common/controller_features.py new file mode 100644 index 000000000..6be247f92 --- /dev/null +++ b/plugins/module_utils/common/controller_features.py @@ -0,0 +1,318 @@ +""" +Class to retrieve and return information about an NDFC controller +""" + +# +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.endpoints import \ + ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError + + +class ControllerFeatures: + """ + - Return feature information from the Controller + - Endpoint: /appcenter/cisco/ndfc/api/v1/fm/features + - Usage (where params is AnsibleModule.params): + + ```python + instance = ControllerFeatures(params) + instance.rest_send = RestSend(AnsibleModule) + # retrieves all feature information + try: + instance.refresh() + except ControllerResponseError as error: + # handle error + # filters the feature information + instance.filter = "pmn" + # retrieves the admin_state for feature pmn + pmn_admin_state = instance.admin_state + # retrieves the operational state for feature pmn + pmn_oper_state = instance.oper_state + # etc... + ``` + + - Retrievable properties for the filtered feature + - admin_state - str + - "enabled" + - "disabled" + - apidoc - list of dict + - [ + { + "url": "https://path/to/api-docs", + "subpath": "pmn", + "schema": null + } + ] + - description - str + - "Media Controller for IP Fabrics" + - healthz - str + - "https://path/to/healthz" + - hidden - bool + - True + - False + - featureset - dict + - { "lan": { "default": false }} + - name - str + - "IP Fabric for Media" + - oper_state - str + - "started" + - "stopped" + - "" + - predisablecheck - str + - "https://path/to/predisablecheck" + - installed - str + - "2024-05-08 18:02:45.626691263 +0000 UTC" + - kind - str + - "feature" + - requires - list + - ["pmn-telemetry-mgmt", "pmn-telemetry-data"] + - spec - str + - "" + - ui - bool + - True + - False + + Response: + { + "status": "success", + "data": { + "name": "", + "version": 179, + "features": { + "change-mgmt": { + "name": "Change Control", + "description": "Tracking, Approval, and Rollback...", + "ui": false, + "predisablecheck": "https://path/preDisableCheck", + "spec": "", + "admin_state": "disabled", + "oper_state": "", + "kind": "featurette", + "featureset": { + "lan": { + "default": false + } + } + } + etc... + } + } + } + """ + + def __init__(self, params): + self.class_name = self.__class__.__name__ + self.params = params + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED ControllerFeatures()") + + self.check_mode = self.params.get("check_mode", None) + if self.check_mode is None: + msg = f"{self.class_name}.__init__(): " + msg += "check_mode is required" + raise ValueError(msg) + + self.conversion = ConversionUtils() + self.endpoints = ApiEndpoints() + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["data"] = None + self.properties["rest_send"] = None + self.properties["result"] = None + self.properties["response"] = None + + def refresh(self): + """ + - Refresh self.response_data with current features info + from the controller + - Raise ``ValueError`` if the endpoint assignment fails. + """ + method_name = inspect.stack()[0][3] + + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.rest_send must be set " + msg += "before calling commit." + raise ValueError(msg) + + path = self.endpoints.fm_features.get("path") + verb = self.endpoints.fm_features.get("verb") + self.rest_send.path = path + self.rest_send.verb = verb + + # Store the current value of check_mode, then disable + # check_mode since ControllerFeatures() only reads data + # from the controller. + # Restore the value of check_mode after the commit. + current_check_mode = self.rest_send.check_mode + self.rest_send.check_mode = False + self.rest_send.commit() + self.rest_send.check_mode = current_check_mode + + if self.rest_send.result_current["success"] is False: + msg = f"{self.class_name}.refresh() failed: {self.rest_send.result_current}" + raise ControllerResponseError(msg) + + self.properties["response_data"] = ( + self.rest_send.response_current.get("DATA", {}) + .get("data", {}) + .get("features", {}) + ) + if self.response_data is None: + msg = f"{self.class_name}.refresh() failed: response " + msg += "does not contain DATA key. Controller response: " + msg += f"{self.rest_send.response_current}" + raise ControllerResponseError(msg) + + def _get(self, item): + """ + - Return the value of the item from the filtered response_data. + - Return None if the item does not exist. + """ + data = self.response_data.get(self.filter, {}).get(item, None) + return self.conversion.make_boolean(self.conversion.make_none(data)) + + @property + def admin_state(self): + """ + - Return the controller admin_state for filter, if it exists. + - Return None otherwise + - Possible values: + - enabled + - disabled + - None + """ + return self._get("admin_state") + + @property + def enabled(self): + """ + - Return True if the filtered feature admin_state is "enabled". + - Return False otherwise. + - Possible values: + - True + - False + """ + if self.admin_state == "enabled": + return True + return False + + @property + def filter(self): + """ + - getter: Return the filter value + - setter: Set the filter value + - The filter value should be the name of the feature + - For example: + - lan + - Full LAN functionality in addition to Fabric + Discovery + - pmn + - Media Controller for IP Fabrics + - vxlan + - Automation, Compliance, and Management for + NX-OS and Other devices + + """ + return self.properties.get("filter") + + @filter.setter + def filter(self, value): + self.properties["filter"] = value + + @property + def oper_state(self): + """ + - Return the oper_state for the filtered feature, if it exists. + - Return None otherwise + - Possible values: + - started + - stopped + - "" + """ + return self._get("oper_state") + + @property + def started(self): + """ + - Return True if the filtered feature oper_state is "started". + - Return False otherwise. + - Possible values: + - True + - False + """ + if self.oper_state == "started": + return True + return False + + @property + def response_data(self): + """ + Return the data retrieved from the request + """ + return self.properties.get("response_data") + + @property + def result(self): + """ + Return the GET result from the Controller + """ + return self.properties.get("result") + + @property + def response(self): + """ + Return the GET response from the Controller + """ + return self.properties.get("response") + + @property + def rest_send(self): + """ + - An instance of the RestSend class. + - Raise ``TypeError`` if the value is not an instance of RestSend. + """ + return self.properties["rest_send"] + + @rest_send.setter + def rest_send(self, value): + test = None + msg = f"{self.class_name}.rest_send must be an instance of RestSend. " + try: + test = value.class_name + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if test != "RestSend": + self.log.debug(msg) + raise TypeError(msg) + self.properties["rest_send"] = value From fed0bb4d043559bfcfc5da4179bd79adeb1424fa Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 May 2024 15:14:24 -1000 Subject: [PATCH 003/230] FabricTypes(): add fabric_type to feature mapping FabricTypes(): add a mapping from fabric_type to the feature name required to be enabled on the controller to support fabric_type. FabricTypes().feature_name - property to retrieve the feature_name required to be enabled on the controller given FabricTypes().fabric_type. For example: instance = FabricTypes() instance.fabric_type = "VXLAN_EVPN" feature = instance.feature_name # returns "vxlan" --- plugins/module_utils/fabric/fabric_types.py | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/plugins/module_utils/fabric/fabric_types.py b/plugins/module_utils/fabric/fabric_types.py index 9cd8f9dfa..275314ae6 100644 --- a/plugins/module_utils/fabric/fabric_types.py +++ b/plugins/module_utils/fabric/fabric_types.py @@ -65,15 +65,25 @@ def _init_fabric_types(self) -> None: This is the single place to add new fabric types. Initialize the following: + - fabric_type_to_feature_name_map dict() - fabric_type_to_template_name_map dict() - _valid_fabric_types - Sorted list() of fabric types - _mandatory_payload_keys_all_fabrics list() """ self._fabric_type_to_template_name_map = {} + self._fabric_type_to_template_name_map["IPFM"] = "Easy_Fabric_IPFM" self._fabric_type_to_template_name_map["LAN_CLASSIC"] = "LAN_Classic" self._fabric_type_to_template_name_map["VXLAN_EVPN"] = "Easy_Fabric" self._fabric_type_to_template_name_map["VXLAN_EVPN_MSD"] = "MSD_Fabric" + # Map fabric type to the feature name that must be running + # on the controller to enable the fabric type. + self._fabric_type_to_feature_name_map = {} + self._fabric_type_to_feature_name_map["VXLAN_EVPN"] = "vxlan" + self._fabric_type_to_feature_name_map["VXLAN_EVPN_MSD"] = "vxlan" + self._fabric_type_to_feature_name_map["LAN_CLASSIC"] = "lan" + self._fabric_type_to_feature_name_map["IPFM"] = "pmn" + self._valid_fabric_types = sorted(self._fabric_type_to_template_name_map.keys()) self._mandatory_parameters_all_fabrics = [] @@ -81,6 +91,9 @@ def _init_fabric_types(self) -> None: self._mandatory_parameters_all_fabrics.append("FABRIC_TYPE") self._mandatory_parameters = {} + self._mandatory_parameters["IPFM"] = copy.copy( + self._mandatory_parameters_all_fabrics + ) self._mandatory_parameters["LAN_CLASSIC"] = copy.copy( self._mandatory_parameters_all_fabrics ) @@ -127,6 +140,20 @@ def fabric_type(self, value): raise ValueError(msg) self._properties["fabric_type"] = value + @property + def feature_name(self): + """ + - getter: Return the feature name that must be enabled on the controller + for the currently-set fabric type. + - getter: raise ``ValueError`` if FabricTypes().fabric_type is not set. + """ + if self.fabric_type is None: + msg = f"{self.class_name}.feature_name: " + msg += f"Set {self.class_name}.fabric_type before accessing " + msg += f"{self.class_name}.feature_name" + raise ValueError(msg) + return self._fabric_type_to_feature_name_map[self.fabric_type] + @property def mandatory_parameters(self): """ From 9a95506f9e27565f4b8d3d3556ae48b2c4f225b6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 May 2024 15:19:28 -1000 Subject: [PATCH 004/230] Verify controller feature is enabled for fabric_type dcnm_fabric.py: Modify Merged() and Replaced() classes to leverage ControllerFeatures() and FabricTypes() to verify that appropriate feature is enabled on the controller prior to initiating operations on a given fabric. --- plugins/modules/dcnm_fabric.py | 62 ++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/plugins/modules/dcnm_fabric.py b/plugins/modules/dcnm_fabric.py index 81b39ded3..0ba389cad 100644 --- a/plugins/modules/dcnm_fabric.py +++ b/plugins/modules/dcnm_fabric.py @@ -1922,6 +1922,8 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError +from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_features import \ + ControllerFeatures from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend @@ -1981,6 +1983,8 @@ def __init__(self, params): self.log.debug(msg) self.endpoints = ApiEndpoints() + self.controller_features = ControllerFeatures(params) + self.features = {} self._implemented_states = set() @@ -2049,6 +2053,34 @@ def get_want(self) -> None: for config in merged_configs: self.want.append(copy.deepcopy(config)) + def get_controller_features(self) -> None: + """ + - Retrieve the state of relevant controller features + - Populate self.features + - key: FABRIC_TYPE + - value: True or False + - True if feature is started for this fabric type + - False otherwise + """ + method_name = inspect.stack()[0][3] + self.features = {} + msg = f"{self.class_name}.{method_name}: " + msg + f"params = {json_pretty(self.params)}" + self.log.debug(msg) + self.controller_features.rest_send = RestSend(self.ansible_module) + try: + self.controller_features.refresh() + except ControllerResponseError as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Controller returned error when attempting to retrieve " + msg += "controller features. " + msg += f"Error detail: {error}" + self.ansible_module.fail_json(f"{msg}", **self.results.failed_result) + for fabric_type in self.fabric_types.valid_fabric_types: + self.fabric_types.fabric_type = fabric_type + self.controller_features.filter = self.fabric_types.feature_name + self.features[fabric_type] = self.controller_features.started + @property def ansible_module(self): """ @@ -2167,13 +2199,22 @@ def get_need(self): Build self.need for merged state """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] self.payloads = {} for want in self.want: fabric_name = want.get("FABRIC_NAME", None) fabric_type = want.get("FABRIC_TYPE", None) + if self.features[fabric_type] is False: + msg = f"{self.class_name}.{method_name}: " + msg += f"Features required for fabric {fabric_name} " + msg += f"of type {fabric_type} are not running on the " + msg += "controller. Review controller settings at " + msg += "Fabric Controller -> Admin -> System Settings -> " + msg += "Feature Management" + self.ansible_module.fail_json(f"{msg}", **self.results.failed_result) + try: self._verify_playbook_params.config_playbook = want except TypeError as error: @@ -2257,6 +2298,7 @@ def commit(self): self.fabric_details.rest_send = self.rest_send self.fabric_summary.rest_send = self.rest_send + self.get_controller_features() self.get_want() self.get_have() self.get_need() @@ -2421,11 +2463,26 @@ def get_need(self): Build self.need for replaced state """ + method_name = inspect.stack()[0][3] self.payloads = {} for want in self.want: + + fabric_name = want.get("FABRIC_NAME", None) + fabric_type = want.get("FABRIC_TYPE", None) + # Skip fabrics that do not exist on the controller - if want["FABRIC_NAME"] not in self.have.all_data: + if fabric_name not in self.have.all_data: continue + + if self.features[fabric_type] is False: + msg = f"{self.class_name}.{method_name}: " + msg += f"Features required for fabric {fabric_name} " + msg += f"of type {fabric_type} are not running on the " + msg += "controller. Review controller settings at " + msg += "Fabric Controller -> Admin -> System Settings -> " + msg += "Feature Management" + self.ansible_module.fail_json(f"{msg}", **self.results.failed_result) + self.need_replaced.append(want) def commit(self): @@ -2440,6 +2497,7 @@ def commit(self): self.fabric_details.rest_send = self.rest_send self.fabric_summary.rest_send = self.rest_send + self.get_controller_features() self.get_want() self.get_have() self.get_need() From d470cb8bd64fec3ab8ca8d54bfba1fa86c1950c8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 May 2024 15:46:31 -1000 Subject: [PATCH 005/230] Remove debug message --- plugins/modules/dcnm_fabric.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/modules/dcnm_fabric.py b/plugins/modules/dcnm_fabric.py index 0ba389cad..582f5af6b 100644 --- a/plugins/modules/dcnm_fabric.py +++ b/plugins/modules/dcnm_fabric.py @@ -2064,9 +2064,6 @@ def get_controller_features(self) -> None: """ method_name = inspect.stack()[0][3] self.features = {} - msg = f"{self.class_name}.{method_name}: " - msg + f"params = {json_pretty(self.params)}" - self.log.debug(msg) self.controller_features.rest_send = RestSend(self.ansible_module) try: self.controller_features.refresh() From 939a698f951d2b0b173c6c62d935a1d21f452012 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 May 2024 16:38:31 -1000 Subject: [PATCH 006/230] FabricDelete().register_result(): fabric_name needs to be upper-case --- plugins/module_utils/fabric/delete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/fabric/delete.py b/plugins/module_utils/fabric/delete.py index e802a2dc9..9c8b6ddc3 100644 --- a/plugins/module_utils/fabric/delete.py +++ b/plugins/module_utils/fabric/delete.py @@ -289,7 +289,7 @@ def register_result(self, fabric_name): return if self.rest_send.result_current.get("success", None) is True: - self.results.diff_current = {"fabric_name": fabric_name} + self.results.diff_current = {"FABRIC_NAME": fabric_name} # need this to match the else clause below since we # pass response_current (altered or not) to the results object response_current = copy.deepcopy(self.rest_send.response_current) From 9ad7d0b2cf6de3a06d698d2d0bfe7d4d90eb7ba6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 May 2024 16:39:49 -1000 Subject: [PATCH 007/230] dcnm_fabric IT: Add dcnm_fabric_merged_basic_ipfm --- .../tests/dcnm_fabric_merged_basic_ipfm.yaml | 409 ++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_basic_ipfm.yaml diff --git a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_basic_ipfm.yaml b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_basic_ipfm.yaml new file mode 100644 index 000000000..0c0638c95 --- /dev/null +++ b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_basic_ipfm.yaml @@ -0,0 +1,409 @@ +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 00:77.09 +################################################################################ +# DESCRIPTION - BASIC FABRIC MERGED STATE TEST for IPFM +# +# Test basic merge of new IPFM fabric configuration and verify results. +# - config-save and config-deploy not tested here. +# - See dcnm_fabric_merged_save_deploy_ipfm.yaml instead. +################################################################################ +# STEPS +################################################################################ +# SETUP +################################################################################ +# 1. The following fabrics must be empty on the controller +# See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml +# - fabric_name_4 +# - fabric_type_4 # IPFM +# 3. Delete fabrics under test, if they exist +# - fabric_name_4 +################################################################################ +# TEST +################################################################################ +# 4. Create fabrics and verify result +# - fabric_name_4 +# 5. Merge additional configs into fabric_4 and verify result +################################################################################ +# CLEANUP +################################################################################ +# 6. Delete fabrics under test +# - fabric_name_4 +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_image_policy integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: deleted +# fabric_name_4: IPFM_Fabric +# fabric_type_4: IPFM +################################################################################ +# MERGED - SETUP - Delete fabrics +################################################################################ +- name: MERGED - SETUP - Delete fabrics + cisco.dcnm.dcnm_fabric: + state: deleted + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + register: result +- debug: + var: result +################################################################################ +# MERGED - TEST - Create IPFM fabric type with basic config +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "create", +# "check_mode": false, +# "sequence_number": 1, +# "state": "merged" +# } +# ], +# "response": [ +# { +# "DATA": { +# "nvPairs": { +# "FABRIC_NAME": "IPFM_Fabric" +# } +# }, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/Easy_Fabric_IPFM", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: MERGED - TEST - Create all supported fabric types with minimal config + cisco.dcnm.dcnm_fabric: + state: merged + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + FABRIC_TYPE: "{{ fabric_type_4 }}" + DEPLOY: true + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].FABRIC_NAME == fabric_name_4 + - result.diff[0].sequence_number == 1 + - result.metadata[0].action == "create" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - (result.response | length) == 1 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "POST" + - result.response[0].RETURN_CODE == 200 +################################################################################ +# MERGED - TEST - Merge additional valid configs into fabric_4 +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_MTU: "1500", +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# }, +# { +# "sequence_number": 3 +# }, +# { +# "sequence_number": 4 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "update", +# "check_mode": false, +# "sequence_number": 1, +# "state": "merged" +# }, +# { +# "action": "config_save", +# "check_mode": false, +# "sequence_number": 2, +# "state": "merged" +# }, +# { +# "action": "config_deploy", +# "check_mode": false, +# "sequence_number": 3, +# "state": "merged" +# } +# ], +# "response": [ +# { +# "DATA": { +# "nvPairs": { +# "FABRIC_MTU: "1500", +# "FABRIC_NAME": "IPFM_Fabric", +# } +# }, +# "MESSAGE": "OK", +# "METHOD": "PUT", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/Easy_Fabric_IPFM", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# }, +# { +# "MESSAGE": "Fabric IPFM_Fabric DEPLOY is False or None. Skipping config-save.", +# "RETURN_CODE": 200, +# "sequence_number": 2 +# }, +# { +# "MESSAGE": "Fabric IPFM_Fabric DEPLOY is False or None. Skipping config-deploy.", +# "RETURN_CODE": 200, +# "sequence_number": 3 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# }, +# { +# "changed": true, +# "sequence_number": 2, +# "success": true +# }, +# { +# "changed": true, +# "sequence_number": 3, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: MERGED - TEST - Merge additional configs into fabric_4 + cisco.dcnm.dcnm_fabric: &merge_fabric_4 + state: merged + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + FABRIC_TYPE: "{{ fabric_type_4 }}" + FABRIC_MTU: 1500 + DEPLOY: false + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 3 + - result.diff[0].FABRIC_NAME == fabric_name_4 + - result.diff[0].FABRIC_MTU == "1500" + - result.diff[0].sequence_number == 1 + - result.diff[1].sequence_number == 2 + - result.diff[2].sequence_number == 3 + - (result.metadata | length) == 3 + - result.metadata[0].action == "update" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - result.metadata[1].action == "config_save" + - result.metadata[1].check_mode == False + - result.metadata[1].sequence_number == 2 + - result.metadata[1].state == "merged" + - result.metadata[2].action == "config_deploy" + - result.metadata[2].check_mode == False + - result.metadata[2].sequence_number == 3 + - result.metadata[2].state == "merged" + - (result.response | length) == 3 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "PUT" + - result.response[0].RETURN_CODE == 200 + - result.response[0].DATA.nvPairs.FABRIC_MTU == "1500" + - result.response[1].sequence_number == 2 + - result.response[1].RETURN_CODE == 200 + - result.response[1].MESSAGE is match '.*Skipping config-save.*' + - result.response[2].sequence_number == 3 + - result.response[2].RETURN_CODE == 200 + - result.response[2].MESSAGE is match '.*Skipping config-deploy.*' + - (result.result | length) == 3 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 + - result.result[1].changed == true + - result.result[1].success == true + - result.result[1].sequence_number == 2 + - result.result[2].changed == true + - result.result[2].success == true + - result.result[2].sequence_number == 3 +################################################################################ +# MERGED - TEST - Merge additional valid configs into fabric_4 - idempotence +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [ndfc1] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "update", +# "check_mode": false, +# "sequence_number": 1, +# "state": "merged" +# } +# ], +# "response": [ +# { +# "MESSAGE": "No fabrics to update for merged state.", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": false, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: MERGED - TEST - Merge additional config into fabric_4 - idempotence + cisco.dcnm.dcnm_fabric: *merge_fabric_4 + register: result +- debug: + var: result +- assert: + that: + - result.changed == false + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "update" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - (result.response | length) == 1 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "No fabrics to update for merged state." + - result.response[0].RETURN_CODE == 200 + - (result.result | length) == 1 + - result.result[0].changed == false + - result.result[0].success == true + - result.result[0].sequence_number == 1 +################################################################################ +# MERGED - CLEANUP - Delete fabric_4 +################################################################################ +# Expected result +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "delete", +# "check_mode": false, +# "sequence_number": 1, +# "state": "deleted" +# } +# ], +# "response": [ +# { +# "DATA": "Fabric 'IPFM_Fabric' is deleted successfully!", +# "MESSAGE": "OK", +# "METHOD": "DELETE", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: MERGED - CLEANUP - Delete fabric_4 + cisco.dcnm.dcnm_fabric: + state: deleted + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].FABRIC_NAME == fabric_name_4 + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "delete" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "deleted" + - (result.response | length) == 1 + - result.response[0].DATA is match '.*deleted successfully.*' + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "DELETE" + - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 From dd1540a11bc6070d7c0db311095c4dc21629b32c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 May 2024 16:53:10 -1000 Subject: [PATCH 008/230] dcnm_fabric IT: Add dcnm_tests.yaml with approprate vars Includes all vars required for the test cases listed. --- playbooks/roles/dcnm_fabric/dcnm_tests.yaml | 43 +++++++-------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml index ec82ee92a..98a583609 100644 --- a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml +++ b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml @@ -10,35 +10,20 @@ vars: # This testcase field can run any test in the tests directory for the role - testcase: spine_leaf_basic - fabric_name: fabric-name - spine1: n9k-spine1.example.com - spine2: n9k-spine2.example.com - leaf1: n9k-leaf1.example.com - leaf2: n9k-leaf2.example.com - leaf3: n9k-leaf3.example.com - leaf4: n9k-leaf4.example.com - username: admin - password: "secret-password" + # testcase: dcnm_fabric_deleted_basic + testcase: dcnm_fabric_merged_basic + # testcase: dcnm_fabric_merged_save_deploy + # testcase: dcnm_fabric_merged_basic_ipfm + # testcase: dcnm_fabric_replaced_basic + # testcase: dcnm_fabric_replaced_save_deploy + fabric_name_1: VXLAN_EVPN_Fabric + fabric_type_1: VXLAN_EVPN + fabric_name_2: VXLAN_EVPN_MSD_Fabric + fabric_type_2: VXLAN_EVPN_MSD + fabric_name_3: LAN_CLASSIC_Fabric + fabric_type_3: LAN_CLASSIC + fabric_name_4: IPFM_Fabric + fabric_type_4: IPFM roles: - dcnm_fabric - -# Uncomment the following play if you want to verify connectivity between -# host a and host c and d across the vxlan fabric setup by test spine_leaf_basic -# - -# - hosts: nxos -# gather_facts: no -# connection: ansible.netcommon.network_cli -# -# tasks: -# - name: Verify IP reachability for vni 4000 -# nxos_ping: -# dest: 192.168.1.20 -# state: present -# -# - name: Verify IP reachability for vni 7000 -# nxos_ping: -# dest: 192.168.2.20 -# state: present From 6f55123aeb8c5f55c1e9f837eab150d9c92b9e20 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 8 May 2024 16:58:47 -1000 Subject: [PATCH 009/230] dcnm_fabric IT: Add notes regarding controller config --- playbooks/roles/dcnm_fabric/dcnm_tests.yaml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml index 98a583609..7dc1aa4cc 100644 --- a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml +++ b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml @@ -1,9 +1,16 @@ --- # This playbook can be used to execute the dcnm_fabric test role. # -# Replace the vars: section with details for your 2 spine, 4 leaf fabric. -# +# Modify the vars section with details for testing setup. # +# NOTES: +# 1. For the IPFM test cases (dcnm_*_ipfm), ensure that the controller +# is running in IPFM mode. i.e. Ensure that +# Fabric Controller -> Admin -> System Settings -> Feature Management +# "IP Fabric for Media" is checked. +# 2. For all other test cases, ensure that +# Fabric Controller -> Admin -> System Settings -> Feature Management +# "Fabric Builder" is checked. - hosts: dcnm gather_facts: no connection: ansible.netcommon.httpapi From 0abea3f9e8aa3cb438c1163a93e920318ba9e107 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 May 2024 06:41:17 -1000 Subject: [PATCH 010/230] Update unit tests to reflect addition of IPFM fabric type --- tests/unit/modules/dcnm/dcnm_fabric/test_fabric_common.py | 2 +- tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py | 2 +- tests/unit/modules/dcnm/dcnm_fabric/test_fabric_types.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_common.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_common.py index f1b59e765..214e608d0 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_common.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_common.py @@ -386,7 +386,7 @@ def test_fabric_common_00112(fabric_common, fabric_name, expected) -> None: MATCH_00113a += r"Playbook configuration for fabric .* contains an invalid\s+" MATCH_00113a += r"FABRIC_TYPE\s+\(.*\)\.\s+" MATCH_00113a += r"Valid values for FABRIC_TYPE:\s+" -MATCH_00113a += r"\['LAN_CLASSIC', 'VXLAN_EVPN', 'VXLAN_EVPN_MSD'\]\.\s+" +MATCH_00113a += r"\[.*]\.\s+" MATCH_00113a += r"Bad configuration:\s+" diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py index ca1df3aa5..6de2dc84a 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py @@ -350,7 +350,7 @@ def mock_dcnm_send(*args, **kwargs): assert len(instance.results.result) == 1 assert instance.results.diff[0].get("sequence_number", None) == 1 - assert instance.results.diff[0].get("fabric_name", None) == "f1" + assert instance.results.diff[0].get("FABRIC_NAME", None) == "f1" assert instance.results.metadata[0].get("action", None) == "delete" assert instance.results.metadata[0].get("check_mode", None) is False diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_types.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_types.py index 2b9ae3d86..b4d2bdc6c 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_types.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_types.py @@ -58,7 +58,7 @@ def test_fabric_types_00010(fabric_types) -> None: MATCH_00020 = r"FabricTypes\.fabric_type.setter:\s+" MATCH_00020 += r"Invalid fabric type: INVALID_FABRIC_TYPE.\s+" -MATCH_00020 += r"Expected one of: LAN_CLASSIC, VXLAN_EVPN, VXLAN_EVPN_MSD\." +MATCH_00020 += r"Expected one of:\s+.*\." @pytest.mark.parametrize( From 1dd1f8f35bab97e32bd8f5fa3dbd1972b2dd8ba0 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 May 2024 07:04:02 -1000 Subject: [PATCH 011/230] Standardize API endpoint definition and access Standardize how API endpoints are defined and accessed. 1. Create a hierarchical directory structure as follows (we can decide if we want to follow the controller API exactly or not, below parallels exactly): module_utils/common/api module_utils/common/api/v1 module_utils/common/api/v1/configtemplate module_utils/common/api/v1/elastic_service module_utils/common/api/v1/event module_utils/common/api/v1/fm module_utils/common/api/v1/imagemanagement module_utils/common/api/v1/lan_discovery module_utils/common/api/v1/lan_fabric module_utils/common/api/v1/pmn etc... module_utils/common/api/v2 etc... API endpoint definition will then follow the controller's hierarchy per above. Starting with two endpoint classes for v1/fm with this commit. --- plugins/module_utils/common/api/common_api.py | 50 +++++++++++ plugins/module_utils/common/api/v1/common.py | 32 +++++++ plugins/module_utils/common/api/v1/fm.py | 62 +++++++++++++ .../common/controller_features.py | 12 ++- plugins/module_utils/common/endpoints.py | 88 ------------------- 5 files changed, 149 insertions(+), 95 deletions(-) create mode 100644 plugins/module_utils/common/api/common_api.py create mode 100644 plugins/module_utils/common/api/v1/common.py create mode 100644 plugins/module_utils/common/api/v1/fm.py delete mode 100644 plugins/module_utils/common/endpoints.py diff --git a/plugins/module_utils/common/api/common_api.py b/plugins/module_utils/common/api/common_api.py new file mode 100644 index 000000000..f86888622 --- /dev/null +++ b/plugins/module_utils/common/api/common_api.py @@ -0,0 +1,50 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +class CommonApi: + """ + API endpoints common methods and properties. + """ + def __init__(self): + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.CommonApi()") + self.api = "/appcenter/cisco/ndfc/api" + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["path"] = None + self.properties["verb"] = None + + @property + def path(self): + """ + Return the endpoint path. + """ + return self.properties["path"] + + @property + def verb(self): + """ + Return the endpoint verb. + """ + return self.properties["verb"] diff --git a/plugins/module_utils/common/api/v1/common.py b/plugins/module_utils/common/api/v1/common.py new file mode 100644 index 000000000..c7638d0c1 --- /dev/null +++ b/plugins/module_utils/common/api/v1/common.py @@ -0,0 +1,32 @@ +# 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 +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.common_api import CommonApi + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +class Common(CommonApi): + """ + v1 API enpoints common methods and properties. + """ + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.CommonV1()") + self.api_v1 = f"{self.api}/v1" diff --git a/plugins/module_utils/common/api/v1/fm.py b/plugins/module_utils/common/api/v1/fm.py new file mode 100644 index 000000000..b01573c04 --- /dev/null +++ b/plugins/module_utils/common/api/v1/fm.py @@ -0,0 +1,62 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common import Common + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +class FM(Common): + """ + V1 API Feature Manager (FM) endpoints common methods and properties. + """ + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fm = f"{self.api_v1}/fm" + self.log.debug("ENTERED api.v1.Common()") + +class Features(FM): + """ + V1 API Feature Manager (FM) features endpoint. + """ + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + self.log.debug("ENTERED api.v1.fm.Features()") + + def _build_properties(self): + self.properties["path"] = f"{self.fm}/features" + self.properties["verb"] = "GET" + +class Version(FM): + """ + V1 API Feature Manager (FM) about/version endpoint. + """ + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + self.log.debug("ENTERED api.v1.fm.Version()") + + def _build_properties(self): + self.properties["path"] = f"{self.fm}/about/version" + self.properties["verb"] = "GET" diff --git a/plugins/module_utils/common/controller_features.py b/plugins/module_utils/common/controller_features.py index 6be247f92..5433e626e 100644 --- a/plugins/module_utils/common/controller_features.py +++ b/plugins/module_utils/common/controller_features.py @@ -28,8 +28,8 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils -from ansible_collections.cisco.dcnm.plugins.module_utils.common.endpoints import \ - ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm import \ + Features from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError @@ -140,7 +140,7 @@ def __init__(self, params): raise ValueError(msg) self.conversion = ConversionUtils() - self.endpoints = ApiEndpoints() + self.api_features = Features() self._init_properties() def _init_properties(self): @@ -164,10 +164,8 @@ def refresh(self): msg += "before calling commit." raise ValueError(msg) - path = self.endpoints.fm_features.get("path") - verb = self.endpoints.fm_features.get("verb") - self.rest_send.path = path - self.rest_send.verb = verb + self.rest_send.path = self.api_features.path + self.rest_send.verb = self.api_features.verb # Store the current value of check_mode, then disable # check_mode since ControllerFeatures() only reads data diff --git a/plugins/module_utils/common/endpoints.py b/plugins/module_utils/common/endpoints.py deleted file mode 100644 index ef6554a55..000000000 --- a/plugins/module_utils/common/endpoints.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import copy -import inspect -import logging -import re - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ - ConversionUtils - - -class ApiEndpoints: - """ - Endpoints for common API calls - - Usage - - endpoints = ApiEndpoints() - try: - endpoint = endpoints.features - except ValueError as error: - raise ValueError(error) from error - - rest_send = RestSend(self.ansible_module) - rest_send.path = endpoint.get("path") - rest_send.verb = endpoint.get("verb") - rest_send.commit() - """ - - def __init__(self): - self.class_name = self.__class__.__name__ - - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED ApiEndpoints()") - - self.conversion = ConversionUtils() - - self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" - self.endpoint_fm = f"{self.endpoint_api_v1}/fm" - - self.endpoint_features = f"{self.endpoint_fm}/features" - - self._init_properties() - - def _init_properties(self): - """ """ - self.properties = {} - - @property - def fm_features(self): - """ - - return feature manager features endpoint - - verb: GET - - path: /appcenter/cisco/ndfc/api/v1/fm/features - """ - endpoint = {} - endpoint["path"] = f"{self.endpoint_fm}/features" - endpoint["verb"] = "GET" - return endpoint - - @property - def fm_version(self): - """ - - return feature manager version endpoint - - verb: GET - - path: /appcenter/cisco/ndfc/api/v1/fm/about/version - """ - endpoint = {} - endpoint["path"] = f"{self.endpoint_fm}/about/version" - endpoint["verb"] = "GET" - return endpoint From 9e50baebf124b058737adc7120ed681827e1dd44 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 May 2024 07:46:48 -1000 Subject: [PATCH 012/230] dcnm_fabric IT: Add dcnm_fabric_merged_save_deploy_ipfm Also, add leaf_1 and leaf_2 vars. leaf_1 is needed for IPFM IT leaf _1 and leaf_2 are needed for VXLAN_EVPN and LAN_CLASSIC IT. --- playbooks/roles/dcnm_fabric/dcnm_tests.yaml | 7 +- .../dcnm_fabric_merged_save_deploy_ipfm.yaml | 467 ++++++++++++++++++ 2 files changed, 472 insertions(+), 2 deletions(-) create mode 100644 tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy_ipfm.yaml diff --git a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml index 7dc1aa4cc..4e39237ac 100644 --- a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml +++ b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml @@ -18,11 +18,12 @@ vars: # This testcase field can run any test in the tests directory for the role # testcase: dcnm_fabric_deleted_basic - testcase: dcnm_fabric_merged_basic + # testcase: dcnm_fabric_merged_basic # testcase: dcnm_fabric_merged_save_deploy - # testcase: dcnm_fabric_merged_basic_ipfm # testcase: dcnm_fabric_replaced_basic # testcase: dcnm_fabric_replaced_save_deploy + # testcase: dcnm_fabric_merged_basic_ipfm + testcase: dcnm_fabric_merged_save_deploy_ipfm fabric_name_1: VXLAN_EVPN_Fabric fabric_type_1: VXLAN_EVPN fabric_name_2: VXLAN_EVPN_MSD_Fabric @@ -31,6 +32,8 @@ fabric_type_3: LAN_CLASSIC fabric_name_4: IPFM_Fabric fabric_type_4: IPFM + leaf_1: 172.22.150.103 + leaf_2: 172.22.150.104 roles: - dcnm_fabric diff --git a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy_ipfm.yaml b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy_ipfm.yaml new file mode 100644 index 000000000..f4410962e --- /dev/null +++ b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy_ipfm.yaml @@ -0,0 +1,467 @@ +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 00:77.09 +################################################################################ +# DESCRIPTION - BASIC FABRIC MERGED STATE TEST for IPFM +# +# Test basic merge of new IPFM fabric configuration and verify results. +# - config-save and config-deploy not tested here. +# - See dcnm_fabric_merged_save_deploy_ipfm.yaml instead. +################################################################################ +# STEPS +################################################################################ +# SETUP +################################################################################ +# 1. The following fabrics must be empty on the controller +# See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml +# - fabric_name_4 +# - fabric_type_4 # IPFM +# 3. Delete fabrics under test, if they exist +# - fabric_name_4 +################################################################################ +# TEST +################################################################################ +# 4. Create fabrics and verify result +# - fabric_name_4 +# 5. Merge additional configs into fabric_4 and verify result +################################################################################ +# CLEANUP +################################################################################ +# 6. Delete fabrics under test +# - fabric_name_4 +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_image_policy integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: deleted +# fabric_name_4: IPFM_Fabric +# fabric_type_4: IPFM +# leaf_1: 172.22.150.103 +################################################################################ +# MERGED - SETUP - Delete fabrics +################################################################################ +- name: MERGED - SETUP - Delete fabrics + cisco.dcnm.dcnm_fabric: + state: deleted + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + register: result +- debug: + var: result +################################################################################ +# MERGED - TEST - Create IPFM fabric type with basic config +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "create", +# "check_mode": false, +# "sequence_number": 1, +# "state": "merged" +# } +# ], +# "response": [ +# { +# "DATA": { +# "nvPairs": { +# "FABRIC_NAME": "IPFM_Fabric" +# } +# }, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/Easy_Fabric_IPFM", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: MERGED - TEST - Create IPFM fabric_4 with minimal config + cisco.dcnm.dcnm_fabric: + state: merged + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + FABRIC_TYPE: "{{ fabric_type_4 }}" + DEPLOY: true + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].FABRIC_NAME == fabric_name_4 + - result.diff[0].sequence_number == 1 + - result.metadata[0].action == "create" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - (result.response | length) == 1 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "POST" + - result.response[0].RETURN_CODE == 200 +################################################################################ +# MERGED - TEST - Add one leaf switch to fabric_4 +################################################################################ +- name: Merge leaf_1 into fabric_4 + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name_4 }}" + state: merged + config: + - seed_ip: "{{ leaf_1 }}" + auth_proto: MD5 + user_name: admin + password: Cisco!2345 + max_hops: 0 + role: leaf + preserve_config: false + register: result +- debug: + var: result +################################################################################ +# MERGED - TEST - Merge additional valid configs into fabric_4 with DEPLOY true +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_MTU: "1500", +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# }, +# { +# "FABRIC_NAME": "IPFM_Fabric", +# "config_save": "OK", +# "sequence_number": 2 +# }, +# { +# "FABRIC_NAME": "IPFM_Fabric", +# "config_deploy": "OK", +# "sequence_number": 3 +# }, +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "update", +# "check_mode": false, +# "sequence_number": 1, +# "state": "merged" +# }, +# { +# "action": "config_save", +# "check_mode": false, +# "sequence_number": 2, +# "state": "merged" +# }, +# { +# "action": "config_deploy", +# "check_mode": false, +# "sequence_number": 3, +# "state": "merged" +# } +# ], +# "response": [ +# { +# "DATA": { +# "nvPairs": { +# "FABRIC_MTU: "1500", +# "FABRIC_NAME": "IPFM_Fabric", +# } +# }, +# "MESSAGE": "OK", +# "METHOD": "PUT", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/Easy_Fabric_IPFM", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# }, +# { +# "DATA": { +# "status": "Config save is completed" +# }, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/config-save", +# "RETURN_CODE": 200, +# "sequence_number": 2 +# }, +# { +# "DATA": { +# "status": "Configuration deployment completed." +# }, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/config-deploy?forceShowRun=false", +# "RETURN_CODE": 200, +# "sequence_number": 3 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# }, +# { +# "changed": true, +# "sequence_number": 2, +# "success": true +# }, +# { +# "changed": true, +# "sequence_number": 3, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: MERGED - TEST - Merge additional configs into fabric_4 with DEPLOY true + cisco.dcnm.dcnm_fabric: &merge_fabric_4 + state: merged + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + FABRIC_TYPE: "{{ fabric_type_4 }}" + FABRIC_MTU: 1500 + DEPLOY: true + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 3 + - result.diff[0].FABRIC_NAME == fabric_name_4 + - result.diff[0].FABRIC_MTU == "1500" + - result.diff[0].sequence_number == 1 + - result.diff[1].FABRIC_NAME == fabric_name_4 + - result.diff[1].config_save == "OK" + - result.diff[1].sequence_number == 2 + - result.diff[2].FABRIC_NAME == fabric_name_4 + - result.diff[2].config_deploy == "OK" + - result.diff[2].sequence_number == 3 + - (result.metadata | length) == 3 + - result.metadata[0].action == "update" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - result.metadata[1].action == "config_save" + - result.metadata[1].check_mode == False + - result.metadata[1].sequence_number == 2 + - result.metadata[1].state == "merged" + - result.metadata[2].action == "config_deploy" + - result.metadata[2].check_mode == False + - result.metadata[2].sequence_number == 3 + - result.metadata[2].state == "merged" + - (result.response | length) == 3 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "PUT" + - result.response[0].RETURN_CODE == 200 + - result.response[0].DATA.nvPairs.FABRIC_MTU == "1500" + - result.response[1].DATA.status is match 'Config save is completed' + - result.response[1].MESSAGE == "OK" + - result.response[1].RETURN_CODE == 200 + - result.response[1].sequence_number == 2 + - result.response[2].DATA.status is match 'Configuration deployment completed.' + - result.response[2].MESSAGE == "OK" + - result.response[2].RETURN_CODE == 200 + - result.response[2].sequence_number == 3 + - (result.result | length) == 3 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 + - result.result[1].changed == true + - result.result[1].success == true + - result.result[1].sequence_number == 2 + - result.result[2].changed == true + - result.result[2].success == true + - result.result[2].sequence_number == 3 +################################################################################ +# MERGED - TEST - Merge additional valid configs into fabric_4 - idempotence +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [ndfc1] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "update", +# "check_mode": false, +# "sequence_number": 1, +# "state": "merged" +# } +# ], +# "response": [ +# { +# "MESSAGE": "No fabrics to update for merged state.", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": false, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: MERGED - TEST - Merge additional config into fabric_4 - idempotence + cisco.dcnm.dcnm_fabric: *merge_fabric_4 + register: result +- debug: + var: result +- assert: + that: + - result.changed == false + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "update" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - (result.response | length) == 1 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "No fabrics to update for merged state." + - result.response[0].RETURN_CODE == 200 + - (result.result | length) == 1 + - result.result[0].changed == false + - result.result[0].success == true + - result.result[0].sequence_number == 1 +################################################################################ +# MERGED - CLEANUP - Delete switch from fabric_4 +################################################################################ +- name: Delete switch from fabric_4 + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name_4 }}" + state: deleted + config: + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.response | length) == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "DELETE" + - result.response[0].RETURN_CODE == 200 +################################################################################ +# MERGED - CLEANUP - Delete fabric_4 +################################################################################ +# Expected result +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "delete", +# "check_mode": false, +# "sequence_number": 1, +# "state": "deleted" +# } +# ], +# "response": [ +# { +# "DATA": "Fabric 'IPFM_Fabric' is deleted successfully!", +# "MESSAGE": "OK", +# "METHOD": "DELETE", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: MERGED - CLEANUP - Delete fabric_4 + cisco.dcnm.dcnm_fabric: + state: deleted + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].FABRIC_NAME == fabric_name_4 + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "delete" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "deleted" + - (result.response | length) == 1 + - result.response[0].DATA is match '.*deleted successfully.*' + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "DELETE" + - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 From 037d2d34a2ce6d59ac1ef887f880756765b7ee4d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 May 2024 08:59:54 -1000 Subject: [PATCH 013/230] dcnm_fabric IT: Add dcnm_fabric_replaced_save_deploy_ipfm Also, update comments in other IT regarding nxos credentials. --- playbooks/roles/dcnm_fabric/dcnm_tests.yaml | 3 +- .../tests/dcnm_fabric_merged_save_deploy.yaml | 13 +- .../dcnm_fabric_merged_save_deploy_ipfm.yaml | 9 +- .../tests/dcnm_fabric_replaced_basic.yaml | 2 +- .../dcnm_fabric_replaced_save_deploy.yaml | 5 +- ...dcnm_fabric_replaced_save_deploy_ipfm.yaml | 471 ++++++++++++++++++ 6 files changed, 492 insertions(+), 11 deletions(-) create mode 100644 tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_save_deploy_ipfm.yaml diff --git a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml index 4e39237ac..059cca1ac 100644 --- a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml +++ b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml @@ -23,7 +23,8 @@ # testcase: dcnm_fabric_replaced_basic # testcase: dcnm_fabric_replaced_save_deploy # testcase: dcnm_fabric_merged_basic_ipfm - testcase: dcnm_fabric_merged_save_deploy_ipfm + # testcase: dcnm_fabric_merged_save_deploy_ipfm + # testcase: dcnm_fabric_replaced_save_deploy_ipfm fabric_name_1: VXLAN_EVPN_Fabric fabric_type_1: VXLAN_EVPN fabric_name_2: VXLAN_EVPN_MSD_Fabric diff --git a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy.yaml b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy.yaml index de9d48c20..b53516dd6 100644 --- a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy.yaml +++ b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy.yaml @@ -45,7 +45,8 @@ # REQUIREMENTS ################################################################################ # Example vars for dcnm_fabric integration tests -# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# Add fabric and leaf vars to cisco/dcnm/playbooks/dcnm_tests.yaml +# Add nxos_username and nxos_password vars to cisco/dcnm/playbooks/dcnm_hosts.yaml # # vars: # # This testcase field can run any test in the tests directory for the role @@ -56,6 +57,8 @@ # fabric_type_3: LAN_CLASSIC # leaf_1: 172.22.150.103 # leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword ################################################################################ # MERGED - SETUP - Delete fabrics ################################################################################ @@ -215,8 +218,8 @@ config: - seed_ip: "{{ leaf_1 }}" auth_proto: MD5 - user_name: admin - password: Cisco!2345 + user_name: "{{ nxos_username}}" + password: "{{ nxos_password }}" max_hops: 0 role: leaf preserve_config: false @@ -231,8 +234,8 @@ config: - seed_ip: "{{ leaf_2 }}" auth_proto: MD5 - user_name: admin - password: Cisco!2345 + user_name: "{{ nxos_username}}" + password: "{{ nxos_password }}" max_hops: 0 role: leaf # preserve_config must be True for LAN_CLASSIC diff --git a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy_ipfm.yaml b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy_ipfm.yaml index f4410962e..d799f900c 100644 --- a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy_ipfm.yaml +++ b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy_ipfm.yaml @@ -35,7 +35,8 @@ # REQUIREMENTS ################################################################################ # Example vars for dcnm_image_policy integration tests -# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# Add fabric and leaf vars to cisco/dcnm/playbooks/dcnm_tests.yaml +# Add nxos_username and nxos_password vars to cisco/dcnm/playbooks/dcnm_hosts.yaml # # vars: # # This testcase field can run any test in the tests directory for the role @@ -43,6 +44,8 @@ # fabric_name_4: IPFM_Fabric # fabric_type_4: IPFM # leaf_1: 172.22.150.103 +# nxos_username: admin +# nxos_password: mypassword ################################################################################ # MERGED - SETUP - Delete fabrics ################################################################################ @@ -138,8 +141,8 @@ config: - seed_ip: "{{ leaf_1 }}" auth_proto: MD5 - user_name: admin - password: Cisco!2345 + user_name: "{{ nxos_username}}" + password: "{{ nxos_password }}" max_hops: 0 role: leaf preserve_config: false diff --git a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_basic.yaml b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_basic.yaml index 6bca1d141..445b8092c 100644 --- a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_basic.yaml +++ b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_basic.yaml @@ -41,7 +41,7 @@ # REQUIREMENTS ################################################################################ # Example vars for dcnm_image_policy integration tests -# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml # # vars: # # This testcase field can run any test in the tests directory for the role diff --git a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_save_deploy.yaml b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_save_deploy.yaml index 3545a4a2f..2de009239 100644 --- a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_save_deploy.yaml +++ b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_save_deploy.yaml @@ -45,7 +45,8 @@ # REQUIREMENTS ################################################################################ # Example vars for dcnm_fabric integration tests -# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# Add fabric and leaf vars to cisco/dcnm/playbooks/dcnm_tests.yaml +# Add nxos_username and nxos_password vars to cisco/dcnm/playbooks/dcnm_hosts.yaml # # vars: # # This testcase field can run any test in the tests directory for the role @@ -56,6 +57,8 @@ # fabric_type_3: LAN_CLASSIC # leaf_1: 172.22.150.103 # leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword ################################################################################ ################################################################################ diff --git a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_save_deploy_ipfm.yaml b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_save_deploy_ipfm.yaml new file mode 100644 index 000000000..2ae7c8415 --- /dev/null +++ b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_save_deploy_ipfm.yaml @@ -0,0 +1,471 @@ +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 00:77.09 +################################################################################ +# DESCRIPTION - FABRIC REPLACED STATE TEST with SAVE and DEPLOY for IPFM +# +# Test merge of new fabric configuration and verify results. +# Test config-save and config-deploy on populated fabric. +# - config-save and config-deploy are tested. +# - See dcnm_fabric_merged_basic_ipfm.yaml for quicker test without save/deploy. +################################################################################ +# STEPS +################################################################################ +# SETUP +################################################################################ +# 1. The following fabric must be empty on the controller (or not exist). +# See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml +# - fabric_name_4 +# - fabric_type_4 # IPFM +# 2. Delete fabric under test, if it exists +# - fabric_name_4 +# - fabric_name_4 +################################################################################ +# TEST +################################################################################ +# 3. Create fabric and verify result +# - fabric_name_4 +# 4. Add switch to the fabric and verify result +# - leaf_1 +# 5. Merge additional configs into the fabric and verify result +# 6. Replace fabric config with default config and verify result +################################################################################ +# CLEANUP +################################################################################ +# 7. Delete the switch from the fabric +# - leaf_1 +# 8. Delete the fabric +# - fabric_name_4 +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_fabric integration tests +# Add fabric and leaf vars to cisco/dcnm/playbooks/dcnm_tests.yaml +# Add nxos_username and nxos_password vars to cisco/dcnm/playbooks/dcnm_hosts.yaml +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: deleted +# fabric_name_4: IPFM_Fabric +# fabric_type_4: IPFM +# leaf_1: 172.22.150.103 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ + +################################################################################ +# REPLACED - SETUP - Delete fabrics +################################################################################ +- name: REPLACED - SETUP - Delete fabric + cisco.dcnm.dcnm_fabric: + state: deleted + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + register: result +- debug: + var: result + +################################################################################ +# REPLACED - TEST - Create IPFM fabric using non-default fabric config +# DEPLOY is set to True the fabric but has no effect since the module +# skips config-save and config-deploy for empty fabrics. +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_MTU": 1500, +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# }, +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "create", +# "check_mode": false, +# "sequence_number": 1, +# "state": "merged" +# } +# ], +# "response": [ +# { +# "DATA": { +# "nvPairs": { +# "FABRIC_NAME": "IPFM_Fabric", +# "FABRIC_MTU": "1500" +# } +# }, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/Easy_Fabric_IPFM", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: REPLACED - TEST - Create IPFM fabric with non-default config. + cisco.dcnm.dcnm_fabric: + state: merged + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + FABRIC_TYPE: "{{ fabric_type_4 }}" + FABRIC_MTU: 1500 + DEPLOY: true + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].sequence_number == 1 + - result.diff[0].FABRIC_MTU == 1500 + - (result.metadata | length) == 1 + - result.metadata[0].action == "create" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - (result.response | length) == 1 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "POST" + - result.response[0].RETURN_CODE == 200 + - result.response[0].DATA.nvPairs.FABRIC_MTU == "1500" + - result.response[0].DATA.nvPairs.FABRIC_NAME == fabric_name_4 + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 +################################################################################ +# REPLACED - SETUP - Add leaf_1 to fabric_4 +################################################################################ +- name: Merge leaf_1 into fabric_4 + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name_4 }}" + state: merged + config: + - seed_ip: "{{ leaf_1 }}" + auth_proto: MD5 + user_name: "{{ nxos_username }}" + password: "{{ nxos_password }}" + max_hops: 0 + role: leaf + preserve_config: false + register: result +- debug: + var: result + +################################################################################ +# REPLACED - TEST - Replace fabric_4 config with default config +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [172.22.150.244] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_NAME": "IPFM_Fabric", +# "FABRIC_MTU": 9216, +# "sequence_number": 1 +# }, +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "replace", +# "check_mode": false, +# "sequence_number": 1, +# "state": "replaced" +# } +# ], +# "response": [ +# { +# "DATA": { +# "nvPairs": { +# "FABRIC_MTU": "9216", +# "FABRIC_NAME": "IPFM_Fabric", +# } +# }, +# "MESSAGE": "OK", +# "METHOD": "PUT", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/Easy_Fabric_IPFM", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# }, +# { +# "DATA": { +# "status": "Config save is completed" +# }, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/config-save", +# "RETURN_CODE": 200, +# "sequence_number": 2 +# }, +# { +# "DATA": { +# "status": "Configuration deployment completed." +# }, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/config-deploy?forceShowRun=false", +# "RETURN_CODE": 200, +# "sequence_number": 3 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# }, +# { +# "changed": true, +# "sequence_number": 2, +# "success": true +# }, +# { +# "changed": true, +# "sequence_number": 3, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: REPLACED - TEST - Replace fabric_4 config with default config + cisco.dcnm.dcnm_fabric: &replace_fabric_4 + state: replaced + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + FABRIC_TYPE: "{{ fabric_type_4 }}" + DEPLOY: true + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 3 + - result.diff[0].sequence_number == 1 + - result.diff[0].FABRIC_MTU == "9216" + - result.diff[0].FABRIC_NAME == fabric_name_4 + - result.diff[1].sequence_number == 2 + - result.diff[2].sequence_number == 3 + - (result.metadata | length) == 3 + - result.metadata[0].action == "replace" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "replaced" + - result.metadata[1].action == "config_save" + - result.metadata[1].check_mode == False + - result.metadata[1].sequence_number == 2 + - result.metadata[1].state == "replaced" + - result.metadata[2].action == "config_deploy" + - result.metadata[2].check_mode == False + - result.metadata[2].sequence_number == 3 + - result.metadata[2].state == "replaced" + - (result.response | length) == 3 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "PUT" + - result.response[0].RETURN_CODE == 200 + - result.response[0].DATA.nvPairs.FABRIC_MTU == "9216" + - result.response[0].DATA.nvPairs.FABRIC_NAME == fabric_name_4 + - result.response[1].sequence_number == 2 + - result.response[1].DATA.status == 'Config save is completed' + - result.response[1].MESSAGE == "OK" + - result.response[1].METHOD == "POST" + - result.response[1].RETURN_CODE == 200 + - result.response[2].sequence_number == 3 + - result.response[2].DATA.status == 'Configuration deployment completed.' + - result.response[2].MESSAGE == "OK" + - result.response[2].METHOD == "POST" + - result.response[2].RETURN_CODE == 200 + - (result.result | length) == 3 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 + - result.result[1].changed == true + - result.result[1].success == true + - result.result[1].sequence_number == 2 + - result.result[2].changed == true + - result.result[2].success == true + - result.result[2].sequence_number == 3 + +################################################################################ +# REPLACED - TEST - Replace fabric_4 config with default config - idempotence +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "replace", +# "check_mode": false, +# "sequence_number": 1, +# "state": "replaced" +# } +# ], +# "response": [ +# { +# "MESSAGE": "No fabrics to update for replaced state.", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": false, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: REPLACED - TEST - Replace fabric_4 config with default config - idempotence + cisco.dcnm.dcnm_fabric: *replace_fabric_4 + register: result +- debug: + var: result +- assert: + that: + - result.changed == false + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "replace" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "replaced" + - (result.response | length) == 1 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "No fabrics to update for replaced state." + - result.response[0].RETURN_CODE == 200 + - (result.result | length) == 1 + - result.result[0].changed == false + - result.result[0].success == true + - result.result[0].sequence_number == 1 + +################################################################################ +# REPLACED - CLEANUP - Delete switch from fabric_4 +################################################################################ +- name: Delete switch from fabric_4 + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name_4 }}" + state: deleted + config: + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.response | length) == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "DELETE" + - result.response[0].RETURN_CODE == 200 + +################################################################################ +# REPLACED - CLEANUP - Delete fabric_4 +################################################################################ +# Expected result +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "delete", +# "check_mode": false, +# "sequence_number": 1, +# "state": "deleted" +# } +# ], +# "response": [ +# { +# "DATA": "Fabric 'IPFM_Fabric' is deleted successfully!", +# "MESSAGE": "OK", +# "METHOD": "DELETE", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: REPLACED - CLEANUP - Delete fabric_4 + cisco.dcnm.dcnm_fabric: + state: deleted + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].FABRIC_NAME == fabric_name_4 + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "delete" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "deleted" + - (result.response | length) == 1 + - result.response[0].DATA is match '.*deleted successfully.*' + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "DELETE" + - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 From 99f57edac9918d054f30ac553649a060cfe169a4 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 May 2024 09:03:15 -1000 Subject: [PATCH 014/230] ControllerFeatures(): run thru black and isort --- plugins/module_utils/common/controller_features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/common/controller_features.py b/plugins/module_utils/common/controller_features.py index 5433e626e..e95eeef30 100644 --- a/plugins/module_utils/common/controller_features.py +++ b/plugins/module_utils/common/controller_features.py @@ -26,10 +26,10 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ - ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm import \ Features +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError From 5c320f874e71df14459c0d929d1d3027de71fb7c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 May 2024 09:07:59 -1000 Subject: [PATCH 015/230] Run api endpoint classes thru black, isort, pylint --- plugins/module_utils/common/api/common_api.py | 2 ++ plugins/module_utils/common/api/v1/common.py | 6 +++++- plugins/module_utils/common/api/v1/fm.py | 14 +++++++++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/common/api/common_api.py b/plugins/module_utils/common/api/common_api.py index f86888622..4be680b94 100644 --- a/plugins/module_utils/common/api/common_api.py +++ b/plugins/module_utils/common/api/common_api.py @@ -19,10 +19,12 @@ import logging + class CommonApi: """ API endpoints common methods and properties. """ + def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") diff --git a/plugins/module_utils/common/api/v1/common.py b/plugins/module_utils/common/api/v1/common.py index c7638d0c1..e803b056e 100644 --- a/plugins/module_utils/common/api/v1/common.py +++ b/plugins/module_utils/common/api/v1/common.py @@ -13,17 +13,21 @@ # limitations under the License. from __future__ import absolute_import, division, print_function -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.common_api import CommonApi __metaclass__ = type __author__ = "Allen Robel" import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.common_api import \ + CommonApi + + class Common(CommonApi): """ v1 API enpoints common methods and properties. """ + def __init__(self): super().__init__() self.class_name = self.__class__.__name__ diff --git a/plugins/module_utils/common/api/v1/fm.py b/plugins/module_utils/common/api/v1/fm.py index b01573c04..b526b0d49 100644 --- a/plugins/module_utils/common/api/v1/fm.py +++ b/plugins/module_utils/common/api/v1/fm.py @@ -13,17 +13,21 @@ # limitations under the License. from __future__ import absolute_import, division, print_function -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common import Common __metaclass__ = type __author__ = "Allen Robel" import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common import \ + Common + + class FM(Common): """ V1 API Feature Manager (FM) endpoints common methods and properties. """ + def __init__(self): super().__init__() self.class_name = self.__class__.__name__ @@ -31,32 +35,36 @@ def __init__(self): self.fm = f"{self.api_v1}/fm" self.log.debug("ENTERED api.v1.Common()") + class Features(FM): """ V1 API Feature Manager (FM) features endpoint. """ + def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self._build_properties() self.log.debug("ENTERED api.v1.fm.Features()") - + def _build_properties(self): self.properties["path"] = f"{self.fm}/features" self.properties["verb"] = "GET" + class Version(FM): """ V1 API Feature Manager (FM) about/version endpoint. """ + def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self._build_properties() self.log.debug("ENTERED api.v1.fm.Version()") - + def _build_properties(self): self.properties["path"] = f"{self.fm}/about/version" self.properties["verb"] = "GET" From 6448f704d4fee4af40b9fc192393982655e9aa9a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 May 2024 10:56:50 -1000 Subject: [PATCH 016/230] ControllerFeatures(): Add unit tests, 100% coverage --- .../common/controller_features.py | 74 +- .../unit/module_utils/common/common_utils.py | 65 +- .../responses_ControllerFeatures.json | 632 ++++++++++++++++++ .../common/test_controller_features.py | 355 ++++++++++ 4 files changed, 1092 insertions(+), 34 deletions(-) create mode 100644 tests/unit/module_utils/common/fixtures/responses_ControllerFeatures.json create mode 100644 tests/unit/module_utils/common/test_controller_features.py diff --git a/plugins/module_utils/common/controller_features.py b/plugins/module_utils/common/controller_features.py index e95eeef30..8f478c030 100644 --- a/plugins/module_utils/common/controller_features.py +++ b/plugins/module_utils/common/controller_features.py @@ -23,6 +23,7 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import copy import inspect import logging @@ -136,7 +137,7 @@ def __init__(self, params): self.check_mode = self.params.get("check_mode", None) if self.check_mode is None: msg = f"{self.class_name}.__init__(): " - msg += "check_mode is required" + msg += "check_mode is required." raise ValueError(msg) self.conversion = ConversionUtils() @@ -145,10 +146,11 @@ def __init__(self, params): def _init_properties(self): self.properties = {} - self.properties["data"] = None + self.properties["filter"] = None self.properties["rest_send"] = None self.properties["result"] = None self.properties["response"] = None + self.properties["response_data"] = None def refresh(self): """ @@ -161,7 +163,7 @@ def refresh(self): if self.rest_send is None: msg = f"{self.class_name}.{method_name}: " msg += f"{self.class_name}.rest_send must be set " - msg += "before calling commit." + msg += "before calling refresh()." raise ValueError(msg) self.rest_send.path = self.api_features.path @@ -176,18 +178,22 @@ def refresh(self): self.rest_send.commit() self.rest_send.check_mode = current_check_mode - if self.rest_send.result_current["success"] is False: - msg = f"{self.class_name}.refresh() failed: {self.rest_send.result_current}" + self.properties["result"] = copy.deepcopy(self.rest_send.result_current) + if self.result["success"] is False: + msg = f"{self.class_name}.{method_name}: " + msg += f"Bad controller response: {self.rest_send.response_current}" raise ControllerResponseError(msg) + self.properties["response"] = copy.deepcopy(self.rest_send.response_current) + self.properties["response_data"] = ( self.rest_send.response_current.get("DATA", {}) .get("data", {}) .get("features", {}) ) - if self.response_data is None: - msg = f"{self.class_name}.refresh() failed: response " - msg += "does not contain DATA key. Controller response: " + if self.response_data == {}: + msg = f"{self.class_name}.{method_name}: " + msg += "Controller response does not match expected structure: " msg += f"{self.rest_send.response_current}" raise ControllerResponseError(msg) @@ -260,17 +266,11 @@ def oper_state(self): return self._get("oper_state") @property - def started(self): + def response(self): """ - - Return True if the filtered feature oper_state is "started". - - Return False otherwise. - - Possible values: - - True - - False + Return the GET response from the Controller """ - if self.oper_state == "started": - return True - return False + return self.properties.get("response") @property def response_data(self): @@ -279,32 +279,20 @@ def response_data(self): """ return self.properties.get("response_data") - @property - def result(self): - """ - Return the GET result from the Controller - """ - return self.properties.get("result") - - @property - def response(self): - """ - Return the GET response from the Controller - """ - return self.properties.get("response") - @property def rest_send(self): """ - An instance of the RestSend class. - Raise ``TypeError`` if the value is not an instance of RestSend. """ - return self.properties["rest_send"] + return self.properties.get("rest_send") @rest_send.setter def rest_send(self, value): + method_name = inspect.stack()[0][3] test = None - msg = f"{self.class_name}.rest_send must be an instance of RestSend. " + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of RestSend. " try: test = value.class_name except AttributeError as error: @@ -314,3 +302,23 @@ def rest_send(self, value): self.log.debug(msg) raise TypeError(msg) self.properties["rest_send"] = value + + @property + def result(self): + """ + Return the GET result from the Controller + """ + return self.properties.get("result") + + @property + def started(self): + """ + - Return True if the filtered feature oper_state is "started". + - Return False otherwise. + - Possible values: + - True + - False + """ + if self.oper_state == "started": + return True + return False diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index b04044962..70db881ce 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -23,6 +23,8 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_features import \ + ControllerFeatures from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ ControllerVersion from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log @@ -33,18 +35,62 @@ from .fixture import load_fixture +params = { + "state": "merged", + "config": {"switches": [{"ip_address": "172.22.150.105"}]}, + "check_mode": False, +} + + +class ResponseGenerator: + """ + Given a generator, return the items in the generator with + each call to the next property + + For usage in the context of dcnm_image_policy unit tests, see: + test: test_image_policy_create_bulk_00037 + file: tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py + + Simplified usage example below. + + def responses(): + yield {"key1": "value1"} + yield {"key2": "value2"} + + gen = ResponseGenerator(responses()) + + print(gen.next) # {"key1": "value1"} + print(gen.next) # {"key2": "value2"} + """ + + def __init__(self, gen): + self.gen = gen + + @property + def next(self): + """ + Return the next item in the generator + """ + return next(self.gen) + + def public_method_for_pylint(self) -> Any: + """ + Add one public method to appease pylint + """ + class MockAnsibleModule: """ Mock the AnsibleModule class """ + check_mode = False params = {"config": {"switches": [{"ip_address": "172.22.150.105"}]}} argument_spec = { "config": {"required": True, "type": "dict"}, "state": {"default": "merged", "choices": ["merged", "deleted", "query"]}, - "check_mode": False + "check_mode": False, } supports_check_mode = True @@ -65,6 +111,14 @@ def public_method_for_pylint(self) -> Any: # https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +@pytest.fixture(name="controller_features") +def controller_features_fixture(): + """ + return ControllerFeatures + """ + return ControllerFeatures(params) + + @pytest.fixture(name="controller_version") def controller_version_fixture(): """ @@ -115,6 +169,15 @@ def merge_dicts_data(key: str) -> Dict[str, str]: return data +def responses_controller_features(key: str) -> Dict[str, str]: + """ + Return ControllerFeatures controller responses + """ + response_file = "responses_ControllerFeatures" + response = load_fixture(response_file).get(key) + return response + + def responses_controller_version(key: str) -> Dict[str, str]: """ Return ControllerVersion controller responses diff --git a/tests/unit/module_utils/common/fixtures/responses_ControllerFeatures.json b/tests/unit/module_utils/common/fixtures/responses_ControllerFeatures.json new file mode 100644 index 000000000..70b5ac3a3 --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/responses_ControllerFeatures.json @@ -0,0 +1,632 @@ +{ + "test_controller_features_00040a": { + "DATA": { + "data": { + "features": { + "change-mgmt": { + "admin_state": "disabled", + "description": "Tracking, Approval, and Rollback of all Configuration Changes", + "featureset": { + "lan": { + "default": false + } + }, + "kind": "featurette", + "name": "Change Control", + "oper_state": "", + "predisablecheck": "https://dcnm-lan-fabric.cisco-ndfc.svc:9443/rest/chngmgmt/preDisableCheck", + "spec": "", + "ui": false + }, + "cvisualizer": { + "admin_state": "disabled", + "description": "Network Visualization of K8s Clusters", + "featureset": { + "lan": { + "default": false + } + }, + "kind": "feature", + "name": "Kubernetes Visualizer", + "oper_state": "", + "spec": "", + "ui": false + }, + "elasticservice": { + "admin_state": "enabled", + "apidoc": [ + { + "schema": null, + "subpath": "elastic-service", + "url": "https://dcnm-elasticservice.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "L4-L7 Services", + "featureset": { + "lan": { + "default": true + } + }, + "hidden": true, + "installed": "2024-02-05 19:12:57.098455128 +0000 UTC", + "kind": "feature", + "name": "L4-L7 Services", + "oper_state": "started", + "spec": "", + "ui": true + }, + "epl": { + "admin_state": "disabled", + "apidoc": [ + { + "schema": null, + "subpath": "eplui", + "url": "https://dcnm-eplui.cisco-ndfc.svc:9443/v3/api-docs" + }, + { + "schema": null, + "subpath": "epl", + "url": "https://dcnm-eplapi.cisco-ndfc.svc:8443/v3/api-docs" + } + ], + "description": "Tracking Endpoint IP-MAC Location with Historical Information", + "featureset": { + "lan": { + "default": false + } + }, + "kind": "feature", + "name": "Endpoint Locator", + "oper_state": "stopped", + "predisablecheck": "https://dcnm-eplapi.cisco-ndfc.svc:8443/epl/preDisableCheck", + "spec": "", + "ui": true + }, + "eventmgr-data": { + "admin_state": "disabled", + "apidoc": [ + { + "schema": null, + "subpath": "event", + "url": "https://dcnm-eventmgr.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "Event Management on Data Network", + "featureset": { + "lan": { + "default": true + }, + "lan-base": { + "default": true + }, + "san": { + "default": true + } + }, + "hidden": true, + "kind": "feature", + "name": "Syslog Trap On Data", + "oob_nw_mode": "Data", + "oper_state": "stopped", + "service_network": "Data", + "spec": "", + "ui": false + }, + "eventmgr-mgmt": { + "admin_state": "enabled", + "apidoc": [ + { + "schema": null, + "subpath": "event", + "url": "https://dcnm-eventmgr.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "Event Management on Managemnt Network", + "featureset": { + "lan": { + "default": true + }, + "lan-base": { + "default": true + } + }, + "hidden": true, + "installed": "2024-02-05 19:12:59.354572155 +0000 UTC", + "kind": "feature", + "name": "Syslog Trap On Management", + "oob_nw_mode": "Management", + "oper_state": "started", + "service_ip": "172.22.150.254", + "service_network": "Management", + "spec": "", + "ui": false + }, + "ficon": { + "admin_state": "disabled", + "description": "FICON feature for SAN fabric", + "featureset": { + "san": { + "default": false + } + }, + "kind": "featurette", + "name": "FICON", + "oper_state": "", + "spec": "", + "ui": false + }, + "img-mgmt": { + "admin_state": "enabled", + "apidoc": [ + { + "schema": null, + "subpath": "imagemanagement", + "url": "https://dcnm-imagemanagement.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "Image Management Common", + "featureset": { + "lan": { + "default": true + }, + "lan-base": { + "default": true + }, + "san": { + "default": true + } + }, + "hidden": true, + "installed": "2024-02-05 19:13:05.296678029 +0000 UTC", + "kind": "feature", + "name": "Image Management Common", + "oper_state": "started", + "predisablecheck": "https://dcnm-imagemanagement.cisco-ndfc.svc:9443/rest/policymgnt/imgMgmtPreDisableCheck", + "spec": "", + "ui": false + }, + "infoblox": { + "admin_state": "disabled", + "description": "Integration with IP Address Management (IPAM) Systems", + "featureset": { + "lan": { + "default": false + } + }, + "kind": "feature", + "name": "IPAM Integration", + "oper_state": "", + "spec": "", + "ui": true + }, + "lan": { + "admin_state": "enabled", + "apidoc": [ + { + "schema": null, + "subpath": "lan-fabric/rest", + "url": "https://dcnm-lan-fabric.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "Full LAN functionality in addition to Fabric Discovery", + "featureset": null, + "installed": "2024-02-05 19:13:02.089607918 +0000 UTC", + "kind": "feature-set", + "name": "Fabric Controller", + "oper_state": "started", + "predisablecheck": "https://dcnm-lan-fabric.cisco-ndfc.svc:9443/rest/control/fabrics/lanFabricPreDisableCheck", + "spec": "", + "ui": false + }, + "lan-base": { + "admin_state": "disabled", + "description": "Discovery, Inventory and Topology for LAN deployments", + "featureset": null, + "kind": "feature-set", + "name": "Fabric Discovery", + "oper_state": "", + "spec": "", + "ui": false + }, + "lan-common": { + "admin_state": "enabled", + "apidoc": [ + { + "schema": null, + "subpath": "lan-discovery", + "url": "https://dcnm-lan-discovery.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "Lan Common", + "featureset": { + "lan": { + "default": true + }, + "lan-base": { + "default": true + } + }, + "healthz": "https://dcnm-lan-fabric.cisco-ndfc.svc:9443/healthz", + "hidden": true, + "installed": "2024-02-05 19:13:03.528035747 +0000 UTC", + "kind": "feature", + "name": "Lan Common", + "oper_state": "started", + "predisablecheck": "https://dcnm-lan-fabric.cisco-ndfc.svc:9443/rest/control/fabrics/lanCommonPreDisableCheck", + "requires": [ + "lan-discovery-worker", + "cc" + ], + "spec": "", + "ui": true + }, + "nxcloud": { + "admin_state": "disabled", + "description": "Nexus Cloud Connector", + "featureset": { + "lan": { + "default": false + } + }, + "hidden": true, + "kind": "feature", + "name": "Nexus Cloud Connector", + "oper_state": "", + "spec": "", + "ui": false + }, + "openstackviz": { + "admin_state": "disabled", + "description": "Network Visualization of Openstack Clusters", + "featureset": { + "lan": { + "default": false + } + }, + "kind": "feature", + "name": "Openstack Visualizer", + "oper_state": "", + "spec": "", + "ui": false + }, + "pm": { + "admin_state": "disabled", + "apidoc": [ + { + "schema": null, + "subpath": "pm", + "url": "https://dcnm-pm.cisco-ndfc.svc:9443/v3/api-docs" + }, + { + "schema": null, + "subpath": "pm", + "url": "https://dcnm-pm-worker.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "Monitor Environment and Interface Statistics", + "featureset": { + "lan": { + "default": false + }, + "san": { + "default": true + } + }, + "kind": "feature", + "name": "Performance Monitoring", + "oper_state": "stopped", + "predisablecheck": "https://dcnm-pm.cisco-ndfc.svc:9443/pmPreDisableCheck", + "requires": [ + "pm-worker" + ], + "spec": "", + "ui": false + }, + "pmn": { + "admin_state": "enabled", + "apidoc": [ + { + "schema": null, + "subpath": "pmn", + "url": "https://dcnm-pmn.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "Media Controller for IP Fabrics", + "featureset": { + "lan": { + "default": false + } + }, + "healthz": "https://dcnm-pmn.cisco-ndfc:9443/healthz", + "installed": "2024-05-09 17:25:50.710270448 +0000 UTC", + "kind": "feature", + "name": "IP Fabric for Media", + "oper_state": "started", + "predisablecheck": "https://dcnm-lan-fabric.cisco-ndfc.svc:9443/rest/control/fabrics/lanIPFMPreDisableCheck", + "requires": [ + "pmn-telemetry-mgmt", + "pmn-telemetry-data" + ], + "spec": "", + "ui": true + }, + "pmn-telemetry-data": { + "admin_state": "disabled", + "description": "Media Controller for IP Fabrics", + "featureset": { + "lan": { + "default": false + } + }, + "hidden": true, + "kind": "feature", + "name": "IP Fabric for Media", + "oob_nw_mode": "Data", + "oper_state": "", + "requires": [ + "pmn-telemetry-data-worker" + ], + "service_network": "Data", + "spec": "", + "ui": false + }, + "pmn-telemetry-mgmt": { + "admin_state": "enabled", + "description": "Media Controller for IP Fabrics", + "featureset": { + "lan": { + "default": false + } + }, + "hidden": true, + "installed": "2024-05-09 17:25:51.650786638 +0000 UTC", + "kind": "feature", + "name": "IP Fabric for Media", + "oob_nw_mode": "Management", + "oper_state": "started", + "requires": [ + "pmn-telemetry-mgmt-worker" + ], + "service_ip": "172.22.150.238", + "service_network": "Management", + "spec": "", + "ui": false + }, + "poap-data": { + "admin_state": "disabled", + "description": "POAP service on Data Network", + "featureset": { + "lan": { + "default": true + }, + "lan-base": { + "default": true + }, + "san": { + "default": true + } + }, + "hidden": true, + "kind": "feature", + "name": "POAP Service On Data", + "oob_nw_mode": "Data", + "oper_state": "stopped", + "service_network": "Data", + "spec": "", + "ui": false + }, + "poap-mgmt": { + "admin_state": "enabled", + "description": "POAP service on Managemnt Network", + "featureset": { + "lan": { + "default": true + }, + "lan-base": { + "default": true + } + }, + "hidden": true, + "installed": "2024-02-05 19:13:06.853864082 +0000 UTC", + "kind": "feature", + "name": "POAP Service On Management", + "oob_nw_mode": "Management", + "oper_state": "started", + "service_ip": "172.22.150.253", + "service_network": "Management", + "spec": "", + "ui": false + }, + "preport": { + "admin_state": "enabled", + "description": "Programmable report application", + "featureset": { + "lan": { + "default": true + }, + "san": { + "default": true + } + }, + "hidden": true, + "installed": "2024-02-05 19:13:00.916698974 +0000 UTC", + "kind": "feature", + "name": "Programmable report application", + "oper_state": "started", + "spec": "", + "ui": false + }, + "ptp": { + "admin_state": "disabled", + "apidoc": [ + { + "schema": null, + "subpath": "ptp", + "url": "https://dcnm-ptp.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "Monitor Precision Timing Protocol (PTP) Statistics", + "featureset": { + "lan": { + "default": false + } + }, + "kind": "feature", + "name": "PTP Monitoring", + "oper_state": "", + "requires": [ + "pmn-telemetry-mgmt", + "pmn-telemetry-data" + ], + "spec": "", + "ui": true + }, + "san": { + "admin_state": "disabled", + "apidoc": [ + { + "schema": null, + "subpath": "san-discovery", + "url": "https://dcnm-san-discovery-manager.cisco-ndfc.svc:9443/v3/api-docs" + }, + { + "schema": null, + "subpath": "san-discovery", + "url": "https://dcnm-san-inventory.cisco-ndfc.svc:9443/v3/api-docs" + }, + { + "schema": null, + "subpath": "san-config", + "url": "https://dcnm-san-config.cisco-ndfc.svc:9443/v3/api-docs" + }, + { + "schema": null, + "subpath": "storage", + "url": "https://dcnm-storage.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "SAN Management for MDS and Nexus switches", + "featureset": null, + "healthz": "https://dcnm-san-discovery-manager.cisco-ndfc.svc:9443/healthz", + "kind": "feature-set", + "name": "SAN Controller", + "oper_state": "", + "predisablecheck": "https://dcnm-san-discovery-manager.cisco-ndfc.svc:9443/san/sanPreDisableCheck", + "requires": [ + "san-discovery-worker" + ], + "spec": "", + "ui": true + }, + "san-dm": { + "admin_state": "disabled", + "description": "SAN Web Device Manager", + "featureset": { + "san": { + "default": true + } + }, + "kind": "feature", + "name": "SAN Web Device Manager", + "oper_state": "", + "spec": "", + "ui": false + }, + "san-insight": { + "admin_state": "disabled", + "apidoc": [ + { + "schema": null, + "subpath": "san-insight", + "url": "https://dcnm-san-insight-ui.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "SAN Analytics Visualization", + "featureset": { + "san": { + "default": false + } + }, + "healthz": "https://dcnm-san-insight-manager.cisco-ndfc.svc:9443/healthz", + "kind": "feature", + "name": "SAN Insights", + "oper_state": "", + "requires": [ + "san-insights-pp-worker", + "san-insights-rc-worker" + ], + "spec": "", + "ui": false + }, + "vmmplugin": { + "admin_state": "disabled", + "apidoc": [ + { + "schema": null, + "subpath": "vmm", + "url": "https://dcnm-vmm.cisco-ndfc.svc:9443/v3/api-docs" + } + ], + "description": "Network Visualization of Virtual Machines", + "featureset": { + "lan": { + "default": false + }, + "san": { + "default": false + } + }, + "kind": "feature", + "name": "VMM Visualizer", + "oper_state": "", + "spec": "", + "ui": false + }, + "vxlan": { + "admin_state": "disabled", + "apidoc": [ + { + "schema": null, + "subpath": "", + "url": "https://sgm.cisco-ndfc.svc:9443/api-docs" + } + ], + "description": "Automation, Compliance, and Management for NX-OS and Other devices", + "featureset": { + "lan": { + "default": true + } + }, + "kind": "feature", + "name": "Fabric Builder", + "oper_state": "stopped", + "predisablecheck": "https://dcnm-lan-fabric.cisco-ndfc.svc:9443/rest/control/fabrics/lanVXLANPreDisableCheck", + "spec": "", + "ui": false + } + }, + "name": "", + "version": 201 + }, + "status": "success" + }, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/features", + "RETURN_CODE": 200 + }, + "test_controller_features_00050a": { + "DATA": {}, + "MESSAGE": "Internal server error", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/features", + "RETURN_CODE": 500 + }, + "test_controller_features_00060a": { + "DATA": {}, + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/fm/features", + "RETURN_CODE": 200 + } +} \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_controller_features.py b/tests/unit/module_utils/common/test_controller_features.py new file mode 100644 index 000000000..830e45a68 --- /dev/null +++ b/tests/unit/module_utils/common/test_controller_features.py @@ -0,0 +1,355 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-import +# pylint: disable=redefined-outer-name +# pylint: disable=protected-access +# pylint: disable=unused-argument +# pylint: disable=invalid-name + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import inspect + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm import ( + Features, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import ( + ConversionUtils, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_features import ( + ControllerFeatures, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import ( + ControllerResponseError, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import ( + RestSend, +) +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + MockAnsibleModule, + ResponseGenerator, + does_not_raise, + controller_features_fixture, + responses_controller_features, + params, +) + + +def test_controller_features_00010(controller_features) -> None: + """ + Classes and Methods + - ControllerFeatures + - __init__() + + Test + - Class attributes are initialized to expected values + - Exception is not raised + """ + with does_not_raise(): + instance = controller_features + assert instance.class_name == "ControllerFeatures" + assert isinstance(instance.api_features, Features) + assert isinstance(instance.conversion, ConversionUtils) + assert instance.check_mode is False + assert instance.filter is None + assert instance.response is None + assert instance.response_data is None + assert instance.rest_send is None + assert instance.result is None + + +def test_controller_features_00020(controller_features) -> None: + """ + Classes and Methods + - ControllerFeatures + - __init__() + + Test + - ``ValueError`` is raised when params is missing check_mode + """ + params = {} + match = r"ControllerFeatures\.__init__\(\):\s+" + match += r"check_mode is required\." + with pytest.raises(ValueError, match=match): + instance = ControllerFeatures(params) # pylint: disable=unused-variable + + +def test_controller_features_00030(controller_features) -> None: + """ + Classes and Methods + - ControllerFeatures() + - __init__() + - refresh() + + Summary + - Verify ControllerFeatures().refresh() raises ``ValueError`` + when ``ControllerFeatures().rest_send`` is not set. + + Code Flow - Setup + - ControllerFeatures() is instantiated + + Code Flow - Test + - ControllerFeatures().refresh() is called without having + first set ControllerFeatures().rest_send + + Expected Result + - ``ValueError`` is raised + - Exception message matches expected + """ + with does_not_raise(): + instance = controller_features + + match = r"ControllerFeatures\.refresh: " + match += r"ControllerFeatures\.rest_send must be set before calling\s+" + match += r"refresh\(\)\." + with pytest.raises(ValueError, match=match): + instance.refresh() + + +def test_controller_features_00040(monkeypatch, controller_features) -> None: + """ + Classes and Methods + - ControllerFeatures() + - __init__() + - refresh() + + Summary + - Verify refresh() success case: + - RETURN_CODE is 200. + - Controller response contains expected structure and values. + + Code Flow - Setup + - ControllerFeatures() is instantiated + - dcnm_send() is patched to return the mocked controller response + - ControllerFeatures().RestSend() is instantiated + - ControllerFeatures().refresh() is called + - responses_ControllerFeatures contains a dict with: + - RETURN_CODE == 200 + - DATA == [] + + Code Flow - Test + - ControllerFeatures().refresh() is called + + Expected Result + - Exception is not raised + - instance.response_data returns expected controller features data + - ControllerFeatures()._properties are updated + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." + PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + + def responses(): + yield responses_controller_features(key) + + gen = ResponseGenerator(responses()) + + def mock_dcnm_send(*args, **kwargs): + item = gen.next + return item + + with does_not_raise(): + instance = controller_features + instance.rest_send = RestSend(MockAnsibleModule()) + instance.rest_send.unit_test = True + instance.rest_send.timeout = 1 + + monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + + with does_not_raise(): + instance.refresh() + instance.filter = "pmn" + + assert instance.filter == "pmn" + assert instance.admin_state == "enabled" + assert instance.oper_state == "started" + assert instance.enabled is True + assert instance.started is True + assert isinstance(instance.response, dict) + assert isinstance(instance.response_data, dict) + assert isinstance(instance.result, dict) + assert instance.response.get("MESSAGE", None) == "OK" + assert instance.response.get("RETURN_CODE", None) == 200 + assert instance.result.get("success", None) is True + assert instance.result.get("found", None) is True + + with does_not_raise(): + instance.filter = "vxlan" + + assert instance.filter == "vxlan" + assert instance.admin_state == "disabled" + assert instance.oper_state == "stopped" + assert instance.enabled is False + assert instance.started is False + + +def test_controller_features_00050(monkeypatch, controller_features) -> None: + """ + Classes and Methods + - ControllerFeatures() + - __init__() + - refresh() + + Summary + - Verify refresh() failure behavior: + - RETURN_CODE is 500. + + Code Flow - Setup + - ControllerFeatures() is instantiated + - dcnm_send() is patched to return the mocked controller response + - ControllerFeatures().RestSend() is instantiated + - ControllerFeatures().refresh() is called + - responses_ControllerFeatures contains a dict with: + - RETURN_CODE == 500 + + Code Flow - Test + - ControllerFeatures().refresh() is called + + Expected Result + - ``ControllerResponseError`` is raised + - Exception message matches expected + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." + PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + + def responses(): + yield responses_controller_features(key) + + gen = ResponseGenerator(responses()) + + def mock_dcnm_send(*args, **kwargs): + item = gen.next + return item + + with does_not_raise(): + instance = controller_features + instance.rest_send = RestSend(MockAnsibleModule()) + instance.rest_send.unit_test = True + instance.rest_send.timeout = 1 + + monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + + match = r"ControllerFeatures\.refresh: Bad controller response:" + with pytest.raises(ControllerResponseError, match=match): + instance.refresh() + + +def test_controller_features_00060(monkeypatch, controller_features) -> None: + """ + Classes and Methods + - ControllerFeatures() + - __init__() + - refresh() + + Summary + - Verify refresh() failure due to unexpected controller response structure.: + - RETURN_CODE is 200. + - DATA is missing. + + Code Flow - Setup + - ControllerFeatures() is instantiated + - dcnm_send() is patched to return the mocked controller response + - ControllerFeatures().RestSend() is instantiated + - ControllerFeatures().refresh() is called + - responses_ControllerFeatures contains a dict with: + - RETURN_CODE == 200 + - DATA is missing + + Code Flow - Test + - ControllerFeatures().refresh() is called + + Expected Result + - ``ControllerResponseError`` is raised + - Exception message matches expected + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." + PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + + def responses(): + yield responses_controller_features(key) + + gen = ResponseGenerator(responses()) + + def mock_dcnm_send(*args, **kwargs): + item = gen.next + return item + + with does_not_raise(): + instance = controller_features + instance.rest_send = RestSend(MockAnsibleModule()) + instance.rest_send.unit_test = True + instance.rest_send.timeout = 1 + + monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + + match = r"ControllerFeatures\.refresh: " + match += r"Controller response does not match expected structure:" + with pytest.raises(ControllerResponseError, match=match): + instance.refresh() + + +MATCH_00070 = r"ControllerFeatures\.rest_send: " +MATCH_00070 += r"value must be an instance of RestSend\..*" + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (RestSend(MockAnsibleModule()), False, does_not_raise()), + (ControllerFeatures(params), True, pytest.raises(TypeError, match=MATCH_00070)), + (None, True, pytest.raises(TypeError, match=MATCH_00070)), + ("foo", True, pytest.raises(TypeError, match=MATCH_00070)), + (10, True, pytest.raises(TypeError, match=MATCH_00070)), + ([10], True, pytest.raises(TypeError, match=MATCH_00070)), + ({10}, True, pytest.raises(TypeError, match=MATCH_00070)), + ], +) +def test_controller_features_00070( + controller_features, value, does_raise, expected +) -> None: + """ + Classes and Methods + - ControllerFeatures + - __init__() + - rest_send.setter + + Test + - ``TypeError`` is raised when ControllerFeatures().rest_send is + passed a value that is not an instance of RestSend() + """ + with does_not_raise(): + instance = controller_features + with expected: + instance.rest_send = value + if not does_raise: + assert instance.rest_send == value From 78dbc9b03534b26f9e8180098ed4cb41ad507502 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 May 2024 11:07:09 -1000 Subject: [PATCH 017/230] dcnm_fabric: Update docs with IPFM fabric parameters --- docs/cisco.dcnm.dcnm_fabric_module.rst | 1155 +++++++++++++++++++++++- plugins/modules/dcnm_fabric.py | 398 +++++++- 2 files changed, 1548 insertions(+), 5 deletions(-) diff --git a/docs/cisco.dcnm.dcnm_fabric_module.rst b/docs/cisco.dcnm.dcnm_fabric_module.rst index 8dc3e27eb..ab2e1ff7e 100644 --- a/docs/cisco.dcnm.dcnm_fabric_module.rst +++ b/docs/cisco.dcnm.dcnm_fabric_module.rst @@ -112,11 +112,1162 @@ Parameters
- LAN_CLASSIC_PARAMETERS + IPFM_FABRIC_PARAMETERS + +
+ - +
+ + + + +
IPFM (IP Fabric for Media) fabric specific parameters.
+
The following parameters are specific to IPFM fabrics.
+
Fabric for a fully automated deployment of IP Fabric for Media Network with Nexus 9000 switches.
+
The indentation of these parameters is meant only to logically group them.
+
They should be at the same YAML level as FABRIC_TYPE and FABRIC_NAME.
+ + + + + + +
+ AAA_REMOTE_IP_ENABLED + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
Enable only, when IP Authorization is enabled in the AAA Server
+ + + + + + +
+ AAA_SERVER_CONF + +
+ string +
+ + + Default:
""
+ + +
AAA Configurations
+ + + + + + +
+ ASM_GROUP_RANGES
list - / elements=dictionary +
+ + + Default:
""
+ + +
ASM group ranges with prefixes (len:4-32) example: 239.1.1.0/25, max 20 ranges. Enabling SPT-Threshold Infinity to prevent switchover to source-tree.
+ + + + + + +
+ BOOTSTRAP_CONF + +
+ string +
+ + + Default:
""
+ + +
Additional CLIs required during device bootup/login e.g. AAA/Radius
+ + + + + + +
+ BOOTSTRAP_ENABLE + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
Automatic IP Assignment For POAP
+ + + + + + +
+ BOOTSTRAP_MULTISUBNET + +
+ string +
+ + + Default:
"#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix"
+ + +
lines with # prefix are ignored here
+ + + + + + +
+ CDP_ENABLE + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
Enable CDP on management interface
+ + + + + + +
+ DHCP_ENABLE + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
Automatic IP Assignment For POAP From Local DHCP Server
+ + + + + + +
+ DHCP_END + +
+ string +
+ + + Default:
""
+ + +
End Address For Switch Out-of-Band POAP
+ + + + + + +
+ DHCP_IPV6_ENABLE + +
+ string +
+ + +
    Choices: +
  • DHCPv4 ←
  • +
+ + +
No description available
+ + + + + + +
+ DHCP_START + +
+ string +
+ + + Default:
""
+ + +
Start Address For Switch Out-of-Band POAP
+ + + + + + +
+ DNS_SERVER_IP_LIST + +
+ string +
+ + + Default:
""
+ + +
Comma separated list of IP Addresses (v4/v6)
+ + + + + + +
+ DNS_SERVER_VRF + +
+ string +
+ + + Default:
""
+ + +
One VRF for all DNS servers or a comma separated list of VRFs, one per DNS server
+ + + + + + +
+ ENABLE_AAA + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
Include AAA configs from Manageability tab during device bootup
+ + + + + + +
+ ENABLE_ASM + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
Enable groups with receivers sending (*,G) joins
+ + + + + + +
+ ENABLE_NBM_PASSIVE + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
Enable NBM mode to pim-passive for default VRF
+ + + + + + +
+ EXTRA_CONF_INTRA_LINKS + +
+ string +
+ + + Default:
""
+ + +
Additional CLIs For All Intra-Fabric Links
+ + + + + + +
+ EXTRA_CONF_LEAF + +
+ string +
+ + + Default:
""
+ + +
Additional CLIs For All Leafs and Tier2 Leafs As Captured From Show Running Configuration
+ + + + + + +
+ EXTRA_CONF_SPINE + +
+ string +
+ + + Default:
""
+ + +
Additional CLIs For All Spines As Captured From Show Running Configuration
+ + + + + + +
+ FABRIC_INTERFACE_TYPE + +
+ string +
+ + +
    Choices: +
  • p2p ←
  • +
+ + +
Only Numbered(Point-to-Point) is supported
+ + + + + + +
+ FABRIC_MTU + +
+ integer +
+ + + Default:
9216
+ + +
. Must be an even number
+ + + + + + +
+ FABRIC_NAME + +
+ string +
+ + + Default:
""
+ + +
Name of the fabric (Max Size 64)
+ + + + + + +
+ FEATURE_PTP + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
No description available
+ + + + + + +
+ ISIS_AUTH_ENABLE + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
No description available
+ + + + + + +
+ ISIS_AUTH_KEY + +
+ string +
+ + + Default:
""
+ + +
Cisco Type 7 Encrypted
+ + + + + + +
+ ISIS_AUTH_KEYCHAIN_KEY_ID + +
+ integer +
+ + + Default:
127
+ + +
No description available
+ + + + + + +
+ ISIS_AUTH_KEYCHAIN_NAME + +
+ string +
+ + + Default:
""
+ + +
No description available
+ + + + + + +
+ ISIS_LEVEL + +
+ string +
+ + +
    Choices: +
  • level-1
  • +
  • level-2 ←
  • +
+ + +
Supported IS types: level-1, level-2
+ + + + + + +
+ ISIS_P2P_ENABLE + +
+ boolean +
+ + +
    Choices: +
  • no
  • +
  • yes ←
  • +
+ + +
This will enable network point-to-point on fabric interfaces which are numbered
+ + + + + + +
+ L2_HOST_INTF_MTU + +
+ integer +
+ + + Default:
9216
+ + +
. Must be an even number
+ + + + + + +
+ LINK_STATE_ROUTING + +
+ string +
+ + +
    Choices: +
  • ospf ←
  • +
  • is-is
  • +
+ + +
Used for Spine-Leaf Connectivity
+ + + + + + +
+ LINK_STATE_ROUTING_TAG + +
+ string +
+ + + Default:
1
+ + +
Routing process tag for the fabric
+ + + + + + +
+ LOOPBACK0_IP_RANGE + +
+ string +
+ + + Default:
"10.2.0.0/22"
+ + +
Routing Loopback IP Address Range
+ + + + + + +
+ MGMT_GW + +
+ string +
+ + + Default:
""
+ + +
Default Gateway For Management VRF On The Switch
+ + + + + + +
+ MGMT_PREFIX + +
+ integer +
+ + + Default:
24
+ + +
No description available
+ + + + + + +
+ NTP_SERVER_IP_LIST + +
+ string +
+ + + Default:
""
+ + +
Comma separated list of IP Addresses (v4/v6)
+ + + + + + +
+ NTP_SERVER_VRF + +
+ string +
+ + + Default:
""
+ + +
One VRF for all NTP servers or a comma separated list of VRFs, one per NTP server
+ + + + + + +
+ NXAPI_VRF + +
+ string +
+ + +
    Choices: +
  • management ←
  • +
  • default
  • +
+ + +
VRF used for NX-API communication
+ + + + + + +
+ OSPF_AREA_ID + +
+ string +
+ + + Default:
"0.0.0.0"
+ + +
OSPF Area Id in IP address format
+ + + + + + +
+ OSPF_AUTH_ENABLE + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
No description available
+ + + + + + +
+ OSPF_AUTH_KEY + +
+ string +
+ + + Default:
""
+ + +
3DES Encrypted
+ + + + + + +
+ OSPF_AUTH_KEY_ID + +
+ integer +
+ + + Default:
127
+ + +
No description available
+ + + + + + +
+ PIM_HELLO_AUTH_ENABLE + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
No description available
+ + + + + + +
+ PIM_HELLO_AUTH_KEY + +
+ string +
+ + + Default:
""
+ + +
3DES Encrypted
+ + + + + + +
+ PM_ENABLE + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
No description available
+ + + + + + +
+ POWER_REDUNDANCY_MODE + +
+ string +
+ + +
    Choices: +
  • ps-redundant ←
  • +
  • combined
  • +
  • insrc-redundant
  • +
+ + +
Default power supply mode for the fabric
+ + + + + + +
+ PTP_DOMAIN_ID + +
+ integer +
+ + + Default:
0
+ + +
Multiple Independent PTP Clocking Subdomains on a Single Network
+ + + + + + +
+ PTP_LB_ID + +
+ integer +
+ + + Default:
0
+ + +
No description available
+ + + + + + +
+ PTP_PROFILE + +
+ string +
+ + +
    Choices: +
  • IEEE-1588v2
  • +
  • SMPTE-2059-2 ←
  • +
  • AES67-2015
  • +
+ + +
Enabled on ISL links only
+ + + + + + +
+ ROUTING_LB_ID + +
+ integer +
+ + + Default:
0
+ + +
No description available
+ + + + + + +
+ RP_IP_RANGE + +
+ string +
+ + + Default:
"10.254.254.0/24"
+ + +
RP Loopback IP Address Range
+ + + + + + +
+ RP_LB_ID + +
+ integer +
+ + + Default:
254
+ + +
No description available
+ + + + + + +
+ SNMP_SERVER_HOST_TRAP + +
+ boolean +
+ + +
    Choices: +
  • no
  • +
  • yes ←
  • +
+ + +
Configure NDFC as a receiver for SNMP traps
+ + + + + + +
+ STATIC_UNDERLAY_IP_ALLOC + +
+ boolean +
+ + +
    Choices: +
  • no ←
  • +
  • yes
  • +
+ + +
Checking this will disable Dynamic Fabric IP Address Allocations
+ + + + + + +
+ SUBNET_RANGE + +
+ string +
+ + + Default:
"10.4.0.0/16"
+ + +
Address range to assign Numbered IPs
+ + + + + + +
+ SUBNET_TARGET_MASK + +
+ integer +
+ + +
    Choices: +
  • 30 ←
  • +
  • 31
  • +
+ + +
Mask for Fabric Subnet IP Range
+ + + + + + +
+ SYSLOG_SERVER_IP_LIST + +
+ string +
+ + + Default:
""
+ + +
Comma separated list of IP Addresses (v4/v6)
+ + + + + + +
+ SYSLOG_SERVER_VRF + +
+ string +
+ + + Default:
""
+ + +
One VRF for all Syslog servers or a comma separated list of VRFs, one per Syslog server
+ + + + + + +
+ SYSLOG_SEV + +
+ string +
+ + + Default:
""
+ + +
Comma separated list of Syslog severity values, one per Syslog server
+ + + + + + +
+ LAN_CLASSIC_FABRIC_PARAMETERS + +
+ -
diff --git a/plugins/modules/dcnm_fabric.py b/plugins/modules/dcnm_fabric.py index 582f5af6b..e866d5f97 100644 --- a/plugins/modules/dcnm_fabric.py +++ b/plugins/modules/dcnm_fabric.py @@ -1545,15 +1545,407 @@ - Default Overlay VRF Template For Borders required: false type: str - LAN_CLASSIC_PARAMETERS: + IPFM_FABRIC_PARAMETERS: + description: + - IPFM (IP Fabric for Media) fabric specific parameters. + - The following parameters are specific to IPFM fabrics. + - Fabric for a fully automated deployment of IP Fabric for Media Network with Nexus 9000 switches. + - The indentation of these parameters is meant only to logically group them. + - They should be at the same YAML level as FABRIC_TYPE and FABRIC_NAME. + suboptions: + AAA_REMOTE_IP_ENABLED: + default: false + description: + - Enable only, when IP Authorization is enabled in the AAA Server + required: false + type: bool + AAA_SERVER_CONF: + default: '' + description: + - AAA Configurations + required: false + type: str + ASM_GROUP_RANGES: + default: '' + description: + - 'ASM group ranges with prefixes (len:4-32) example: 239.1.1.0/25, + max 20 ranges. Enabling SPT-Threshold Infinity to prevent switchover + to source-tree.' + required: false + type: list + BOOTSTRAP_CONF: + default: '' + description: + - Additional CLIs required during device bootup/login e.g. AAA/Radius + required: false + type: str + BOOTSTRAP_ENABLE: + default: false + description: + - Automatic IP Assignment For POAP + required: false + type: bool + BOOTSTRAP_MULTISUBNET: + default: '#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix' + description: + - 'lines with # prefix are ignored here' + required: false + type: str + CDP_ENABLE: + default: false + description: + - Enable CDP on management interface + required: false + type: bool + DHCP_ENABLE: + default: false + description: + - Automatic IP Assignment For POAP From Local DHCP Server + required: false + type: bool + DHCP_END: + default: '' + description: + - End Address For Switch Out-of-Band POAP + required: false + type: str + DHCP_IPV6_ENABLE: + choices: + - DHCPv4 + default: DHCPv4 + description: + - No description available + required: false + type: str + DHCP_START: + default: '' + description: + - Start Address For Switch Out-of-Band POAP + required: false + type: str + DNS_SERVER_IP_LIST: + default: '' + description: + - Comma separated list of IP Addresses (v4/v6) + required: false + type: str + DNS_SERVER_VRF: + default: '' + description: + - One VRF for all DNS servers or a comma separated list of VRFs, one + per DNS server + required: false + type: str + ENABLE_AAA: + default: false + description: + - Include AAA configs from Manageability tab during device bootup + required: false + type: bool + ENABLE_ASM: + default: false + description: + - Enable groups with receivers sending (*,G) joins + required: false + type: bool + ENABLE_NBM_PASSIVE: + default: false + description: + - Enable NBM mode to pim-passive for default VRF + required: false + type: bool + EXTRA_CONF_INTRA_LINKS: + default: '' + description: + - Additional CLIs For All Intra-Fabric Links + required: false + type: str + EXTRA_CONF_LEAF: + default: '' + description: + - Additional CLIs For All Leafs and Tier2 Leafs As Captured From Show + Running Configuration + required: false + type: str + EXTRA_CONF_SPINE: + default: '' + description: + - Additional CLIs For All Spines As Captured From Show Running Configuration + required: false + type: str + FABRIC_INTERFACE_TYPE: + choices: + - p2p + default: p2p + description: + - Only Numbered(Point-to-Point) is supported + required: false + type: str + FABRIC_MTU: + default: 9216 + description: + - . Must be an even number + required: false + type: int + FABRIC_NAME: + default: '' + description: + - Name of the fabric (Max Size 64) + required: false + type: str + FEATURE_PTP: + default: false + description: + - No description available + required: false + type: bool + ISIS_AUTH_ENABLE: + default: false + description: + - No description available + required: false + type: bool + ISIS_AUTH_KEY: + default: '' + description: + - Cisco Type 7 Encrypted + required: false + type: str + ISIS_AUTH_KEYCHAIN_KEY_ID: + default: 127 + description: + - No description available + required: false + type: int + ISIS_AUTH_KEYCHAIN_NAME: + default: '' + description: + - No description available + required: false + type: str + ISIS_LEVEL: + choices: + - level-1 + - level-2 + default: level-2 + description: + - 'Supported IS types: level-1, level-2' + required: false + type: str + ISIS_P2P_ENABLE: + default: true + description: + - This will enable network point-to-point on fabric interfaces which + are numbered + required: false + type: bool + L2_HOST_INTF_MTU: + default: 9216 + description: + - . Must be an even number + required: false + type: int + LINK_STATE_ROUTING: + choices: + - ospf + - is-is + default: ospf + description: + - Used for Spine-Leaf Connectivity + required: false + type: str + LINK_STATE_ROUTING_TAG: + default: 1 + description: + - Routing process tag for the fabric + required: false + type: str + LOOPBACK0_IP_RANGE: + default: 10.2.0.0/22 + description: + - Routing Loopback IP Address Range + required: false + type: str + MGMT_GW: + default: '' + description: + - Default Gateway For Management VRF On The Switch + required: false + type: str + MGMT_PREFIX: + default: 24 + description: + - No description available + required: false + type: int + NTP_SERVER_IP_LIST: + default: '' + description: + - Comma separated list of IP Addresses (v4/v6) + required: false + type: str + NTP_SERVER_VRF: + default: '' + description: + - One VRF for all NTP servers or a comma separated list of VRFs, one + per NTP server + required: false + type: str + NXAPI_VRF: + choices: + - management + - default + default: management + description: + - VRF used for NX-API communication + required: false + type: str + OSPF_AREA_ID: + default: 0.0.0.0 + description: + - OSPF Area Id in IP address format + required: false + type: str + OSPF_AUTH_ENABLE: + default: false + description: + - No description available + required: false + type: bool + OSPF_AUTH_KEY: + default: '' + description: + - 3DES Encrypted + required: false + type: str + OSPF_AUTH_KEY_ID: + default: 127 + description: + - No description available + required: false + type: int + PIM_HELLO_AUTH_ENABLE: + default: false + description: + - No description available + required: false + type: bool + PIM_HELLO_AUTH_KEY: + default: '' + description: + - 3DES Encrypted + required: false + type: str + PM_ENABLE: + default: false + description: + - No description available + required: false + type: bool + POWER_REDUNDANCY_MODE: + choices: + - ps-redundant + - combined + - insrc-redundant + default: ps-redundant + description: + - Default power supply mode for the fabric + required: false + type: str + PTP_DOMAIN_ID: + default: 0 + description: + - 'Multiple Independent PTP Clocking Subdomains on a Single Network ' + required: false + type: int + PTP_LB_ID: + default: 0 + description: + - No description available + required: false + type: int + PTP_PROFILE: + choices: + - IEEE-1588v2 + - SMPTE-2059-2 + - AES67-2015 + default: SMPTE-2059-2 + description: + - Enabled on ISL links only + required: false + type: str + ROUTING_LB_ID: + default: 0 + description: + - No description available + required: false + type: int + RP_IP_RANGE: + default: 10.254.254.0/24 + description: + - RP Loopback IP Address Range + required: false + type: str + RP_LB_ID: + default: 254 + description: + - No description available + required: false + type: int + SNMP_SERVER_HOST_TRAP: + default: true + description: + - Configure NDFC as a receiver for SNMP traps + required: false + type: bool + STATIC_UNDERLAY_IP_ALLOC: + default: false + description: + - Checking this will disable Dynamic Fabric IP Address Allocations + required: false + type: bool + SUBNET_RANGE: + default: 10.4.0.0/16 + description: + - Address range to assign Numbered IPs + required: false + type: str + SUBNET_TARGET_MASK: + choices: + - 30 + - 31 + default: 30 + description: + - Mask for Fabric Subnet IP Range + required: false + type: int + SYSLOG_SERVER_IP_LIST: + default: '' + description: + - Comma separated list of IP Addresses (v4/v6) + required: false + type: str + SYSLOG_SERVER_VRF: + default: '' + description: + - One VRF for all Syslog servers or a comma separated list of VRFs, + one per Syslog server + required: false + type: str + SYSLOG_SEV: + default: '' + description: + - 'Comma separated list of Syslog severity values, one per Syslog + server ' + required: false + type: str + LAN_CLASSIC_FABRIC_PARAMETERS: description: - LAN Classic fabric specific parameters. - The following parameters are specific to Classic LAN fabrics. - Fabric to manage a legacy Classic LAN deployment with Nexus switches. - The indentation of these parameters is meant only to logically group them. - They should be at the same YAML level as FABRIC_TYPE and FABRIC_NAME. - type: list - elements: dict suboptions: AAA_REMOTE_IP_ENABLED: default: false From a21888ac5f3e0b569e085be0d42275dd702bb294 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 May 2024 11:57:52 -1000 Subject: [PATCH 018/230] dcnm_fabric: fix PEP8 and doc errors --- docs/cisco.dcnm.dcnm_fabric_module.rst | 3 ++- plugins/module_utils/common/controller_features.py | 2 +- plugins/modules/dcnm_fabric.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/cisco.dcnm.dcnm_fabric_module.rst b/docs/cisco.dcnm.dcnm_fabric_module.rst index ab2e1ff7e..8c012612c 100644 --- a/docs/cisco.dcnm.dcnm_fabric_module.rst +++ b/docs/cisco.dcnm.dcnm_fabric_module.rst @@ -176,6 +176,7 @@ Parameters
list + / elements=string
@@ -738,7 +739,7 @@ Parameters - Default:
1
+ Default:
"1"
Routing process tag for the fabric
diff --git a/plugins/module_utils/common/controller_features.py b/plugins/module_utils/common/controller_features.py index 8f478c030..cbdc94df0 100644 --- a/plugins/module_utils/common/controller_features.py +++ b/plugins/module_utils/common/controller_features.py @@ -292,7 +292,7 @@ def rest_send(self, value): method_name = inspect.stack()[0][3] test = None msg = f"{self.class_name}.{method_name}: " - msg += f"value must be an instance of RestSend. " + msg += "value must be an instance of RestSend. " try: test = value.class_name except AttributeError as error: diff --git a/plugins/modules/dcnm_fabric.py b/plugins/modules/dcnm_fabric.py index e866d5f97..90766c73a 100644 --- a/plugins/modules/dcnm_fabric.py +++ b/plugins/modules/dcnm_fabric.py @@ -1573,6 +1573,7 @@ to source-tree.' required: false type: list + elements: str BOOTSTRAP_CONF: default: '' description: @@ -1755,7 +1756,7 @@ required: false type: str LINK_STATE_ROUTING_TAG: - default: 1 + default: "1" description: - Routing process tag for the fabric required: false From f2bd8d7a42dd15988547329ce0bb870cb80e0245 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 9 May 2024 13:36:34 -1000 Subject: [PATCH 019/230] Add EXTRA_CONF_LEAF param in EXAMPLES section Just to make the example a bit more interesting... --- plugins/modules/dcnm_fabric.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/modules/dcnm_fabric.py b/plugins/modules/dcnm_fabric.py index 90766c73a..6d8c04bbe 100644 --- a/plugins/modules/dcnm_fabric.py +++ b/plugins/modules/dcnm_fabric.py @@ -2244,6 +2244,9 @@ BGP_AS: 65000 ANYCAST_GW_MAC: 0001.aabb.ccdd UNDERLAY_IS_V6: false + EXTRA_CONF_LEAF: | + interface Ethernet1/1-16 + description managed by NDFC DEPLOY: false - FABRIC_NAME: MSD_Fabric FABRIC_TYPE: VXLAN_EVPN_MSD From 0b77513bf1988be1e7abab10bdd59debd0a69bcd Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 11 May 2024 09:11:33 -1000 Subject: [PATCH 020/230] dcnm_endpoints: Initial lan-fabric endpoints Additions: plugins/module_utils/api/v1/lan_fabric.py plugins/module_utils/api/v1/rest/control/fabrics.py Modifications plugins/module_utils/api/common_api.py - Add ConversionUtils() instance --- plugins/module_utils/common/api/common_api.py | 4 + .../module_utils/common/api/v1/lan_fabric.py | 36 ++++++++ .../common/api/v1/rest/__init__.py | 0 .../common/api/v1/rest/control/__init__.py | 0 .../common/api/v1/rest/control/fabrics.py | 88 +++++++++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 plugins/module_utils/common/api/v1/lan_fabric.py create mode 100644 plugins/module_utils/common/api/v1/rest/__init__.py create mode 100644 plugins/module_utils/common/api/v1/rest/control/__init__.py create mode 100644 plugins/module_utils/common/api/v1/rest/control/fabrics.py diff --git a/plugins/module_utils/common/api/common_api.py b/plugins/module_utils/common/api/common_api.py index 4be680b94..776ce62d3 100644 --- a/plugins/module_utils/common/api/common_api.py +++ b/plugins/module_utils/common/api/common_api.py @@ -19,6 +19,9 @@ import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils + class CommonApi: """ @@ -28,6 +31,7 @@ class CommonApi: def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.conversion = ConversionUtils() self.log.debug("ENTERED api.CommonApi()") self.api = "/appcenter/cisco/ndfc/api" self._init_properties() diff --git a/plugins/module_utils/common/api/v1/lan_fabric.py b/plugins/module_utils/common/api/v1/lan_fabric.py new file mode 100644 index 000000000..36da5c51a --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric.py @@ -0,0 +1,36 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common import \ + Common + + +class LanFabric(Common): + """ + V1 API lan-fabrics endpoints common methods and properties. + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.lan_fabric = f"{self.api_v1}/lan-fabric" + self.log.debug("ENTERED api.v1.LanFabric()") diff --git a/plugins/module_utils/common/api/v1/rest/__init__.py b/plugins/module_utils/common/api/v1/rest/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/api/v1/rest/control/__init__.py b/plugins/module_utils/common/api/v1/rest/control/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py new file mode 100644 index 000000000..e7b2c2483 --- /dev/null +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -0,0 +1,88 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric import \ + LanFabric + +class Fabrics(LanFabric): + """ + V1 API Fabrics endpoints common methods and properties. + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.rest_control_fabrics = f"{self.lan_fabric}/rest/control/fabrics" + self._build_properties() + self.log.debug("ENTERED api.v1.LanFabric.Fabrics()") + + def _build_properties(self): + self.properties["fabric_name"] = None + + @property + def fabric_name(self): + """ + - getter: Return the fabric_name. + - setter: Set the fabric_name. + - setter: Raise ``ValueError`` if fabric_name is not valid. + """ + return self.properties["fabric_name"] + + @fabric_name.setter + def fabric_name(self, value): + method_name = inspect.stack()[0][3] + try: + self.conversion.validate_fabric_name(value) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) + self.properties["fabric_name"] = value + +class FabricsDetails(Fabrics): + """ + V1 API Fabrics details endpoint. + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + self.log.debug("ENTERED api.v1.LanFabric.Fabrics.FabricsDetails()") + + def _build_properties(self): + self.properties["verb"] = "GET" + + @property + def path(self): + """ + - Override the path property to mandate fabric_name is set. + - Raise ``ValueError`` if fabric_name is not set. + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.rest_control_fabrics}/{self.fabric_name}" \ No newline at end of file From 132ce6f5fcad128990e3d69f33c00a062529949b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 11 May 2024 11:26:11 -1000 Subject: [PATCH 021/230] Subclasses can define mandatory properties, more Fabrics(): add path property FabricsDelete(): new class for fabric delete endpoint FabricsDetails(): inherit path property from Fabrics() --- plugins/module_utils/common/api/common_api.py | 3 ++ .../common/api/v1/rest/control/fabrics.py | 54 +++++++++++++------ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/plugins/module_utils/common/api/common_api.py b/plugins/module_utils/common/api/common_api.py index 776ce62d3..edbbe9c0e 100644 --- a/plugins/module_utils/common/api/common_api.py +++ b/plugins/module_utils/common/api/common_api.py @@ -32,6 +32,9 @@ def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.conversion = ConversionUtils() + # Popuate in subclasses to indicate which properties + # are mandatory for the subclass. + self.required_properties = set() self.log.debug("ENTERED api.CommonApi()") self.api = "/appcenter/cisco/ndfc/api" self._init_properties() diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py index e7b2c2483..25308d9b9 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -33,10 +33,13 @@ def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.rest_control_fabrics = f"{self.lan_fabric}/rest/control/fabrics" - self._build_properties() self.log.debug("ENTERED api.v1.LanFabric.Fabrics()") + self._build_properties() def _build_properties(self): + """ + - Set the fabric_name property. + """ self.properties["fabric_name"] = None @property @@ -59,30 +62,49 @@ def fabric_name(self, value): raise ValueError(msg) self.properties["fabric_name"] = value + @property + def path(self): + """ + - Override the path property to mandate fabric_name is set. + - Raise ``ValueError`` if fabric_name is not set. + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.rest_control_fabrics}/{self.fabric_name}" + +class FabricsDelete(Fabrics): + """ + V1 API Fabrics: fabric delete endpoint. + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + self.log.debug(f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}") + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "DELETE" + class FabricsDetails(Fabrics): """ - V1 API Fabrics details endpoint. + V1 API Fabrics: fabric details endpoint. """ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") self._build_properties() - self.log.debug("ENTERED api.v1.LanFabric.Fabrics.FabricsDetails()") + self.log.debug(f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}") def _build_properties(self): + super()._build_properties() self.properties["verb"] = "GET" - - @property - def path(self): - """ - - Override the path property to mandate fabric_name is set. - - Raise ``ValueError`` if fabric_name is not set. - """ - method_name = inspect.stack()[0][3] - if self.fabric_name is None: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be set prior to accessing path." - raise ValueError(msg) - return f"{self.rest_control_fabrics}/{self.fabric_name}" \ No newline at end of file From cf6f58fe704e004a07ad479182929e9e62398594 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 12 May 2024 08:11:03 -1000 Subject: [PATCH 022/230] Rename classes and files --- .../common/api/{common_api.py => common.py} | 2 +- plugins/module_utils/common/api/v1/fm.py | 6 +- .../module_utils/common/api/v1/lan_fabric.py | 6 +- .../common/api/v1/rest/control/fabrics.py | 86 ++++++++++++++++++- .../common/api/v1/{common.py => v1_common.py} | 6 +- 5 files changed, 94 insertions(+), 12 deletions(-) rename plugins/module_utils/common/api/{common_api.py => common.py} (99%) rename plugins/module_utils/common/api/v1/{common.py => v1_common.py} (94%) diff --git a/plugins/module_utils/common/api/common_api.py b/plugins/module_utils/common/api/common.py similarity index 99% rename from plugins/module_utils/common/api/common_api.py rename to plugins/module_utils/common/api/common.py index edbbe9c0e..9dcd90eaf 100644 --- a/plugins/module_utils/common/api/common_api.py +++ b/plugins/module_utils/common/api/common.py @@ -23,7 +23,7 @@ ConversionUtils -class CommonApi: +class Common: """ API endpoints common methods and properties. """ diff --git a/plugins/module_utils/common/api/v1/fm.py b/plugins/module_utils/common/api/v1/fm.py index b526b0d49..be025ebd8 100644 --- a/plugins/module_utils/common/api/v1/fm.py +++ b/plugins/module_utils/common/api/v1/fm.py @@ -19,11 +19,11 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common import \ - Common +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.v1_common import \ + V1Common -class FM(Common): +class FM(V1Common): """ V1 API Feature Manager (FM) endpoints common methods and properties. """ diff --git a/plugins/module_utils/common/api/v1/lan_fabric.py b/plugins/module_utils/common/api/v1/lan_fabric.py index 36da5c51a..5e84b68ba 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric.py +++ b/plugins/module_utils/common/api/v1/lan_fabric.py @@ -19,11 +19,11 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common import \ - Common +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.v1_common import \ + V1Common -class LanFabric(Common): +class LanFabric(V1Common): """ V1 API lan-fabrics endpoints common methods and properties. """ diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py index 25308d9b9..e1354ae65 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -75,7 +75,89 @@ def path(self): raise ValueError(msg) return f"{self.rest_control_fabrics}/{self.fabric_name}" -class FabricsDelete(Fabrics): +class EpFabricConfigDeploy(Fabrics): + """ + - V1 API Fabrics: fabric config-deploy endpoint. + - parameters: + - force_show_run: boolean + - default: False + - include_all_msd_switches: boolean + - default: False + - fabric_name: string + - required + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + self.log.debug(f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}") + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + self.properties["force_show_run"] = False + self.properties["include_all_msd_switches"] = False + + @property + def force_show_run(self): + """ + - getter: Return the force_show_run. + - setter: Set the force_show_run. + - setter: Raise ``ValueError`` if force_show_run is not valid. + - Default: False + """ + return self.properties["force_show_run"] + + @force_show_run.setter + def force_show_run(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "force_show_run must be a boolean." + raise ValueError(msg) + self.properties["force_show_run"] = value + + @property + def include_all_msd_switches(self): + """ + - getter: Return the include_all_msd_switches. + - setter: Set the include_all_msd_switches. + - setter: Raise ``ValueError`` if include_all_msd_switches is not valid. + - Default: False + """ + return self.properties["include_all_msd_switches"] + + @include_all_msd_switches.setter + def include_all_msd_switches(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += "include_all_msd_switches must be a boolean." + raise ValueError(msg) + self.properties["include_all_msd_switches"] = value + + @property + def path(self): + """ + - Override the path property to mandate fabric_name is set. + - Raise ``ValueError`` if fabric_name is not set. + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + _path = f"{self.rest_control_fabrics}/{self.fabric_name}" + _path += "/config-deploy?" + _path += f"forceShowRun={self.force_show_run}" + _path += f"&inclAllMSDSwitches={self.include_all_msd_switches}" + return _path + + +class EpFabricDelete(Fabrics): """ V1 API Fabrics: fabric delete endpoint. """ @@ -92,7 +174,7 @@ def _build_properties(self): super()._build_properties() self.properties["verb"] = "DELETE" -class FabricsDetails(Fabrics): +class EpFabricDetails(Fabrics): """ V1 API Fabrics: fabric details endpoint. """ diff --git a/plugins/module_utils/common/api/v1/common.py b/plugins/module_utils/common/api/v1/v1_common.py similarity index 94% rename from plugins/module_utils/common/api/v1/common.py rename to plugins/module_utils/common/api/v1/v1_common.py index e803b056e..c4382e857 100644 --- a/plugins/module_utils/common/api/v1/common.py +++ b/plugins/module_utils/common/api/v1/v1_common.py @@ -19,11 +19,11 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.common_api import \ - CommonApi +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.common import \ + Common -class Common(CommonApi): +class V1Common(Common): """ v1 API enpoints common methods and properties. """ From 2a113fbc932ee79b7cffe68ce6d832450a32d88f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 12 May 2024 10:37:54 -1000 Subject: [PATCH 023/230] Fabrics: Refactor, update docstrings, add endpoints. 1. Update all Fabrics subclass docstrings for consistency of content and format. 2. Add Raises section to all Fabrics subclass docstrings. 3. Refactor subclass.path into Fabrics().path_fabric_name which is added to, as needed, in subclasses 4. Add the following endpoints: - EpFabricConfigSave - EpFabricFreezeMode --- .../common/api/v1/rest/control/fabrics.py | 252 ++++++++++++++++-- 1 file changed, 223 insertions(+), 29 deletions(-) diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py index e1354ae65..e232f18cc 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -23,9 +23,13 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric import \ LanFabric + class Fabrics(LanFabric): """ - V1 API Fabrics endpoints common methods and properties. + ## V1 API Fabrics - Fabrics + + ### Description + Fabrics endpoints common methods and properties. """ def __init__(self): @@ -33,7 +37,8 @@ def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.rest_control_fabrics = f"{self.lan_fabric}/rest/control/fabrics" - self.log.debug("ENTERED api.v1.LanFabric.Fabrics()") + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) self._build_properties() def _build_properties(self): @@ -59,14 +64,15 @@ def fabric_name(self, value): except (TypeError, ValueError) as error: msg = f"{self.class_name}.{method_name}: " msg += f"{error}" - raise ValueError(msg) + raise ValueError(msg) from error self.properties["fabric_name"] = value @property - def path(self): + def path_fabric_name(self): """ - - Override the path property to mandate fabric_name is set. - - Raise ``ValueError`` if fabric_name is not set. + - Endpoint path property, including fabric_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". """ method_name = inspect.stack()[0][3] if self.fabric_name is None and "fabric_name" in self.required_properties: @@ -75,16 +81,37 @@ def path(self): raise ValueError(msg) return f"{self.rest_control_fabrics}/{self.fabric_name}" + class EpFabricConfigDeploy(Fabrics): """ - - V1 API Fabrics: fabric config-deploy endpoint. - - parameters: - - force_show_run: boolean - - default: False - - include_all_msd_switches: boolean - - default: False - - fabric_name: string - - required + ## V1 API Fabrics - EpFabricConfigDeploy + + ### Description + Return endpoint to initiate config-deploy on fabric_name. + + ### Raises + - ValueError: If fabric_name is not set. + - ValueError: If fabric_name is invalid. + - ValueError: If force_show_run is not boolean. + - ValueError: If include_all_msd_switches is not boolean. + + ### Parameters: + - force_show_run: boolean + - default: False + - include_all_msd_switches: boolean + - default: False + - fabric_name: string + - required + + ### Usage + ```python + fabric_config_deploy = EpFabricConfigDeploy() + fabric_config_deploy.fabric_name = "MyFabric" + fabric_config_deploy.force_show_run = True + fabric_config_deploy.include_all_msd_switches = True + path = fabric_config_deploy.path + verb = fabric_config_deploy.verb + ``` """ def __init__(self): @@ -93,7 +120,8 @@ def __init__(self): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.required_properties.add("fabric_name") self._build_properties() - self.log.debug(f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}") + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) def _build_properties(self): super()._build_properties() @@ -104,9 +132,9 @@ def _build_properties(self): @property def force_show_run(self): """ - - getter: Return the force_show_run. - - setter: Set the force_show_run. - - setter: Raise ``ValueError`` if force_show_run is not valid. + - getter: Return the force_show_run value. + - setter: Set the force_show_run value. + - setter: Raise ``ValueError`` if force_show_run is not a boolean. - Default: False """ return self.properties["force_show_run"] @@ -125,7 +153,7 @@ def include_all_msd_switches(self): """ - getter: Return the include_all_msd_switches. - setter: Set the include_all_msd_switches. - - setter: Raise ``ValueError`` if include_all_msd_switches is not valid. + - setter: Raise ``ValueError`` if include_all_msd_switches is a boolean. - Default: False """ return self.properties["include_all_msd_switches"] @@ -145,21 +173,111 @@ def path(self): - Override the path property to mandate fabric_name is set. - Raise ``ValueError`` if fabric_name is not set. """ - method_name = inspect.stack()[0][3] - if self.fabric_name is None and "fabric_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be set prior to accessing path." - raise ValueError(msg) - _path = f"{self.rest_control_fabrics}/{self.fabric_name}" + _path = self.path_fabric_name _path += "/config-deploy?" _path += f"forceShowRun={self.force_show_run}" _path += f"&inclAllMSDSwitches={self.include_all_msd_switches}" return _path +class EpFabricConfigSave(Fabrics): + """ + ## V1 API Fabrics - EpFabricConfigSave + + ### Description + Return endpoint to initiate config-save on fabric_name. + + ### Raises + - ValueError: If fabric_name is not set. + - ValueError: If fabric_name is invalid. + - ValueError: If ticket_id is not a string. + + ### Parameters: + - fabric_name: string + - required + - ticket_id: string + - optional unless Change Control is enabled + + ### Usage + ```python + fabric_config_save = EpFabricConfigSave() + fabric_config_save.fabric_name = "MyFabric" + fabric_config_save.ticket_id = "MyTicket1234" + path = fabric_config_save.path + verb = fabric_config_save.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + self.properties["ticket_id"] = None + + @property + def ticket_id(self): + """ + - getter: Return the ticket_id. + - setter: Set the ticket_id. + - setter: Raise ``ValueError`` if ticket_id is not a string. + - Default: None + - Note: ticket_id is optional unless Change Control is enabled. + """ + return self.properties["ticket_id"] + + @ticket_id.setter + def ticket_id(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name}: " + msg += "ticket_id must be a string." + raise ValueError(msg) + self.properties["ticket_id"] = value + + @property + def path(self): + """ + - Endpoint for config-save. + - Set self.ticket_id if Change Control is enabled. + - Raise ``ValueError`` if fabric_name is not set. + """ + _path = self.path_fabric_name + _path += "/config-save" + if self.ticket_id: + _path += f"?ticketId={self.ticket_id}" + return _path + + class EpFabricDelete(Fabrics): """ - V1 API Fabrics: fabric delete endpoint. + ## V1 API Fabrics - EpFabricDelete + + ### Description + Return endpoint to delete ``fabric_name``. + + ### Raises + - ValueError: If fabric_name is not set. + - ValueError: If fabric_name is invalid. + + ### Parameters + - fabric_name: string + - required + + ### Usage + ```python + fabric_delete = EpFabricDelete() + fabric_delete.fabric_name = "MyFabric" + path = fabric_delete.path + verb = fabric_delete.verb + ``` """ def __init__(self): @@ -168,15 +286,44 @@ def __init__(self): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.required_properties.add("fabric_name") self._build_properties() - self.log.debug(f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}") + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) def _build_properties(self): super()._build_properties() self.properties["verb"] = "DELETE" + @property + def path(self): + """ + - Endpoint for fabric delete. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name + + class EpFabricDetails(Fabrics): """ - V1 API Fabrics: fabric details endpoint. + ## V1 API Fabrics - EpFabricDetails + + ### Description + Return the endpoint to query ``fabric_name`` details. + + ### Raises + - ValueError: If fabric_name is not set. + - ValueError: If fabric_name is invalid. + + ### Parameters + - fabric_name: string + - required + + ### Usage + ```python + fabric_details = EpFabricDelete() + fabric_details.fabric_name = "MyFabric" + path = fabric_details.path + verb = fabric_details.verb + ``` """ def __init__(self): @@ -185,8 +332,55 @@ def __init__(self): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.required_properties.add("fabric_name") self._build_properties() - self.log.debug(f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}") + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) def _build_properties(self): super()._build_properties() self.properties["verb"] = "GET" + + @property + def path(self): + return self.path_fabric_name + + +class EpFabricFreezeMode(Fabrics): + """ + ## V1 API Fabrics - EpFabricFreezeMode + + ### Description + Return the endpoint to query ``fabric_name`` freezemode status. + + ### Raises + - ValueError: If fabric_name is not set. + - ValueError: If fabric_name is invalid. + + ### Parameters + - fabric_name: string + - required + + ### Usage + ```python + fabric_details = EpFabricDelete() + fabric_details.fabric_name = "MyFabric" + path = fabric_details.path + verb = fabric_details.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return f"{self.path_fabric_name}/freezemode" From f25db965e7bb450d79824254656b3340b0f66470 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 12 May 2024 11:24:16 -1000 Subject: [PATCH 024/230] Consistent docstring structure. 1. Add Endpoint section to all docstrings. 2. Modify all previously-unmodified docstrings for consistency. 3. Run thru black, isort, pylint. --- plugins/module_utils/common/api/common.py | 8 +++- plugins/module_utils/common/api/v1/fm.py | 23 ++++++++++-- .../module_utils/common/api/v1/lan_fabric.py | 8 +++- .../common/api/v1/rest/control/fabrics.py | 37 +++++++++++++++---- .../module_utils/common/api/v1/v1_common.py | 8 +++- 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/plugins/module_utils/common/api/common.py b/plugins/module_utils/common/api/common.py index 9dcd90eaf..59c83d45a 100644 --- a/plugins/module_utils/common/api/common.py +++ b/plugins/module_utils/common/api/common.py @@ -25,7 +25,13 @@ class Common: """ - API endpoints common methods and properties. + ## API endpoints - Common + + ### Description + Common methods and properties for subclasses. + + ### Endpoint + ``/appcenter/cisco/ndfc/api`` """ def __init__(self): diff --git a/plugins/module_utils/common/api/v1/fm.py b/plugins/module_utils/common/api/v1/fm.py index be025ebd8..cb1c01e1b 100644 --- a/plugins/module_utils/common/api/v1/fm.py +++ b/plugins/module_utils/common/api/v1/fm.py @@ -25,7 +25,12 @@ class FM(V1Common): """ - V1 API Feature Manager (FM) endpoints common methods and properties. + ## V1 API Feature Manager (FM) + + ### Description + Common methods and properties for + ``/appcenter/cisco/ndfc/api/v1/fm`` + endpoints. """ def __init__(self): @@ -38,7 +43,13 @@ def __init__(self): class Features(FM): """ - V1 API Feature Manager (FM) features endpoint. + ## V1 API Feature Manager (FM) - Features + + ### Description + Common methods and properties + + ### Endpoint + ``/fm/features`` """ def __init__(self): @@ -55,7 +66,13 @@ def _build_properties(self): class Version(FM): """ - V1 API Feature Manager (FM) about/version endpoint. + ## V1 API Feature Manager (FM) about/version. + + ### Description + Common methods and properties + + ### Endpoint + ``/fm/about/version`` """ def __init__(self): diff --git a/plugins/module_utils/common/api/v1/lan_fabric.py b/plugins/module_utils/common/api/v1/lan_fabric.py index 5e84b68ba..12bc0446d 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric.py +++ b/plugins/module_utils/common/api/v1/lan_fabric.py @@ -25,7 +25,13 @@ class LanFabric(V1Common): """ - V1 API lan-fabrics endpoints common methods and properties. + ## V1 API - LanFabric() + + ### Description + Common methods and properties for LanFabric() subclasses + + ### Endpoint + ``/appcenter/cisco/ndfc/api/v1/lan-fabric`` """ def __init__(self): diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py index e232f18cc..9c08fb6b8 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -11,7 +11,7 @@ # 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. - +# pylint: disable=line-too-long from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -26,10 +26,13 @@ class Fabrics(LanFabric): """ - ## V1 API Fabrics - Fabrics + ## V1 API Fabrics - LanFabric().Fabrics() ### Description - Fabrics endpoints common methods and properties. + Common methods and properties for Fabrics() subclasses. + + ### Endpoint + - ``/lan-fabric/rest/control/fabrics/{fabric_name}`` """ def __init__(self): @@ -84,11 +87,16 @@ def path_fabric_name(self): class EpFabricConfigDeploy(Fabrics): """ - ## V1 API Fabrics - EpFabricConfigDeploy + ## V1 API - Fabrics().EpFabricConfigDeploy() ### Description Return endpoint to initiate config-deploy on fabric_name. + ### Endpoint + - ``/fabrics/{fabric_name}/config-deploy`` + - ``/fabrics/{fabric_name}/config-deploy?forceShowRun={force_show_run}`` + - ``/fabrics/{fabric_name}/config-deploy?inclAllMSDSwitches={include_all_msd_switches}`` + ### Raises - ValueError: If fabric_name is not set. - ValueError: If fabric_name is invalid. @@ -182,11 +190,15 @@ def path(self): class EpFabricConfigSave(Fabrics): """ - ## V1 API Fabrics - EpFabricConfigSave + ## V1 API - Fabrics().EpFabricConfigSave() ### Description Return endpoint to initiate config-save on fabric_name. + Endpoint: + - ``/fabrics/{fabric_name}/config-save`` + - ``/fabrics/{fabric_name}/config-save?ticketId={ticket_id}`` + ### Raises - ValueError: If fabric_name is not set. - ValueError: If fabric_name is invalid. @@ -258,11 +270,14 @@ def path(self): class EpFabricDelete(Fabrics): """ - ## V1 API Fabrics - EpFabricDelete + ## V1 API - Fabrics().EpFabricDelete() ### Description Return endpoint to delete ``fabric_name``. + ### Endpoint + ``/fabrics/{fabric_name}`` + ### Raises - ValueError: If fabric_name is not set. - ValueError: If fabric_name is invalid. @@ -304,11 +319,14 @@ def path(self): class EpFabricDetails(Fabrics): """ - ## V1 API Fabrics - EpFabricDetails + ## V1 API - Fabrics().EpFabricDetails() ### Description Return the endpoint to query ``fabric_name`` details. + ### Endpoint + ``/fabrics/{fabric_name}`` + ### Raises - ValueError: If fabric_name is not set. - ValueError: If fabric_name is invalid. @@ -346,11 +364,14 @@ def path(self): class EpFabricFreezeMode(Fabrics): """ - ## V1 API Fabrics - EpFabricFreezeMode + ## V1 API - Fabrics().EpFabricFreezeMode() ### Description Return the endpoint to query ``fabric_name`` freezemode status. + ### Endpoint + ``/fabrics/{fabric_name}/freezemode`` + ### Raises - ValueError: If fabric_name is not set. - ValueError: If fabric_name is invalid. diff --git a/plugins/module_utils/common/api/v1/v1_common.py b/plugins/module_utils/common/api/v1/v1_common.py index c4382e857..a5bcb6e9c 100644 --- a/plugins/module_utils/common/api/v1/v1_common.py +++ b/plugins/module_utils/common/api/v1/v1_common.py @@ -25,7 +25,13 @@ class V1Common(Common): """ - v1 API enpoints common methods and properties. + ## v1 API enpoints - V1Common + + ### Description + Common methods and properties for subclasses. + + ### Endpoint + ``/appcenter/cisco/ndfc/api/v1/`` """ def __init__(self): From 5fb4e11954095a9b2a2c95c8bf06d81f2d519966 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 May 2024 10:01:51 -1000 Subject: [PATCH 025/230] Rename v1_common (V1Common) to common_v1 (CommonV1) --- .../api/v1/{v1_common.py => common_v1.py} | 6 +++--- plugins/module_utils/common/api/v1/fm.py | 19 +++++++++---------- .../module_utils/common/api/v1/lan_fabric.py | 8 ++++---- 3 files changed, 16 insertions(+), 17 deletions(-) rename plugins/module_utils/common/api/v1/{v1_common.py => common_v1.py} (90%) diff --git a/plugins/module_utils/common/api/v1/v1_common.py b/plugins/module_utils/common/api/v1/common_v1.py similarity index 90% rename from plugins/module_utils/common/api/v1/v1_common.py rename to plugins/module_utils/common/api/v1/common_v1.py index a5bcb6e9c..30e568e7b 100644 --- a/plugins/module_utils/common/api/v1/v1_common.py +++ b/plugins/module_utils/common/api/v1/common_v1.py @@ -23,12 +23,12 @@ Common -class V1Common(Common): +class CommonV1(Common): """ - ## v1 API enpoints - V1Common + ## v1 API enpoints - Common().CommonV1() ### Description - Common methods and properties for subclasses. + Common methods and properties for API v1 subclasses. ### Endpoint ``/appcenter/cisco/ndfc/api/v1/`` diff --git a/plugins/module_utils/common/api/v1/fm.py b/plugins/module_utils/common/api/v1/fm.py index cb1c01e1b..1d96dc952 100644 --- a/plugins/module_utils/common/api/v1/fm.py +++ b/plugins/module_utils/common/api/v1/fm.py @@ -19,18 +19,17 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.v1_common import \ - V1Common +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common_v1 import \ + CommonV1 -class FM(V1Common): +class FM(CommonV1): """ - ## V1 API Feature Manager (FM) + ## V1 API Feature Manager (FM) - CommonV1().FM() ### Description - Common methods and properties for + Common methods and properties for FM() subclasses ``/appcenter/cisco/ndfc/api/v1/fm`` - endpoints. """ def __init__(self): @@ -38,12 +37,12 @@ def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.fm = f"{self.api_v1}/fm" - self.log.debug("ENTERED api.v1.Common()") + self.log.debug("ENTERED api.v1.CommonV1()") -class Features(FM): +class EpFeatures(FM): """ - ## V1 API Feature Manager (FM) - Features + ## V1 API Feature Manager (FM) - FM().EpFeatures() ### Description Common methods and properties @@ -64,7 +63,7 @@ def _build_properties(self): self.properties["verb"] = "GET" -class Version(FM): +class EpVersion(FM): """ ## V1 API Feature Manager (FM) about/version. diff --git a/plugins/module_utils/common/api/v1/lan_fabric.py b/plugins/module_utils/common/api/v1/lan_fabric.py index 12bc0446d..7f9519933 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric.py +++ b/plugins/module_utils/common/api/v1/lan_fabric.py @@ -19,16 +19,16 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.v1_common import \ - V1Common +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common_v1 import \ + CommonV1 -class LanFabric(V1Common): +class LanFabric(CommonV1): """ ## V1 API - LanFabric() ### Description - Common methods and properties for LanFabric() subclasses + Common methods and properties for CommonV1().LanFabric() subclasses ### Endpoint ``/appcenter/cisco/ndfc/api/v1/lan-fabric`` From dca6e435a598594c8cc3cac0e453f53dd75f2b2e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 May 2024 13:03:44 -1000 Subject: [PATCH 026/230] dcnm_endpoints: Add stagingmanagement, imagemanagement v1/__init__.py v1/image_management.py - ImageManagement() v1/rest/staging_management.py - StagingManagement() - EpStageImage() - EpStageInfo() - EpValidateImage() v1/rest/image_upgrade.py - ImageUpgrade() - EpInstallOptions() - EpUpgradeImage() --- .../module_utils/common/api/v1/__init__.py | 0 .../common/api/v1/image_management.py | 42 ++++++ .../common/api/v1/rest/image_upgrade.py | 121 ++++++++++++++++++ .../common/api/v1/rest/staging_management.py | 111 ++++++++++++++++ 4 files changed, 274 insertions(+) create mode 100644 plugins/module_utils/common/api/v1/__init__.py create mode 100644 plugins/module_utils/common/api/v1/image_management.py create mode 100644 plugins/module_utils/common/api/v1/rest/image_upgrade.py create mode 100644 plugins/module_utils/common/api/v1/rest/staging_management.py diff --git a/plugins/module_utils/common/api/v1/__init__.py b/plugins/module_utils/common/api/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/api/v1/image_management.py b/plugins/module_utils/common/api/v1/image_management.py new file mode 100644 index 000000000..fe31d0e33 --- /dev/null +++ b/plugins/module_utils/common/api/v1/image_management.py @@ -0,0 +1,42 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common_v1 import \ + CommonV1 + + +class ImageManagement(CommonV1): + """ + ## V1 API - ImageManagement() + + ### Description + Common methods and properties for CommonV1().ImageManagement() subclasses + + ### Endpoint + ``/appcenter/cisco/ndfc/api/v1/imagemanagement`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.image_management = f"{self.api_v1}/imagemanagement" + self.log.debug("ENTERED api.v1.ImageManagement()") diff --git a/plugins/module_utils/common/api/v1/rest/image_upgrade.py b/plugins/module_utils/common/api/v1/rest/image_upgrade.py new file mode 100644 index 000000000..5a4fedecd --- /dev/null +++ b/plugins/module_utils/common/api/v1/rest/image_upgrade.py @@ -0,0 +1,121 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.image_management import \ + ImageManagement + + +class ImageUpgrade(ImageManagement): + """ + ## V1 API Fabrics - ImageManagement().ImageUpgrade() + + ### Description + Common methods and properties for ImageUpgrade() subclasses. + + ### Endpoint + - ``/imagemanagement/rest/imageupgrade`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.rest_image_upgrade = f"{self.image_management}/rest/imageupgrade" + msg = f"ENTERED api.v1.ImageManagement.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Add any class-specific properties to self.properties. + """ + + +class EpInstallOptions(ImageUpgrade): + """ + ## V1 API - Fabrics().EpInstallOptions() + + ### Description + Return endpoint information for install-options. + + ### Endpoint + - ``/rest/imageupgrade/install-options`` + + ### Raises + + ### Parameters: + + ### Usage + ```python + ep_install_options = EpInstallOptions() + path = ep_install_options.path + verb = ep_install_options.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + msg = f"ENTERED api.v1.ImageUpgrade.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + self.properties["path"] = f"{self.rest_image_upgrade}/install-options" + + +class EpUpgradeImage(ImageUpgrade): + """ + ## V1 API - Fabrics().EpUpgradeImage() + + ### Description + Return endpoint information for upgrade-image. + + ### Endpoint + - ``/rest/imageupgrade/upgrade-image`` + + ### Raises + + ### Parameters: + + ### Usage + ```python + ep_upgrade_image = EpUpgradeImage() + path = ep_upgrade_image.path + verb = ep_upgrade_image.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + msg = f"ENTERED api.v1.ImageUpgrade.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + self.properties["path"] = f"{self.rest_image_upgrade}/upgrade-image" diff --git a/plugins/module_utils/common/api/v1/rest/staging_management.py b/plugins/module_utils/common/api/v1/rest/staging_management.py new file mode 100644 index 000000000..bf40e372b --- /dev/null +++ b/plugins/module_utils/common/api/v1/rest/staging_management.py @@ -0,0 +1,111 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.image_management import \ + ImageManagement + + +class StagingManagement(ImageManagement): + """ + ## V1 API - ImageManagement().StagingManagement() + + ### Description + Common methods and properties for StagingManagement() subclasses + + ### Endpoint + ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.staging_management = f"{self.image_management}/rest/stagingmanagement" + self.log.debug("ENTERED api.v1.StagingManagement()") + + +class EpStageImage(StagingManagement): + """ + ## V1 API - StagingManagement().EpStageImage() + + ### Description + Return endpoint information for stage-image. + + ### Endpoint + - ``/rest/stagingmanagement/stage-image`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.StagingManagement.EpStageImage()") + self._build_properties() + + def _build_properties(self): + self.properties["path"] = f"{self.staging_management}/stage-image" + self.properties["verb"] = "POST" + + +class EpStageInfo(StagingManagement): + """ + ## V1 API - StagingManagement().EpStageInfo() + + ### Description + Return endpoint information for stage-info. + + ### Endpoint + - ``/rest/stagingmanagement/stage-info`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.StagingManagement.EpStageInfo()") + self._build_properties() + + def _build_properties(self): + self.properties["path"] = f"{self.staging_management}/stage-info" + self.properties["verb"] = "GET" + + +class EpValidateImage(StagingManagement): + """ + ## V1 API - StagingManagement().EpValidateImage() + + ### Description + Return endpoint information for validate-image. + + ### Endpoint + - ``/rest/stagingmanagement/validate-image`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.StagingManagement.EpValidateImage()") + self._build_properties() + + def _build_properties(self): + self.properties["path"] = f"{self.staging_management}/validate-image" + self.properties["verb"] = "POST" From b09f62fc07ff48e0b3f69d9efe212b6e92110353 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 May 2024 15:44:22 -1000 Subject: [PATCH 027/230] dcnm_endpoints: Add ImageMgmt endpoints /api/v1/imagemanagement/rest/imagemgnt - ImageMgmt() - EpBootFlashInfo() --- .../common/api/v1/rest/image_mgmt.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 plugins/module_utils/common/api/v1/rest/image_mgmt.py diff --git a/plugins/module_utils/common/api/v1/rest/image_mgmt.py b/plugins/module_utils/common/api/v1/rest/image_mgmt.py new file mode 100644 index 000000000..895f8fe85 --- /dev/null +++ b/plugins/module_utils/common/api/v1/rest/image_mgmt.py @@ -0,0 +1,65 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.image_management import \ + ImageManagement + + +class ImageMgmt(ImageManagement): + """ + ## V1 API - ImageManagement().ImageMgmt() + + ### Description + Common methods and properties for ImageMgmt() subclasses + + ### Endpoint + ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imagemgnt`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.image_mgmt = f"{self.image_management}/rest/imagemgnt" + self.log.debug("ENTERED api.v1.ImageMgmt()") + + +class EpBootFlashInfo(ImageMgmt): + """ + ## V1 API - ImageMgmt().EpBootFlashInfo() + + ### Description + Return endpoint information for bootflash-info. + + ### Endpoint + - ``/rest/imagemgnt/bootFlash/bootflash-info`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.ImageMgmt.EpBootFlash()") + self._build_properties() + + def _build_properties(self): + self.properties["path"] = f"{self.image_mgmt}/bootFlash/bootflash-info" + self.properties["verb"] = "GET" From 96f46bf7fbe4a074f3caf5273c6a177e26a21a1b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 May 2024 16:42:05 -1000 Subject: [PATCH 028/230] image_mgmt.py rename to image_mgnt.py to parallel NDFC --- .../api/v1/rest/{image_mgmt.py => image_mgnt.py} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename plugins/module_utils/common/api/v1/rest/{image_mgmt.py => image_mgnt.py} (86%) diff --git a/plugins/module_utils/common/api/v1/rest/image_mgmt.py b/plugins/module_utils/common/api/v1/rest/image_mgnt.py similarity index 86% rename from plugins/module_utils/common/api/v1/rest/image_mgmt.py rename to plugins/module_utils/common/api/v1/rest/image_mgnt.py index 895f8fe85..a4916f96f 100644 --- a/plugins/module_utils/common/api/v1/rest/image_mgmt.py +++ b/plugins/module_utils/common/api/v1/rest/image_mgnt.py @@ -23,12 +23,12 @@ ImageManagement -class ImageMgmt(ImageManagement): +class ImageMgnt(ImageManagement): """ - ## V1 API - ImageManagement().ImageMgmt() + ## V1 API - ImageManagement().ImageMgnt() ### Description - Common methods and properties for ImageMgmt() subclasses + Common methods and properties for ImageMgnt() subclasses ### Endpoint ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imagemgnt`` @@ -42,9 +42,9 @@ def __init__(self): self.log.debug("ENTERED api.v1.ImageMgmt()") -class EpBootFlashInfo(ImageMgmt): +class EpBootFlashInfo(ImageMgnt): """ - ## V1 API - ImageMgmt().EpBootFlashInfo() + ## V1 API - ImageMgnt().EpBootFlashInfo() ### Description Return endpoint information for bootflash-info. @@ -57,7 +57,7 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.ImageMgmt.EpBootFlash()") + self.log.debug("ENTERED api.v1.ImageMgnt.EpBootFlash()") self._build_properties() def _build_properties(self): From 690e4b2ce24f1a428bd5cc381741d6940cd54e47 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 May 2024 16:44:06 -1000 Subject: [PATCH 029/230] Rename staging_management classes EpStageImage() -> EpImageStage() EpValidateImage() -> EpImageValidate() --- .../common/api/v1/rest/staging_management.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/plugins/module_utils/common/api/v1/rest/staging_management.py b/plugins/module_utils/common/api/v1/rest/staging_management.py index bf40e372b..36bc3c12e 100644 --- a/plugins/module_utils/common/api/v1/rest/staging_management.py +++ b/plugins/module_utils/common/api/v1/rest/staging_management.py @@ -42,9 +42,9 @@ def __init__(self): self.log.debug("ENTERED api.v1.StagingManagement()") -class EpStageImage(StagingManagement): +class EpImageStage(StagingManagement): """ - ## V1 API - StagingManagement().EpStageImage() + ## V1 API - StagingManagement().EpImageStage() ### Description Return endpoint information for stage-image. @@ -57,7 +57,7 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.StagingManagement.EpStageImage()") + self.log.debug("ENTERED api.v1.StagingManagement.EpImageStage()") self._build_properties() def _build_properties(self): @@ -65,47 +65,47 @@ def _build_properties(self): self.properties["verb"] = "POST" -class EpStageInfo(StagingManagement): +class EpImageValidate(StagingManagement): """ - ## V1 API - StagingManagement().EpStageInfo() + ## V1 API - StagingManagement().EpImageValidate() ### Description - Return endpoint information for stage-info. + Return endpoint information for validate-image. ### Endpoint - - ``/rest/stagingmanagement/stage-info`` + - ``/rest/stagingmanagement/validate-image`` """ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.StagingManagement.EpStageInfo()") + self.log.debug("ENTERED api.v1.StagingManagement.EpImageValidate()") self._build_properties() def _build_properties(self): - self.properties["path"] = f"{self.staging_management}/stage-info" - self.properties["verb"] = "GET" + self.properties["path"] = f"{self.staging_management}/validate-image" + self.properties["verb"] = "POST" -class EpValidateImage(StagingManagement): +class EpStageInfo(StagingManagement): """ - ## V1 API - StagingManagement().EpValidateImage() + ## V1 API - StagingManagement().EpStageInfo() ### Description - Return endpoint information for validate-image. + Return endpoint information for stage-info. ### Endpoint - - ``/rest/stagingmanagement/validate-image`` + - ``/rest/stagingmanagement/stage-info`` """ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.StagingManagement.EpValidateImage()") + self.log.debug("ENTERED api.v1.StagingManagement.EpStageInfo()") self._build_properties() def _build_properties(self): - self.properties["path"] = f"{self.staging_management}/validate-image" - self.properties["verb"] = "POST" + self.properties["path"] = f"{self.staging_management}/stage-info" + self.properties["verb"] = "GET" From 3f7b99102f12e54bf752e309be648a1922f47260 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 13 May 2024 16:44:33 -1000 Subject: [PATCH 030/230] dcnm_endpoints: Add policy_mgnt endpoint classes --- .../common/api/v1/rest/policy_mgnt.py | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 plugins/module_utils/common/api/v1/rest/policy_mgnt.py diff --git a/plugins/module_utils/common/api/v1/rest/policy_mgnt.py b/plugins/module_utils/common/api/v1/rest/policy_mgnt.py new file mode 100644 index 000000000..6dabd2fb0 --- /dev/null +++ b/plugins/module_utils/common/api/v1/rest/policy_mgnt.py @@ -0,0 +1,222 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.image_management import \ + ImageManagement + + +class PolicyMgnt(ImageManagement): + """ + ## V1 API - ImageManagement().PolicyMgnt() + + ### Description + Common methods and properties for PolicyMgnt() subclasses + + ### Endpoint + ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.policy_mgmt = f"{self.image_management}/rest/policymgnt" + self.log.debug("ENTERED api.v1.PolicyMgnt()") + + +class EpPolicies(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPolicies() + + ### Description + Return endpoint information. + + ### Endpoint path + - ``/rest/policymgnt/policies`` + + ### Endpoint verb + - GET + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.PolicyMgnt.EpPolicies()") + self._build_properties() + + def _build_properties(self): + self.properties["path"] = f"{self.policy_mgmt}/policies" + self.properties["verb"] = "GET" + + +class EpPoliciesAllAttached(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPoliciesAllAttached() + + ### Description + Return endpoint information. + + ### Endpoint + - ``/rest/policymgnt/all-attached-policies`` + + ### Endpoint verb + - GET + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.PolicyMgnt.EpPoliciesAllAttached()") + self._build_properties() + + def _build_properties(self): + self.properties["path"] = f"{self.policy_mgmt}/all-attached-policies" + self.properties["verb"] = "GET" + + +class EpPolicyAttach(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPolicyAttach() + + ### Description + Return endpoint information. + + ### Endpoint + - ``/rest/policymgnt/attach-policy`` + + ### Endpoint verb + - POST + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.PolicyMgnt.EpPolicyAttach()") + self._build_properties() + + def _build_properties(self): + self.properties["path"] = f"{self.policy_mgmt}/attach-policy" + self.properties["verb"] = "POST" + + +class EpPolicyCreate(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPolicyCreate() + + ### Description + Return endpoint information. + + ### Endpoint path + - ``/rest/policymgnt/platform-policy`` + + ### Endpoint verb + - POST + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.PolicyMgnt.EpPolicyCreate()") + self._build_properties() + + def _build_properties(self): + self.properties["path"] = f"{self.policy_mgmt}/platform-policy" + self.properties["verb"] = "POST" + + +class EpPolicyDetach(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPolicyDetach() + + ### Description + Return endpoint information for detach-policy. + + ### Endpoint + - ``/rest/policymgnt/detach-policy`` + + ### Endpoint verb + - DELETE + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.PolicyMgnt.EpPolicyDetach()") + self._build_properties() + + def _build_properties(self): + self.properties["path"] = f"{self.policy_mgmt}/detach-policy" + self.properties["verb"] = "DELETE" + + +class EpPolicyInfo(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPolicyInfo() + + ### Description + Return endpoint information for detach-policy. + + ### Endpoint + - ``/rest/policymgnt/image-policy/{policy_name}`` + + ### Endpoint verb + - GET + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.PolicyMgnt.EpPolicyDetach()") + self._build_properties() + + def _build_properties(self): + self.properties["policy_name"] = None + self.properties["path"] = f"{self.policy_mgmt}/image-policy" + self.properties["verb"] = "GET" + + @property + def path(self): + method_name = inspect.stack()[0][3] + if self.policy_name is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.policy_name must be set before " + msg += f"accessing {method_name}." + raise ValueError(msg) + return f"{self.properties["path"]}/{self.policy_name}" + + @property + def policy_name(self): + """ + - getter: Return the policy_name. + - setter: Set the policy_name. + """ + return self.properties["policy_name"] + + @policy_name.setter + def policy_name(self, value): + self.properties["policy_name"] = value From a75f838320ee26a10d5098a1d61fdb72daec37f1 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 May 2024 09:12:16 -1000 Subject: [PATCH 031/230] dcnm_endpoints: Add UT, more... 1. Add unit tests for the following: - staging_management - policy_mgnt - image_upgrade - image_mgnt 2. Rename docstring Endpoint section to Path, throughout. 3. Add Verb section to docstrings throughout. 4. Move Raises section in docstrings to directly after Description throughout. 5. ControllerFeatures(): Modify to align with renamed EpFeatures() class. --- plugins/module_utils/common/api/__init__.py | 0 plugins/module_utils/common/api/common.py | 2 +- .../module_utils/common/api/v1/common_v1.py | 2 +- plugins/module_utils/common/api/v1/fm.py | 10 +- .../common/api/v1/image_management.py | 2 +- .../module_utils/common/api/v1/lan_fabric.py | 2 +- .../common/api/v1/rest/control/fabrics.py | 89 +++++++----- .../common/api/v1/rest/image_mgnt.py | 7 +- .../common/api/v1/rest/image_upgrade.py | 6 +- .../common/api/v1/rest/policy_mgnt.py | 33 +++-- .../common/api/v1/rest/staging_management.py | 17 ++- .../common/controller_features.py | 4 +- .../unit/module_utils/common/api/__init__.py | 0 .../common/api/test_v1_api_image_mgnt.py | 39 ++++++ .../api/test_v1_api_image_upgrade_ep.py | 53 +++++++ .../common/api/test_v1_api_policy_mgnt.py | 129 ++++++++++++++++++ .../api/test_v1_api_staging_management.py | 67 +++++++++ 17 files changed, 393 insertions(+), 69 deletions(-) create mode 100644 plugins/module_utils/common/api/__init__.py create mode 100644 tests/unit/module_utils/common/api/__init__.py create mode 100644 tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py create mode 100644 tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py create mode 100644 tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py create mode 100644 tests/unit/module_utils/common/api/test_v1_api_staging_management.py diff --git a/plugins/module_utils/common/api/__init__.py b/plugins/module_utils/common/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/api/common.py b/plugins/module_utils/common/api/common.py index 59c83d45a..1530e5f97 100644 --- a/plugins/module_utils/common/api/common.py +++ b/plugins/module_utils/common/api/common.py @@ -30,7 +30,7 @@ class Common: ### Description Common methods and properties for subclasses. - ### Endpoint + ### Path ``/appcenter/cisco/ndfc/api`` """ diff --git a/plugins/module_utils/common/api/v1/common_v1.py b/plugins/module_utils/common/api/v1/common_v1.py index 30e568e7b..ef3251384 100644 --- a/plugins/module_utils/common/api/v1/common_v1.py +++ b/plugins/module_utils/common/api/v1/common_v1.py @@ -30,7 +30,7 @@ class CommonV1(Common): ### Description Common methods and properties for API v1 subclasses. - ### Endpoint + ### Path ``/appcenter/cisco/ndfc/api/v1/`` """ diff --git a/plugins/module_utils/common/api/v1/fm.py b/plugins/module_utils/common/api/v1/fm.py index 1d96dc952..5ecd5ba7d 100644 --- a/plugins/module_utils/common/api/v1/fm.py +++ b/plugins/module_utils/common/api/v1/fm.py @@ -47,8 +47,11 @@ class EpFeatures(FM): ### Description Common methods and properties - ### Endpoint + ### Path ``/fm/features`` + + ### Verb + - GET """ def __init__(self): @@ -70,8 +73,11 @@ class EpVersion(FM): ### Description Common methods and properties - ### Endpoint + ### Path ``/fm/about/version`` + + ### Verb + - GET """ def __init__(self): diff --git a/plugins/module_utils/common/api/v1/image_management.py b/plugins/module_utils/common/api/v1/image_management.py index fe31d0e33..1a5ace0d2 100644 --- a/plugins/module_utils/common/api/v1/image_management.py +++ b/plugins/module_utils/common/api/v1/image_management.py @@ -30,7 +30,7 @@ class ImageManagement(CommonV1): ### Description Common methods and properties for CommonV1().ImageManagement() subclasses - ### Endpoint + ### Path ``/appcenter/cisco/ndfc/api/v1/imagemanagement`` """ diff --git a/plugins/module_utils/common/api/v1/lan_fabric.py b/plugins/module_utils/common/api/v1/lan_fabric.py index 7f9519933..2bbc95f61 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric.py +++ b/plugins/module_utils/common/api/v1/lan_fabric.py @@ -30,7 +30,7 @@ class LanFabric(CommonV1): ### Description Common methods and properties for CommonV1().LanFabric() subclasses - ### Endpoint + ### Path ``/appcenter/cisco/ndfc/api/v1/lan-fabric`` """ diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py index 9c08fb6b8..135b5be48 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -31,7 +31,7 @@ class Fabrics(LanFabric): ### Description Common methods and properties for Fabrics() subclasses. - ### Endpoint + ### Path - ``/lan-fabric/rest/control/fabrics/{fabric_name}`` """ @@ -92,16 +92,19 @@ class EpFabricConfigDeploy(Fabrics): ### Description Return endpoint to initiate config-deploy on fabric_name. - ### Endpoint + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If force_show_run is not boolean. + - ``ValueError``: If include_all_msd_switches is not boolean. + + ### Path - ``/fabrics/{fabric_name}/config-deploy`` - ``/fabrics/{fabric_name}/config-deploy?forceShowRun={force_show_run}`` - ``/fabrics/{fabric_name}/config-deploy?inclAllMSDSwitches={include_all_msd_switches}`` - ### Raises - - ValueError: If fabric_name is not set. - - ValueError: If fabric_name is invalid. - - ValueError: If force_show_run is not boolean. - - ValueError: If include_all_msd_switches is not boolean. + ### Verb + - POST ### Parameters: - force_show_run: boolean @@ -195,20 +198,23 @@ class EpFabricConfigSave(Fabrics): ### Description Return endpoint to initiate config-save on fabric_name. - Endpoint: - - ``/fabrics/{fabric_name}/config-save`` - - ``/fabrics/{fabric_name}/config-save?ticketId={ticket_id}`` - ### Raises - - ValueError: If fabric_name is not set. - - ValueError: If fabric_name is invalid. - - ValueError: If ticket_id is not a string. + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If ticket_id is not a string. + + ### Path + - ``/fabrics/{fabric_name}/config-save`` + - ``/fabrics/{fabric_name}/config-save?ticketId={ticket_id}`` + + ### Verb + - POST ### Parameters: - - fabric_name: string - - required - - ticket_id: string - - optional unless Change Control is enabled + - fabric_name: string + - required + - ticket_id: string + - optional unless Change Control is enabled ### Usage ```python @@ -275,16 +281,19 @@ class EpFabricDelete(Fabrics): ### Description Return endpoint to delete ``fabric_name``. - ### Endpoint - ``/fabrics/{fabric_name}`` - ### Raises - - ValueError: If fabric_name is not set. - - ValueError: If fabric_name is invalid. + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}`` + + ### Verb + - DELETE ### Parameters - - fabric_name: string - - required + - fabric_name: string + - required ### Usage ```python @@ -324,16 +333,19 @@ class EpFabricDetails(Fabrics): ### Description Return the endpoint to query ``fabric_name`` details. - ### Endpoint - ``/fabrics/{fabric_name}`` - ### Raises - - ValueError: If fabric_name is not set. - - ValueError: If fabric_name is invalid. + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}`` + + ### Verb + - GET ### Parameters - - fabric_name: string - - required + - fabric_name: string + - required ### Usage ```python @@ -369,12 +381,15 @@ class EpFabricFreezeMode(Fabrics): ### Description Return the endpoint to query ``fabric_name`` freezemode status. - ### Endpoint - ``/fabrics/{fabric_name}/freezemode`` - ### Raises - - ValueError: If fabric_name is not set. - - ValueError: If fabric_name is invalid. + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}/freezemode`` + + ### Verb + - GET ### Parameters - fabric_name: string diff --git a/plugins/module_utils/common/api/v1/rest/image_mgnt.py b/plugins/module_utils/common/api/v1/rest/image_mgnt.py index a4916f96f..fc7b2057e 100644 --- a/plugins/module_utils/common/api/v1/rest/image_mgnt.py +++ b/plugins/module_utils/common/api/v1/rest/image_mgnt.py @@ -30,7 +30,7 @@ class ImageMgnt(ImageManagement): ### Description Common methods and properties for ImageMgnt() subclasses - ### Endpoint + ### Path ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imagemgnt`` """ @@ -49,8 +49,11 @@ class EpBootFlashInfo(ImageMgnt): ### Description Return endpoint information for bootflash-info. - ### Endpoint + ### Path - ``/rest/imagemgnt/bootFlash/bootflash-info`` + + ### Verb + - GET """ def __init__(self): diff --git a/plugins/module_utils/common/api/v1/rest/image_upgrade.py b/plugins/module_utils/common/api/v1/rest/image_upgrade.py index 5a4fedecd..f9d9d5de2 100644 --- a/plugins/module_utils/common/api/v1/rest/image_upgrade.py +++ b/plugins/module_utils/common/api/v1/rest/image_upgrade.py @@ -30,7 +30,7 @@ class ImageUpgrade(ImageManagement): ### Description Common methods and properties for ImageUpgrade() subclasses. - ### Endpoint + ### Path - ``/imagemanagement/rest/imageupgrade`` """ @@ -56,7 +56,7 @@ class EpInstallOptions(ImageUpgrade): ### Description Return endpoint information for install-options. - ### Endpoint + ### Path - ``/rest/imageupgrade/install-options`` ### Raises @@ -92,7 +92,7 @@ class EpUpgradeImage(ImageUpgrade): ### Description Return endpoint information for upgrade-image. - ### Endpoint + ### Path - ``/rest/imageupgrade/upgrade-image`` ### Raises diff --git a/plugins/module_utils/common/api/v1/rest/policy_mgnt.py b/plugins/module_utils/common/api/v1/rest/policy_mgnt.py index 6dabd2fb0..061b56d6b 100644 --- a/plugins/module_utils/common/api/v1/rest/policy_mgnt.py +++ b/plugins/module_utils/common/api/v1/rest/policy_mgnt.py @@ -31,7 +31,7 @@ class PolicyMgnt(ImageManagement): ### Description Common methods and properties for PolicyMgnt() subclasses - ### Endpoint + ### Path ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt`` """ @@ -50,10 +50,10 @@ class EpPolicies(PolicyMgnt): ### Description Return endpoint information. - ### Endpoint path + ### Path path - ``/rest/policymgnt/policies`` - ### Endpoint verb + ### Verb - GET """ @@ -76,10 +76,10 @@ class EpPoliciesAllAttached(PolicyMgnt): ### Description Return endpoint information. - ### Endpoint + ### Path - ``/rest/policymgnt/all-attached-policies`` - ### Endpoint verb + ### Verb - GET """ @@ -102,10 +102,10 @@ class EpPolicyAttach(PolicyMgnt): ### Description Return endpoint information. - ### Endpoint + ### Path - ``/rest/policymgnt/attach-policy`` - ### Endpoint verb + ### Verb - POST """ @@ -128,10 +128,10 @@ class EpPolicyCreate(PolicyMgnt): ### Description Return endpoint information. - ### Endpoint path + ### Path path - ``/rest/policymgnt/platform-policy`` - ### Endpoint verb + ### Verb - POST """ @@ -152,12 +152,12 @@ class EpPolicyDetach(PolicyMgnt): ## V1 API - PolicyMgnt().EpPolicyDetach() ### Description - Return endpoint information for detach-policy. + Return endpoint information. - ### Endpoint + ### Path - ``/rest/policymgnt/detach-policy`` - ### Endpoint verb + ### Verb - DELETE """ @@ -178,12 +178,15 @@ class EpPolicyInfo(PolicyMgnt): ## V1 API - PolicyMgnt().EpPolicyInfo() ### Description - Return endpoint information for detach-policy. + Return endpoint information. + + ### Raises + - ``ValueError``: If path is accessed before setting policy_name. - ### Endpoint + ### Path - ``/rest/policymgnt/image-policy/{policy_name}`` - ### Endpoint verb + ### Verb - GET """ diff --git a/plugins/module_utils/common/api/v1/rest/staging_management.py b/plugins/module_utils/common/api/v1/rest/staging_management.py index 36bc3c12e..3f0eecd24 100644 --- a/plugins/module_utils/common/api/v1/rest/staging_management.py +++ b/plugins/module_utils/common/api/v1/rest/staging_management.py @@ -30,7 +30,7 @@ class StagingManagement(ImageManagement): ### Description Common methods and properties for StagingManagement() subclasses - ### Endpoint + ### Path ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement`` """ @@ -49,8 +49,11 @@ class EpImageStage(StagingManagement): ### Description Return endpoint information for stage-image. - ### Endpoint + ### Path - ``/rest/stagingmanagement/stage-image`` + + ### Verb + - POST """ def __init__(self): @@ -72,8 +75,11 @@ class EpImageValidate(StagingManagement): ### Description Return endpoint information for validate-image. - ### Endpoint + ### Path - ``/rest/stagingmanagement/validate-image`` + + ### Verb + - POST """ def __init__(self): @@ -95,8 +101,11 @@ class EpStageInfo(StagingManagement): ### Description Return endpoint information for stage-info. - ### Endpoint + ### Path - ``/rest/stagingmanagement/stage-info`` + + ### Verb + - GET """ def __init__(self): diff --git a/plugins/module_utils/common/controller_features.py b/plugins/module_utils/common/controller_features.py index cbdc94df0..930efe4ee 100644 --- a/plugins/module_utils/common/controller_features.py +++ b/plugins/module_utils/common/controller_features.py @@ -28,7 +28,7 @@ import logging from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm import \ - Features + EpFeatures from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ @@ -141,7 +141,7 @@ def __init__(self, params): raise ValueError(msg) self.conversion = ConversionUtils() - self.api_features = Features() + self.api_features = EpFeatures() self._init_properties() def _init_properties(self): diff --git a/tests/unit/module_utils/common/api/__init__.py b/tests/unit/module_utils/common/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py b/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py new file mode 100644 index 000000000..4c408d275 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py @@ -0,0 +1,39 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.image_mgnt import \ + EpBootFlashInfo +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + does_not_raise + +PATH_PREFIX = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imagemgnt" + + +def test_ep_image_mgnt_00010(): + """ + ### Class + - EpBootFlashInfo + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpBootFlashInfo() + assert instance.path == f"{PATH_PREFIX}/bootFlash/bootflash-info" + assert instance.verb == "GET" diff --git a/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py b/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py new file mode 100644 index 000000000..424f72509 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py @@ -0,0 +1,53 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.image_upgrade import ( + EpInstallOptions, EpUpgradeImage) +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + does_not_raise + +PATH_PREFIX = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imageupgrade" + + +def test_ep_install_options_00010(): + """ + ### Class + - EpInstallOptions + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpInstallOptions() + assert instance.path == f"{PATH_PREFIX}/install-options" + assert instance.verb == "POST" + + +def test_ep_upgrade_image_00010(): + """ + ### Class + - EpUpgradeImage + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpUpgradeImage() + assert instance.path == f"{PATH_PREFIX}/upgrade-image" + assert instance.verb == "POST" diff --git a/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py b/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py new file mode 100644 index 000000000..e6d6a79a2 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py @@ -0,0 +1,129 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.policy_mgnt import ( + EpPolicies, EpPoliciesAllAttached, EpPolicyAttach, EpPolicyCreate, + EpPolicyDetach, EpPolicyInfo) +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + does_not_raise + +PATH_PREFIX = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt" + + +def test_ep_policy_mgnt_00010(): + """ + ### Class + - EpPolicies + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpPolicies() + assert instance.path == f"{PATH_PREFIX}/policies" + assert instance.verb == "GET" + + +def test_ep_policy_mgnt_00020(): + """ + ### Class + - EpPolicyInfo + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpPolicyInfo() + instance.policy_name = "MyPolicy" + assert instance.path == f"{PATH_PREFIX}/image-policy/MyPolicy" + assert instance.verb == "GET" + + +def test_ep_policy_mgnt_00021(): + """ + ### Class + - EpPolicyInfo + + ### Summary + - Verify ``ValueError`` is raised if path is accessed before + setting policy_name. + """ + with does_not_raise(): + instance = EpPolicyInfo() + match = r"EpPolicyInfo\.path:\s+" + match += r"EpPolicyInfo\.policy_name must be set before accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_policy_mgnt_00030(): + """ + ### Class + - EpPoliciesAllAttached + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpPoliciesAllAttached() + assert instance.path == f"{PATH_PREFIX}/all-attached-policies" + assert instance.verb == "GET" + + +def test_ep_policy_mgnt_00040(): + """ + ### Class + - EpPolicyAttach + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpPolicyAttach() + assert instance.path == f"{PATH_PREFIX}/attach-policy" + assert instance.verb == "POST" + + +def test_ep_policy_mgnt_00050(): + """ + ### Class + - EpPolicyDetach + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpPolicyDetach() + assert instance.path == f"{PATH_PREFIX}/detach-policy" + assert instance.verb == "DELETE" + + +def test_ep_policy_mgnt_00060(): + """ + ### Class + - EpPolicyCreate + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpPolicyCreate() + assert instance.path == f"{PATH_PREFIX}/platform-policy" + assert instance.verb == "POST" diff --git a/tests/unit/module_utils/common/api/test_v1_api_staging_management.py b/tests/unit/module_utils/common/api/test_v1_api_staging_management.py new file mode 100644 index 000000000..5000cd500 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_v1_api_staging_management.py @@ -0,0 +1,67 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.staging_management import ( + EpImageStage, EpImageValidate, EpStageInfo) +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + does_not_raise + +PATH_PREFIX = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement" + + +def test_ep_staging_management_00010(): + """ + ### Class + - EpImageStage + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpImageStage() + assert instance.path == f"{PATH_PREFIX}/stage-image" + assert instance.verb == "POST" + + +def test_ep_staging_management_00020(): + """ + ### Class + - EpImageValidate + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpImageValidate() + assert instance.path == f"{PATH_PREFIX}/validate-image" + assert instance.verb == "POST" + + +def test_ep_staging_management_00030(): + """ + ### Class + - EpStageInfo + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpStageInfo() + assert instance.path == f"{PATH_PREFIX}/stage-info" + assert instance.verb == "GET" From c9716a7b72d155df943118b719d77ba5ca3b20bd Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 May 2024 10:17:42 -1000 Subject: [PATCH 032/230] dcnm_endpoints: Add UT for Fabrics, more... 1. Add unit tests for module_utils/common/api/v1/rest/control/fabrics.py 2. Modify property error messages for consistency. --- .../common/api/v1/rest/control/fabrics.py | 9 +- .../common/api/test_v1_api_fabrics.py | 417 ++++++++++++++++++ 2 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 tests/unit/module_utils/common/api/test_v1_api_fabrics.py diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py index 135b5be48..d05c10432 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -155,7 +155,8 @@ def force_show_run(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " - msg += "force_show_run must be a boolean." + msg += f"Expected boolean for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." raise ValueError(msg) self.properties["force_show_run"] = value @@ -174,7 +175,8 @@ def include_all_msd_switches(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " - msg += "include_all_msd_switches must be a boolean." + msg += f"Expected boolean for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." raise ValueError(msg) self.properties["include_all_msd_switches"] = value @@ -256,7 +258,8 @@ def ticket_id(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, str): msg = f"{self.class_name}.{method_name}: " - msg += "ticket_id must be a string." + msg += f"Expected string for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." raise ValueError(msg) self.properties["ticket_id"] = value diff --git a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py new file mode 100644 index 000000000..b63b59282 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py @@ -0,0 +1,417 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import ( + EpFabricConfigDeploy, EpFabricConfigSave, EpFabricDelete, EpFabricDetails, EpFabricFreezeMode) +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + does_not_raise + +PATH_PREFIX = "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics" +FABRIC_NAME = "MyFabric" + +def test_ep_fabrics_00010(): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify path and verb + - Verify default value for ``force_show_run`` + - Verify default value for ``include_all_msd_switches`` + """ + with does_not_raise(): + instance = EpFabricConfigDeploy() + instance.fabric_name = FABRIC_NAME + assert f"{PATH_PREFIX}/{FABRIC_NAME}/config-deploy" in instance.path + assert "forceShowRun=False" in instance.path + assert "inclAllMSDSwitches=False" in instance.path + assert instance.verb == "POST" + + +def test_ep_fabrics_00020(): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify setting ``force_show_run`` results in change to path. + """ + with does_not_raise(): + instance = EpFabricConfigDeploy() + instance.fabric_name = FABRIC_NAME + instance.force_show_run = True + assert f"{PATH_PREFIX}/{FABRIC_NAME}/config-deploy" in instance.path + assert "forceShowRun=True" in instance.path + assert "inclAllMSDSwitches=False" in instance.path + assert instance.verb == "POST" + + +def test_ep_fabrics_00030(): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify setting ``include_all_msd_switches`` results in change to path. + """ + with does_not_raise(): + instance = EpFabricConfigDeploy() + instance.fabric_name = FABRIC_NAME + instance.include_all_msd_switches = True + assert f"{PATH_PREFIX}/{FABRIC_NAME}/config-deploy" in instance.path + assert "forceShowRun=False" in instance.path + assert "inclAllMSDSwitches=True" in instance.path + assert instance.verb == "POST" + + +def test_ep_fabrics_00040(): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``fabric_name``. + + """ + with does_not_raise(): + instance = EpFabricConfigDeploy() + match = r"EpFabricConfigDeploy.path_fabric_name:\s+" + match += r"fabric_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_00050(): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify ``ValueError`` is raised if ``fabric_name`` + is invalid. + """ + fabric_name = "1_InvalidFabricName" + with does_not_raise(): + instance = EpFabricConfigDeploy() + match = r"EpFabricConfigDeploy.fabric_name:\s+" + match += r"ConversionUtils\.validate_fabric_name:\s+" + match += rf"Invalid fabric name: {fabric_name}\." + with pytest.raises(ValueError, match=match): + instance.fabric_name = fabric_name # pylint: disable=pointless-statement + + +def test_ep_fabrics_00060(): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify ``ValueError`` is raised if ``force_show_run`` + is not a boolean. + """ + with does_not_raise(): + instance = EpFabricConfigDeploy() + match = r"EpFabricConfigDeploy.force_show_run:\s+" + match += r"Expected boolean for force_show_run\.\s+" + match += r"Got NOT_BOOLEAN with type str\." + with pytest.raises(ValueError, match=match): + instance.force_show_run = "NOT_BOOLEAN" # pylint: disable=pointless-statement + + +def test_ep_fabrics_00070(): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify ``ValueError`` is raised if ``include_all_msd_switches`` + is not a boolean. + """ + with does_not_raise(): + instance = EpFabricConfigDeploy() + match = r"EpFabricConfigDeploy.include_all_msd_switches:\s+" + match += r"Expected boolean for include_all_msd_switches\.\s+" + match += r"Got NOT_BOOLEAN with type str\." + with pytest.raises(ValueError, match=match): + instance.include_all_msd_switches = "NOT_BOOLEAN" # pylint: disable=pointless-statement + + +def test_ep_fabrics_00100(): + """ + ### Class + - EpFabricConfigSave + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpFabricConfigSave() + instance.fabric_name = FABRIC_NAME + assert instance.path == f"{PATH_PREFIX}/{FABRIC_NAME}/config-save" + assert instance.verb == "POST" + + +def test_ep_fabrics_00110(): + """ + ### Class + - EpFabricConfigSave + + ### Summary + - Verify ticket_id is added to path when set. + """ + with does_not_raise(): + instance = EpFabricConfigSave() + instance.fabric_name = FABRIC_NAME + instance.ticket_id = "MyTicket1234" + ticket_id_path = f"{PATH_PREFIX}/{FABRIC_NAME}/config-save" + ticket_id_path += "?ticketId=MyTicket1234" + assert instance.path == ticket_id_path + assert instance.verb == "POST" + + +def test_ep_fabrics_00120(): + """ + ### Class + - EpFabricConfigSave + + ### Summary + - Verify ticket_id is added to path when set. + """ + with does_not_raise(): + instance = EpFabricConfigSave() + instance.fabric_name = FABRIC_NAME + instance.ticket_id = "MyTicket1234" + ticket_id_path = f"{PATH_PREFIX}/{FABRIC_NAME}/config-save" + ticket_id_path += "?ticketId=MyTicket1234" + assert instance.path == ticket_id_path + assert instance.verb == "POST" + + +def test_ep_fabrics_00130(): + """ + ### Class + - EpFabricConfigSave + + ### Summary + - Verify ``ValueError`` is raised if ``ticket_id`` + is not a string. + """ + with does_not_raise(): + instance = EpFabricConfigSave() + instance.fabric_name = FABRIC_NAME + match = r"EpFabricConfigSave.ticket_id:\s+" + match += r"Expected string for ticket_id\.\s+" + match += r"Got 10 with type int\." + with pytest.raises(ValueError, match=match): + instance.ticket_id = 10 # pylint: disable=pointless-statement + + +def test_ep_fabrics_00140(): + """ + ### Class + - EpFabricConfigSave + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``fabric_name``. + + """ + with does_not_raise(): + instance = EpFabricConfigSave() + match = r"EpFabricConfigSave.path_fabric_name:\s+" + match += r"fabric_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_00150(): + """ + ### Class + - EpFabricConfigSave + + ### Summary + - Verify ``ValueError`` is raised if ``fabric_name`` + is invalid. + """ + fabric_name = "1_InvalidFabricName" + with does_not_raise(): + instance = EpFabricConfigSave() + match = r"EpFabricConfigSave.fabric_name:\s+" + match += r"ConversionUtils\.validate_fabric_name:\s+" + match += rf"Invalid fabric name: {fabric_name}\." + with pytest.raises(ValueError, match=match): + instance.fabric_name = fabric_name # pylint: disable=pointless-statement + + +def test_ep_fabrics_00200(): + """ + ### Class + - EpFabricDelete + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpFabricDelete() + instance.fabric_name = FABRIC_NAME + assert instance.path == f"{PATH_PREFIX}/{FABRIC_NAME}" + assert instance.verb == "DELETE" + + +def test_ep_fabrics_00240(): + """ + ### Class + - EpFabricDelete + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``fabric_name``. + + """ + with does_not_raise(): + instance = EpFabricDelete() + match = r"EpFabricDelete.path_fabric_name:\s+" + match += r"fabric_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_00250(): + """ + ### Class + - EpFabricDelete + + ### Summary + - Verify ``ValueError`` is raised if ``fabric_name`` + is invalid. + """ + fabric_name = "1_InvalidFabricName" + with does_not_raise(): + instance = EpFabricDelete() + match = r"EpFabricDelete.fabric_name:\s+" + match += r"ConversionUtils\.validate_fabric_name:\s+" + match += rf"Invalid fabric name: {fabric_name}\." + with pytest.raises(ValueError, match=match): + instance.fabric_name = fabric_name # pylint: disable=pointless-statement + + +def test_ep_fabrics_00300(): + """ + ### Class + - EpFabricDetails + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpFabricDetails() + instance.fabric_name = FABRIC_NAME + assert instance.path == f"{PATH_PREFIX}/{FABRIC_NAME}" + assert instance.verb == "GET" + + +def test_ep_fabrics_00340(): + """ + ### Class + - EpFabricDetails + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``fabric_name``. + + """ + with does_not_raise(): + instance = EpFabricDetails() + match = r"EpFabricDetails.path_fabric_name:\s+" + match += r"fabric_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_00350(): + """ + ### Class + - EpFabricDetails + + ### Summary + - Verify ``ValueError`` is raised if ``fabric_name`` + is invalid. + """ + fabric_name = "1_InvalidFabricName" + with does_not_raise(): + instance = EpFabricDetails() + match = r"EpFabricDetails.fabric_name:\s+" + match += r"ConversionUtils\.validate_fabric_name:\s+" + match += rf"Invalid fabric name: {fabric_name}\." + with pytest.raises(ValueError, match=match): + instance.fabric_name = fabric_name # pylint: disable=pointless-statement + + +def test_ep_fabrics_00400(): + """ + ### Class + - EpFabricFreezeMode + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpFabricFreezeMode() + instance.fabric_name = FABRIC_NAME + assert instance.path == f"{PATH_PREFIX}/{FABRIC_NAME}/freezemode" + assert instance.verb == "GET" + + +def test_ep_fabrics_00440(): + """ + ### Class + - EpFabricFreezeMode + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``fabric_name``. + + """ + with does_not_raise(): + instance = EpFabricFreezeMode() + match = r"EpFabricFreezeMode.path_fabric_name:\s+" + match += r"fabric_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_00450(): + """ + ### Class + - EpFabricFreezeMode + + ### Summary + - Verify ``ValueError`` is raised if ``fabric_name`` + is invalid. + """ + fabric_name = "1_InvalidFabricName" + with does_not_raise(): + instance = EpFabricFreezeMode() + match = r"EpFabricFreezeMode.fabric_name:\s+" + match += r"ConversionUtils\.validate_fabric_name:\s+" + match += rf"Invalid fabric name: {fabric_name}\." + with pytest.raises(ValueError, match=match): + instance.fabric_name = fabric_name # pylint: disable=pointless-statement From f8b61ec389e1d7e88d34d084e4da7b031271db64 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 May 2024 11:22:37 -1000 Subject: [PATCH 033/230] dcnm_endpoints: docstring consistency across classes --- plugins/module_utils/common/api/v1/fm.py | 32 ++++++- .../common/api/v1/rest/control/fabrics.py | 37 +++++--- .../common/api/v1/rest/image_mgnt.py | 14 +++ .../common/api/v1/rest/image_upgrade.py | 24 +++-- .../common/api/v1/rest/policy_mgnt.py | 89 ++++++++++++++++++- .../common/api/v1/rest/staging_management.py | 48 +++++++++- 6 files changed, 221 insertions(+), 23 deletions(-) diff --git a/plugins/module_utils/common/api/v1/fm.py b/plugins/module_utils/common/api/v1/fm.py index 5ecd5ba7d..26d9da9a9 100644 --- a/plugins/module_utils/common/api/v1/fm.py +++ b/plugins/module_utils/common/api/v1/fm.py @@ -45,13 +45,27 @@ class EpFeatures(FM): ## V1 API Feature Manager (FM) - FM().EpFeatures() ### Description - Common methods and properties + Return endpoint information. + + ### Raises + - None ### Path ``/fm/features`` ### Verb - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFeatures() + path = instance.path + verb = instance.verb + ``` """ def __init__(self): @@ -71,13 +85,27 @@ class EpVersion(FM): ## V1 API Feature Manager (FM) about/version. ### Description - Common methods and properties + Return endpoint information. + + ### Raises + - None ### Path ``/fm/about/version`` ### Verb - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpVersion() + path = instance.path + verb = instance.verb + ``` """ def __init__(self): diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py index d05c10432..cd28fc476 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -106,13 +106,18 @@ class EpFabricConfigDeploy(Fabrics): ### Verb - POST - ### Parameters: + ### Parameters - force_show_run: boolean + - set the ``forceShowRun`` value - default: False - include_all_msd_switches: boolean + - set the ``inclAllMSDSwitches`` value - default: False - fabric_name: string + - set the ``fabric_name`` to be used in the path - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint ### Usage ```python @@ -212,11 +217,14 @@ class EpFabricConfigSave(Fabrics): ### Verb - POST - ### Parameters: - - fabric_name: string - - required + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required - ticket_id: string - optional unless Change Control is enabled + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint ### Usage ```python @@ -295,8 +303,11 @@ class EpFabricDelete(Fabrics): - DELETE ### Parameters - - fabric_name: string - - required + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint ### Usage ```python @@ -347,8 +358,11 @@ class EpFabricDetails(Fabrics): - GET ### Parameters - - fabric_name: string - - required + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint ### Usage ```python @@ -395,8 +409,11 @@ class EpFabricFreezeMode(Fabrics): - GET ### Parameters - - fabric_name: string - - required + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint ### Usage ```python diff --git a/plugins/module_utils/common/api/v1/rest/image_mgnt.py b/plugins/module_utils/common/api/v1/rest/image_mgnt.py index fc7b2057e..e4e2706d8 100644 --- a/plugins/module_utils/common/api/v1/rest/image_mgnt.py +++ b/plugins/module_utils/common/api/v1/rest/image_mgnt.py @@ -49,11 +49,25 @@ class EpBootFlashInfo(ImageMgnt): ### Description Return endpoint information for bootflash-info. + ### Raises + - None + ### Path - ``/rest/imagemgnt/bootFlash/bootflash-info`` ### Verb - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpBootFlashInfo() + path = instance.path + verb = instance.verb + ``` """ def __init__(self): diff --git a/plugins/module_utils/common/api/v1/rest/image_upgrade.py b/plugins/module_utils/common/api/v1/rest/image_upgrade.py index f9d9d5de2..94b4c0e55 100644 --- a/plugins/module_utils/common/api/v1/rest/image_upgrade.py +++ b/plugins/module_utils/common/api/v1/rest/image_upgrade.py @@ -54,14 +54,20 @@ class EpInstallOptions(ImageUpgrade): ## V1 API - Fabrics().EpInstallOptions() ### Description - Return endpoint information for install-options. + Return endpoint information. + + ### Raises + - None ### Path - ``/rest/imageupgrade/install-options`` - ### Raises + ### Verb + - POST - ### Parameters: + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint ### Usage ```python @@ -90,14 +96,20 @@ class EpUpgradeImage(ImageUpgrade): ## V1 API - Fabrics().EpUpgradeImage() ### Description - Return endpoint information for upgrade-image. + Return endpoint information. + + ### Raises + - None ### Path - ``/rest/imageupgrade/upgrade-image`` - ### Raises + ### Verb + - POST - ### Parameters: + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint ### Usage ```python diff --git a/plugins/module_utils/common/api/v1/rest/policy_mgnt.py b/plugins/module_utils/common/api/v1/rest/policy_mgnt.py index 061b56d6b..39aa6e13c 100644 --- a/plugins/module_utils/common/api/v1/rest/policy_mgnt.py +++ b/plugins/module_utils/common/api/v1/rest/policy_mgnt.py @@ -50,11 +50,25 @@ class EpPolicies(PolicyMgnt): ### Description Return endpoint information. - ### Path path + ### Raises + - None + + ### Path - ``/rest/policymgnt/policies`` ### Verb - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPolicies() + path = instance.path + verb = instance.verb + ``` """ def __init__(self): @@ -76,11 +90,25 @@ class EpPoliciesAllAttached(PolicyMgnt): ### Description Return endpoint information. + ### Raises + - None + ### Path - ``/rest/policymgnt/all-attached-policies`` ### Verb - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPoliciesAllAttached() + path = instance.path + verb = instance.verb + ``` """ def __init__(self): @@ -102,11 +130,25 @@ class EpPolicyAttach(PolicyMgnt): ### Description Return endpoint information. + ### Raises + - None + ### Path - ``/rest/policymgnt/attach-policy`` ### Verb - POST + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPolicyAttach() + path = instance.path + verb = instance.verb + ``` """ def __init__(self): @@ -128,11 +170,25 @@ class EpPolicyCreate(PolicyMgnt): ### Description Return endpoint information. - ### Path path + ### Raises + - None + + ### Path - ``/rest/policymgnt/platform-policy`` ### Verb - POST + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPolicyCreate() + path = instance.path + verb = instance.verb + ``` """ def __init__(self): @@ -154,11 +210,25 @@ class EpPolicyDetach(PolicyMgnt): ### Description Return endpoint information. + ### Raises + - None + ### Path - ``/rest/policymgnt/detach-policy`` ### Verb - DELETE + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPolicyDetach() + path = instance.path + verb = instance.verb + ``` """ def __init__(self): @@ -188,6 +258,21 @@ class EpPolicyInfo(PolicyMgnt): ### Verb - GET + + ### Parameters + - policy_name: str + - set the policy_name + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPolicyInfo() + instance.policy_name = "MyPolicy" + path = instance.path + verb = instance.verb + ``` """ def __init__(self): diff --git a/plugins/module_utils/common/api/v1/rest/staging_management.py b/plugins/module_utils/common/api/v1/rest/staging_management.py index 3f0eecd24..faf2e2994 100644 --- a/plugins/module_utils/common/api/v1/rest/staging_management.py +++ b/plugins/module_utils/common/api/v1/rest/staging_management.py @@ -47,13 +47,27 @@ class EpImageStage(StagingManagement): ## V1 API - StagingManagement().EpImageStage() ### Description - Return endpoint information for stage-image. + Return endpoint information. + + ### Raises + - None ### Path - ``/rest/stagingmanagement/stage-image`` ### Verb - POST + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpImageStage() + path = instance.path + verb = instance.verb + ``` """ def __init__(self): @@ -73,13 +87,27 @@ class EpImageValidate(StagingManagement): ## V1 API - StagingManagement().EpImageValidate() ### Description - Return endpoint information for validate-image. + Return endpoint information. + + ### Raises + - None ### Path - ``/rest/stagingmanagement/validate-image`` ### Verb - POST + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpImageValidate() + path = instance.path + verb = instance.verb + ``` """ def __init__(self): @@ -99,13 +127,27 @@ class EpStageInfo(StagingManagement): ## V1 API - StagingManagement().EpStageInfo() ### Description - Return endpoint information for stage-info. + Return endpoint information. + + ### Raises + - None ### Path - ``/rest/stagingmanagement/stage-info`` ### Verb - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpStageInfo() + path = instance.path + verb = instance.verb + ``` """ def __init__(self): From f1c2825fdd23c99e14d1b6ad096048180b90989b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 May 2024 13:49:11 -1000 Subject: [PATCH 034/230] dcnm_endpoints: Add endpoints + UT, more... 1. Add the following endpoints: - Fabrics(). EpFabricCreate() - Fabrics(). EpFabricUpdate() - Switches().EpFabricSummary() 2. Add UT for the above. 3. FabricTypes().valid_fabric_template_names: New property --- .../common/api/v1/rest/control/fabrics.py | 168 ++++++++++++++ .../common/api/v1/rest/control/switches.py | 140 ++++++++++++ plugins/module_utils/fabric/fabric_types.py | 7 + .../common/api/test_v1_api_fabrics.py | 207 +++++++++++++++++- .../common/api/test_v1_api_switches.py | 78 +++++++ 5 files changed, 591 insertions(+), 9 deletions(-) create mode 100644 plugins/module_utils/common/api/v1/rest/control/switches.py create mode 100644 tests/unit/module_utils/common/api/test_v1_api_switches.py diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py index cd28fc476..bed4af9bf 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -22,6 +22,8 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric import \ LanFabric +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ + FabricTypes class Fabrics(LanFabric): @@ -39,6 +41,7 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fabric_types = FabricTypes() self.rest_control_fabrics = f"{self.lan_fabric}/rest/control/fabrics" msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" self.log.debug(msg) @@ -49,6 +52,7 @@ def _build_properties(self): - Set the fabric_name property. """ self.properties["fabric_name"] = None + self.properties["template_name"] = None @property def fabric_name(self): @@ -84,6 +88,46 @@ def path_fabric_name(self): raise ValueError(msg) return f"{self.rest_control_fabrics}/{self.fabric_name}" + @property + def path_fabric_name_template_name(self): + """ + - Endpoint path property, including fabric_name and template_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + - Raise ``ValueError`` if template_name is not set and + ``self.required_properties`` contains "template_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + if self.template_name is None and "template_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "template_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.rest_control_fabrics}/{self.fabric_name}/{self.template_name}" + + @property + def template_name(self): + """ + - getter: Return the template_name. + - setter: Set the template_name. + - setter: Raise ``ValueError`` if template_name is not a string. + """ + return self.properties["template_name"] + + @template_name.setter + def template_name(self, value): + method_name = inspect.stack()[0][3] + if value not in self.fabric_types.valid_fabric_template_names: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid template_name: {value}. " + msg += "Expected one of: " + msg += f"{', '.join(self.fabric_types.valid_fabric_template_names)}." + raise ValueError(msg) + self.properties["template_name"] = value + class EpFabricConfigDeploy(Fabrics): """ @@ -285,6 +329,68 @@ def path(self): return _path +class EpFabricCreate(Fabrics): + """ + ## V1 API - Fabrics().EpFabricCreate() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If template_name is not set. + - ``ValueError``: If template_name is not a valid fabric template name. + + ### Path + - ``/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` + + ### Verb + - POST + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - template_name: string + - set the ``template_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricCreate() + instance.fabric_name = "MyFabric" + instance.template_name = "Easy_Fabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self.required_properties.add("template_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + + @property + def path(self): + """ + - Endpoint for fabric create. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name_template_name + + class EpFabricDelete(Fabrics): """ ## V1 API - Fabrics().EpFabricDelete() @@ -440,3 +546,65 @@ def _build_properties(self): @property def path(self): return f"{self.path_fabric_name}/freezemode" + + +class EpFabricUpdate(Fabrics): + """ + ## V1 API - Fabrics().EpFabricUpdate() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If template_name is not set. + - ``ValueError``: If template_name is not a valid fabric template name. + + ### Path + - ``/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` + + ### Verb + - PUT + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - template_name: string + - set the ``template_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricUpdate() + instance.fabric_name = "MyFabric" + instance.template_name = "Easy_Fabric_IPFM" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self.required_properties.add("template_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "PUT" + + @property + def path(self): + """ + - Endpoint for fabric create. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name_template_name diff --git a/plugins/module_utils/common/api/v1/rest/control/switches.py b/plugins/module_utils/common/api/v1/rest/control/switches.py new file mode 100644 index 000000000..1d67d383d --- /dev/null +++ b/plugins/module_utils/common/api/v1/rest/control/switches.py @@ -0,0 +1,140 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric import \ + LanFabric + + +class Switches(LanFabric): + """ + ## V1 API Fabrics - LanFabric().Switches() + + ### Description + Common methods and properties for Fabrics() subclasses. + + ### Path + - ``/lan-fabric/rest/control/switches/{fabric_name}`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.rest_control_switches = f"{self.lan_fabric}/rest/control/switches" + msg = f"ENTERED api.v1.LanFabric.Switches.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Set the fabric_name property. + """ + self.properties["fabric_name"] = None + + @property + def fabric_name(self): + """ + - getter: Return the fabric_name. + - setter: Set the fabric_name. + - setter: Raise ``ValueError`` if fabric_name is not valid. + """ + return self.properties["fabric_name"] + + @fabric_name.setter + def fabric_name(self, value): + method_name = inspect.stack()[0][3] + try: + self.conversion.validate_fabric_name(value) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) from error + self.properties["fabric_name"] = value + + @property + def path_fabric_name(self): + """ + - Endpoint path property, including fabric_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.rest_control_switches}/{self.fabric_name}" + + +class EpFabricSummary(Switches): + """ + ## V1 API - Switches().EpFabricSummary() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/switches/{fabric_name}/overview`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricSummary() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Switches.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + """ + - Override the path property to mandate fabric_name is set. + - Raise ``ValueError`` if fabric_name is not set. + """ + return f"{self.path_fabric_name}/overview" diff --git a/plugins/module_utils/fabric/fabric_types.py b/plugins/module_utils/fabric/fabric_types.py index 275314ae6..3d7894a7c 100644 --- a/plugins/module_utils/fabric/fabric_types.py +++ b/plugins/module_utils/fabric/fabric_types.py @@ -188,3 +188,10 @@ def valid_fabric_types(self): Return a sorted list() of valid fabric types. """ return self._properties["valid_fabric_types"] + + @property + def valid_fabric_template_names(self): + """ + Return a sorted list() of valid fabric template names. + """ + return sorted(self._fabric_type_to_template_name_map.values()) \ No newline at end of file diff --git a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py index b63b59282..bc8a9d59b 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py +++ b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py @@ -19,12 +19,14 @@ import pytest from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import ( - EpFabricConfigDeploy, EpFabricConfigSave, EpFabricDelete, EpFabricDetails, EpFabricFreezeMode) + EpFabricConfigDeploy, EpFabricConfigSave, EpFabricCreate, EpFabricDelete, EpFabricDetails, + EpFabricFreezeMode, EpFabricUpdate) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise PATH_PREFIX = "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics" FABRIC_NAME = "MyFabric" +TEMPLATE_NAME = "Easy_Fabric" def test_ep_fabrics_00010(): """ @@ -262,6 +264,98 @@ def test_ep_fabrics_00150(): def test_ep_fabrics_00200(): + """ + ### Class + - EpFabricCreate + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpFabricCreate() + instance.fabric_name = FABRIC_NAME + instance.template_name = TEMPLATE_NAME + assert instance.path == f"{PATH_PREFIX}/{FABRIC_NAME}/{TEMPLATE_NAME}" + assert instance.verb == "POST" + + +def test_ep_fabrics_00240(): + """ + ### Class + - EpFabricCreate + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``fabric_name``. + + """ + with does_not_raise(): + instance = EpFabricCreate() + match = r"EpFabricCreate\.path_fabric_name_template_name:\s+" + match += r"fabric_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_00250(): + """ + ### Class + - EpFabricCreate + + ### Summary + - Verify ``ValueError`` is raised if ``fabric_name`` + is invalid. + """ + fabric_name = "1_InvalidFabricName" + with does_not_raise(): + instance = EpFabricCreate() + match = r"EpFabricCreate.fabric_name:\s+" + match += r"ConversionUtils\.validate_fabric_name:\s+" + match += rf"Invalid fabric name: {fabric_name}\." + with pytest.raises(ValueError, match=match): + instance.fabric_name = fabric_name # pylint: disable=pointless-statement + + +def test_ep_fabrics_00260(): + """ + ### Class + - EpFabricCreate + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``template_name``. + + """ + with does_not_raise(): + instance = EpFabricCreate() + instance.fabric_name = FABRIC_NAME + match = r"EpFabricCreate\.path_fabric_name_template_name:\s+" + match += r"template_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_00270(): + """ + ### Class + - EpFabricCreate + + ### Summary + - Verify ``ValueError`` is raised if ``template_name`` + is invalid. + """ + template_name = "Invalid_Template_Name" + with does_not_raise(): + instance = EpFabricCreate() + instance.fabric_name = FABRIC_NAME + match = r"EpFabricCreate.template_name:\s+" + match += r"Invalid template_name: Invalid_Template_Name\.\s+" + match += r"Expected one of:.*\." + with pytest.raises(ValueError, match=match): + instance.template_name = template_name # pylint: disable=pointless-statement + + +def test_ep_fabrics_00400(): """ ### Class - EpFabricDelete @@ -276,7 +370,7 @@ def test_ep_fabrics_00200(): assert instance.verb == "DELETE" -def test_ep_fabrics_00240(): +def test_ep_fabrics_00440(): """ ### Class - EpFabricDelete @@ -294,7 +388,7 @@ def test_ep_fabrics_00240(): instance.path # pylint: disable=pointless-statement -def test_ep_fabrics_00250(): +def test_ep_fabrics_00450(): """ ### Class - EpFabricDelete @@ -313,7 +407,7 @@ def test_ep_fabrics_00250(): instance.fabric_name = fabric_name # pylint: disable=pointless-statement -def test_ep_fabrics_00300(): +def test_ep_fabrics_00500(): """ ### Class - EpFabricDetails @@ -328,7 +422,7 @@ def test_ep_fabrics_00300(): assert instance.verb == "GET" -def test_ep_fabrics_00340(): +def test_ep_fabrics_00540(): """ ### Class - EpFabricDetails @@ -346,7 +440,7 @@ def test_ep_fabrics_00340(): instance.path # pylint: disable=pointless-statement -def test_ep_fabrics_00350(): +def test_ep_fabrics_00550(): """ ### Class - EpFabricDetails @@ -365,7 +459,7 @@ def test_ep_fabrics_00350(): instance.fabric_name = fabric_name # pylint: disable=pointless-statement -def test_ep_fabrics_00400(): +def test_ep_fabrics_00600(): """ ### Class - EpFabricFreezeMode @@ -380,7 +474,7 @@ def test_ep_fabrics_00400(): assert instance.verb == "GET" -def test_ep_fabrics_00440(): +def test_ep_fabrics_00640(): """ ### Class - EpFabricFreezeMode @@ -398,7 +492,7 @@ def test_ep_fabrics_00440(): instance.path # pylint: disable=pointless-statement -def test_ep_fabrics_00450(): +def test_ep_fabrics_00650(): """ ### Class - EpFabricFreezeMode @@ -415,3 +509,98 @@ def test_ep_fabrics_00450(): match += rf"Invalid fabric name: {fabric_name}\." with pytest.raises(ValueError, match=match): instance.fabric_name = fabric_name # pylint: disable=pointless-statement + + +# NOTE: EpFabricSummary tests are in test_v1_api_switches.py + + +def test_ep_fabrics_00700(): + """ + ### Class + - EpFabricUpdate + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpFabricUpdate() + instance.fabric_name = FABRIC_NAME + instance.template_name = TEMPLATE_NAME + assert instance.path == f"{PATH_PREFIX}/{FABRIC_NAME}/{TEMPLATE_NAME}" + assert instance.verb == "PUT" + + +def test_ep_fabrics_00740(): + """ + ### Class + - EpFabricUpdate + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``fabric_name``. + + """ + with does_not_raise(): + instance = EpFabricUpdate() + match = r"EpFabricUpdate\.path_fabric_name_template_name:\s+" + match += r"fabric_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_00750(): + """ + ### Class + - EpFabricUpdate + + ### Summary + - Verify ``ValueError`` is raised if ``fabric_name`` + is invalid. + """ + fabric_name = "1_InvalidFabricName" + with does_not_raise(): + instance = EpFabricUpdate() + match = r"EpFabricUpdate.fabric_name:\s+" + match += r"ConversionUtils\.validate_fabric_name:\s+" + match += rf"Invalid fabric name: {fabric_name}\." + with pytest.raises(ValueError, match=match): + instance.fabric_name = fabric_name # pylint: disable=pointless-statement + + +def test_ep_fabrics_00760(): + """ + ### Class + - EpFabricUpdate + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``template_name``. + + """ + with does_not_raise(): + instance = EpFabricUpdate() + instance.fabric_name = FABRIC_NAME + match = r"EpFabricUpdate\.path_fabric_name_template_name:\s+" + match += r"template_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_00770(): + """ + ### Class + - EpFabricUpdate + + ### Summary + - Verify ``ValueError`` is raised if ``template_name`` + is invalid. + """ + template_name = "Invalid_Template_Name" + with does_not_raise(): + instance = EpFabricUpdate() + instance.fabric_name = FABRIC_NAME + match = r"EpFabricUpdate.template_name:\s+" + match += r"Invalid template_name: Invalid_Template_Name\.\s+" + match += r"Expected one of:.*\." + with pytest.raises(ValueError, match=match): + instance.template_name = template_name # pylint: disable=pointless-statement diff --git a/tests/unit/module_utils/common/api/test_v1_api_switches.py b/tests/unit/module_utils/common/api/test_v1_api_switches.py new file mode 100644 index 000000000..347f5fb8e --- /dev/null +++ b/tests/unit/module_utils/common/api/test_v1_api_switches.py @@ -0,0 +1,78 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.switches import ( + EpFabricSummary) +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + does_not_raise + +PATH_PREFIX = "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/switches" +FABRIC_NAME = "MyFabric" + +def test_ep_switches_00010(): + """ + ### Class + - EpFabricSummary + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpFabricSummary() + instance.fabric_name = FABRIC_NAME + assert f"{PATH_PREFIX}/{FABRIC_NAME}/overview" in instance.path + assert instance.verb == "GET" + + +def test_ep_switches_00040(): + """ + ### Class + - EpFabricSummary + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``fabric_name``. + + """ + with does_not_raise(): + instance = EpFabricSummary() + match = r"EpFabricSummary.path_fabric_name:\s+" + match += r"fabric_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_switches_00050(): + """ + ### Class + - EpFabricSummary + + ### Summary + - Verify ``ValueError`` is raised if ``fabric_name`` + is invalid. + """ + fabric_name = "1_InvalidFabricName" + with does_not_raise(): + instance = EpFabricSummary() + match = r"EpFabricSummary.fabric_name:\s+" + match += r"ConversionUtils\.validate_fabric_name:\s+" + match += rf"Invalid fabric name: {fabric_name}\." + with pytest.raises(ValueError, match=match): + instance.fabric_name = fabric_name # pylint: disable=pointless-statement From b112bc037f2e56636758b27e731d7018e721d6a3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 May 2024 16:08:13 -1000 Subject: [PATCH 035/230] dcnm_endpoints: Add configtemplate endpoints + UT --- .../common/api/v1/config_template.py | 42 ++++ .../common/api/v1/rest/config/templates.py | 192 ++++++++++++++++++ .../common/api/test_v1_api_templates.py | 93 +++++++++ 3 files changed, 327 insertions(+) create mode 100644 plugins/module_utils/common/api/v1/config_template.py create mode 100644 plugins/module_utils/common/api/v1/rest/config/templates.py create mode 100644 tests/unit/module_utils/common/api/test_v1_api_templates.py diff --git a/plugins/module_utils/common/api/v1/config_template.py b/plugins/module_utils/common/api/v1/config_template.py new file mode 100644 index 000000000..657f573bd --- /dev/null +++ b/plugins/module_utils/common/api/v1/config_template.py @@ -0,0 +1,42 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common_v1 import \ + CommonV1 + + +class ConfigTemplate(CommonV1): + """ + ## V1 API - ConfigTemplate() + + ### Description + Common methods and properties for CommonV1().ConfigTemplate() subclasses + + ### Path + ``/appcenter/cisco/ndfc/api/v1/configtemplate`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.config_template = f"{self.api_v1}/configtemplate" + self.log.debug("ENTERED api.v1.ConfigTemplate()") diff --git a/plugins/module_utils/common/api/v1/rest/config/templates.py b/plugins/module_utils/common/api/v1/rest/config/templates.py new file mode 100644 index 000000000..c4cf5f04f --- /dev/null +++ b/plugins/module_utils/common/api/v1/rest/config/templates.py @@ -0,0 +1,192 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.config_template import \ + ConfigTemplate +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ + FabricTypes + + +class Templates(ConfigTemplate): + """ + ## V1 API Fabrics - ConfigTemplate().Templates() + + ### Description + Common methods and properties for Templates() subclasses. + + ### Path + - ``/configtemplate/rest/config/templates`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fabric_types = FabricTypes() + + self.rest_config_templates = f"{self.config_template}/rest/config/templates" + msg = f"ENTERED api.v1.ConfigTemplate.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Set the fabric_name property. + """ + self.properties["template_name"] = None + + @property + def path_template_name(self): + """ + - Endpoint for template retrieval. + - Raise ``ValueError`` if template_name is not set. + """ + method_name = inspect.stack()[0][3] + if self.template_name is None and "template_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "template_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.rest_config_templates}/{self.template_name}" + + @property + def template_name(self): + """ + - getter: Return the template_name. + - setter: Set the template_name. + - setter: Raise ``ValueError`` if template_name is not a string. + """ + return self.properties["template_name"] + + @template_name.setter + def template_name(self, value): + method_name = inspect.stack()[0][3] + if value not in self.fabric_types.valid_fabric_template_names: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid template_name: {value}. " + msg += "Expected one of: " + msg += f"{', '.join(self.fabric_types.valid_fabric_template_names)}." + raise ValueError(msg) + self.properties["template_name"] = value + + +class EpTemplate(Templates): + """ + ## V1 API - Templates().EpTemplate() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If template_name is not set. + - ``ValueError``: If template_name is not a valid fabric template name. + + ### Path + - ``/rest/config/templates/{template_name}`` + + ### Verb + - GET + + ### Parameters + - template_name: string + - set the ``template_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpTemplate() + instance.template_name = "Easy_Fabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("template_name") + self._build_properties() + msg = f"ENTERED api.v1.ConfigTemplate.Templates.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + """ + - Endpoint for template retrieval. + - Raise ``ValueError`` if template_name is not set. + """ + return self.path_template_name + + +class EpTemplates(Templates): + """ + ## V1 API - Templates().EpTemplates() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/rest/config/templates`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpTemplates() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + msg = f"ENTERED api.v1.ConfigTemplate.Templates.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + """ + - Endpoint for template retrieval. + - Raise ``ValueError`` if template_name is not set. + """ + return self.rest_config_templates diff --git a/tests/unit/module_utils/common/api/test_v1_api_templates.py b/tests/unit/module_utils/common/api/test_v1_api_templates.py new file mode 100644 index 000000000..42080e826 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_v1_api_templates.py @@ -0,0 +1,93 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.config.templates import ( + EpTemplate, EpTemplates) +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + does_not_raise + +PATH_PREFIX = "/appcenter/cisco/ndfc/api/v1/configtemplate/rest/config/templates" +TEMPLATE_NAME = "Easy_Fabric" + + +def test_ep_templates_00010(): + """ + ### Class + - EpTemplate + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpTemplate() + instance.template_name = TEMPLATE_NAME + assert f"{PATH_PREFIX}/{TEMPLATE_NAME}" in instance.path + assert instance.verb == "GET" + + +def test_ep_templates_00040(): + """ + ### Class + - EpTemplate + + ### Summary + - Verify ``ValueError`` is raised if path is accessed + before setting ``template_name``. + + """ + with does_not_raise(): + instance = EpTemplate() + match = r"EpTemplate.path_template_name:\s+" + match += r"template_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_templates_00050(): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify ``ValueError`` is raised if ``template_name`` + is invalid. + """ + template_name = "Invalid_Template_Name" + with does_not_raise(): + instance = EpTemplate() + match = r"EpTemplate.template_name:\s+" + match += r"Invalid template_name: Invalid_Template_Name.\s+" + match += r"Expected one of:\s+" + with pytest.raises(ValueError, match=match): + instance.template_name = template_name # pylint: disable=pointless-statement + + +def test_ep_templates_00100(): + """ + ### Class + - EpTemplates + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpTemplates() + assert instance.path == PATH_PREFIX + assert instance.verb == "GET" From 020efc777d320fc4e2875d06093c6bfcf9d0b5c7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 May 2024 17:03:55 -1000 Subject: [PATCH 036/230] Fix PEP8 issues, import error test_controller_features.py was trying to import the old name, Features, for renamed class EpFeatures. --- .../unit/module_utils/common/api/test_v1_api_fabrics.py | 9 ++++++--- .../unit/module_utils/common/api/test_v1_api_switches.py | 5 +++-- .../unit/module_utils/common/test_controller_features.py | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py index bc8a9d59b..d48f2ed6e 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py +++ b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py @@ -19,8 +19,8 @@ import pytest from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import ( - EpFabricConfigDeploy, EpFabricConfigSave, EpFabricCreate, EpFabricDelete, EpFabricDetails, - EpFabricFreezeMode, EpFabricUpdate) + EpFabricConfigDeploy, EpFabricConfigSave, EpFabricCreate, EpFabricDelete, + EpFabricDetails, EpFabricFreezeMode, EpFabricUpdate) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise @@ -28,6 +28,7 @@ FABRIC_NAME = "MyFabric" TEMPLATE_NAME = "Easy_Fabric" + def test_ep_fabrics_00010(): """ ### Class @@ -153,7 +154,9 @@ def test_ep_fabrics_00070(): match += r"Expected boolean for include_all_msd_switches\.\s+" match += r"Got NOT_BOOLEAN with type str\." with pytest.raises(ValueError, match=match): - instance.include_all_msd_switches = "NOT_BOOLEAN" # pylint: disable=pointless-statement + instance.include_all_msd_switches = ( + "NOT_BOOLEAN" # pylint: disable=pointless-statement + ) def test_ep_fabrics_00100(): diff --git a/tests/unit/module_utils/common/api/test_v1_api_switches.py b/tests/unit/module_utils/common/api/test_v1_api_switches.py index 347f5fb8e..3ec0e2c70 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_switches.py +++ b/tests/unit/module_utils/common/api/test_v1_api_switches.py @@ -18,14 +18,15 @@ import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.switches import ( - EpFabricSummary) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.switches import \ + EpFabricSummary from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise PATH_PREFIX = "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/switches" FABRIC_NAME = "MyFabric" + def test_ep_switches_00010(): """ ### Class diff --git a/tests/unit/module_utils/common/test_controller_features.py b/tests/unit/module_utils/common/test_controller_features.py index 830e45a68..3f171ea9b 100644 --- a/tests/unit/module_utils/common/test_controller_features.py +++ b/tests/unit/module_utils/common/test_controller_features.py @@ -33,7 +33,7 @@ import pytest from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm import ( - Features, + EpFeatures, ) from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import ( ConversionUtils, @@ -70,7 +70,7 @@ def test_controller_features_00010(controller_features) -> None: with does_not_raise(): instance = controller_features assert instance.class_name == "ControllerFeatures" - assert isinstance(instance.api_features, Features) + assert isinstance(instance.api_features, EpFeatures) assert isinstance(instance.conversion, ConversionUtils) assert instance.check_mode is False assert instance.filter is None From 74f892e2c1d54a31c83f602d301f4809e70ecca8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 14 May 2024 17:11:20 -1000 Subject: [PATCH 037/230] Fix PEP8 no line at end of file, and f-string issue --- plugins/module_utils/common/api/v1/rest/policy_mgnt.py | 2 +- plugins/module_utils/fabric/fabric_types.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/common/api/v1/rest/policy_mgnt.py b/plugins/module_utils/common/api/v1/rest/policy_mgnt.py index 39aa6e13c..58ce994e8 100644 --- a/plugins/module_utils/common/api/v1/rest/policy_mgnt.py +++ b/plugins/module_utils/common/api/v1/rest/policy_mgnt.py @@ -295,7 +295,7 @@ def path(self): msg += f"{self.class_name}.policy_name must be set before " msg += f"accessing {method_name}." raise ValueError(msg) - return f"{self.properties["path"]}/{self.policy_name}" + return f"{self.properties['path']}/{self.policy_name}" @property def policy_name(self): diff --git a/plugins/module_utils/fabric/fabric_types.py b/plugins/module_utils/fabric/fabric_types.py index 3d7894a7c..3a4abf43b 100644 --- a/plugins/module_utils/fabric/fabric_types.py +++ b/plugins/module_utils/fabric/fabric_types.py @@ -194,4 +194,4 @@ def valid_fabric_template_names(self): """ Return a sorted list() of valid fabric template names. """ - return sorted(self._fabric_type_to_template_name_map.values()) \ No newline at end of file + return sorted(self._fabric_type_to_template_name_map.values()) From 7ea3a0e92d1451bcb0d207fd52957bd737a2d232 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 15 May 2024 06:49:21 -1000 Subject: [PATCH 038/230] Fabrics().EpFabrics() new endpoint Also modify all usage examples to use "instance" for the instantiated class name. --- .../common/api/v1/rest/control/fabrics.py | 91 ++++++++++++++----- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py index bed4af9bf..f2c825a83 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -165,12 +165,12 @@ class EpFabricConfigDeploy(Fabrics): ### Usage ```python - fabric_config_deploy = EpFabricConfigDeploy() - fabric_config_deploy.fabric_name = "MyFabric" - fabric_config_deploy.force_show_run = True - fabric_config_deploy.include_all_msd_switches = True - path = fabric_config_deploy.path - verb = fabric_config_deploy.verb + instance = EpFabricConfigDeploy() + instance.fabric_name = "MyFabric" + instance.force_show_run = True + instance.include_all_msd_switches = True + path = instance.path + verb = instance.verb ``` """ @@ -272,11 +272,11 @@ class EpFabricConfigSave(Fabrics): ### Usage ```python - fabric_config_save = EpFabricConfigSave() - fabric_config_save.fabric_name = "MyFabric" - fabric_config_save.ticket_id = "MyTicket1234" - path = fabric_config_save.path - verb = fabric_config_save.verb + instance = EpFabricConfigSave() + instance.fabric_name = "MyFabric" + instance.ticket_id = "MyTicket1234" + path = instance.path + verb = instance.verb ``` """ @@ -417,10 +417,10 @@ class EpFabricDelete(Fabrics): ### Usage ```python - fabric_delete = EpFabricDelete() - fabric_delete.fabric_name = "MyFabric" - path = fabric_delete.path - verb = fabric_delete.verb + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb ``` """ @@ -472,10 +472,10 @@ class EpFabricDetails(Fabrics): ### Usage ```python - fabric_details = EpFabricDelete() - fabric_details.fabric_name = "MyFabric" - path = fabric_details.path - verb = fabric_details.verb + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb ``` """ @@ -523,10 +523,10 @@ class EpFabricFreezeMode(Fabrics): ### Usage ```python - fabric_details = EpFabricDelete() - fabric_details.fabric_name = "MyFabric" - path = fabric_details.path - verb = fabric_details.verb + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb ``` """ @@ -608,3 +608,48 @@ def path(self): - Raise ``ValueError`` if fabric_name is not set. """ return self.path_fabric_name_template_name + + +class EpFabrics(Fabrics): + """ + ## V1 API - Fabrics().EpFabrics() + + ### Description + Return the endpoint to query fabrics. + + ### Raises + - None + + ### Path + - ``/rest/control/fabrics`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabrics() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return self.rest_control_fabrics From b7a55216a607c15c59af2904b92f732b2ff131e7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 15 May 2024 07:34:52 -1000 Subject: [PATCH 039/230] FabricDetails(): Leverage EpFabrics() endpoint class 1. import EpFabrics, remove import for ApiEndpoints 2. FabricDetails().__init__(): replace instantiation of self.endpoints with self.ep_fabrics 3. FabricDetails().refresh_super() use EpFabrics() class for endpoint info. 4. Update associated unit tests. --- plugins/module_utils/fabric/fabric_details.py | 13 +++++-------- .../modules/dcnm/dcnm_fabric/test_fabric_details.py | 6 +++--- .../dcnm/dcnm_fabric/test_fabric_details_by_name.py | 8 ++++---- .../dcnm_fabric/test_fabric_details_by_nv_pair.py | 6 +++--- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/plugins/module_utils/fabric/fabric_details.py b/plugins/module_utils/fabric/fabric_details.py index 3590c2d80..e9ead048e 100644 --- a/plugins/module_utils/fabric/fabric_details.py +++ b/plugins/module_utils/fabric/fabric_details.py @@ -28,9 +28,8 @@ Results from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.common import \ FabricCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints - +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabrics class FabricDetails(FabricCommon): """ @@ -52,9 +51,9 @@ def __init__(self, params): self.log.debug(msg) self.data = {} - self.endpoints = ApiEndpoints() self.results = Results() self.conversion = ConversionUtils() + self.ep_fabrics = EpFabrics() def _update_results(self): """ @@ -82,10 +81,8 @@ def refresh_super(self): """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - endpoint = self.endpoints.fabrics - - self.rest_send.path = endpoint.get("path") - self.rest_send.verb = endpoint.get("verb") + self.rest_send.path = self.ep_fabrics.path + self.rest_send.verb = self.ep_fabrics.verb # We always want to get the controller's current fabric state, # regardless of the current value of check_mode. diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py index 59c1e9974..263a66f73 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py @@ -32,14 +32,14 @@ import inspect import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( MockAnsibleModule, ResponseGenerator, does_not_raise, fabric_details_fixture, responses_fabric_details) @@ -61,7 +61,7 @@ def test_fabric_details_00010(fabric_details) -> None: instance = fabric_details assert instance.class_name == "FabricDetails" assert instance.data == {} - assert isinstance(instance.endpoints, ApiEndpoints) + assert isinstance(instance.ep_fabrics, EpFabrics) assert isinstance(instance.results, Results) assert isinstance(instance.conversion, ConversionUtils) diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py index cc2cada11..33e37a07f 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py @@ -32,14 +32,14 @@ import inspect import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( MockAnsibleModule, ResponseGenerator, does_not_raise, fabric_details_by_name_fixture, responses_fabric_details_by_name) @@ -65,7 +65,7 @@ def test_fabric_details_by_name_00010(fabric_details_by_name) -> None: assert instance.data == {} assert instance.data_subclass == {} assert instance._properties["filter"] is None - assert isinstance(instance.endpoints, ApiEndpoints) + assert isinstance(instance.ep_fabrics, EpFabrics) assert isinstance(instance.results, Results) assert isinstance(instance.conversion, ConversionUtils) @@ -549,7 +549,7 @@ def test_fabric_details_by_name_00060(fabric_details_by_name) -> None: match += r"FabricDetailsByName\.filter must be set before calling " match += r"FabricDetailsByName\.filtered_data" with pytest.raises(ValueError, match=match): - instance.filtered_data + instance.filtered_data # pylint: disable=pointless-statement def test_fabric_details_by_name_00061(fabric_details_by_name) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py index def62fdaa..6a3c6ea28 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py @@ -32,14 +32,14 @@ import inspect import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( MockAnsibleModule, ResponseGenerator, does_not_raise, fabric_details_by_nv_pair_fixture, responses_fabric_details_by_nv_pair) @@ -66,7 +66,7 @@ def test_fabric_details_by_nv_pair_00010(fabric_details_by_nv_pair) -> None: assert instance.data_subclass == {} assert instance._properties["filter_key"] is None assert instance._properties["filter_value"] is None - assert isinstance(instance.endpoints, ApiEndpoints) + assert isinstance(instance.ep_fabrics, EpFabrics) assert isinstance(instance.results, Results) assert isinstance(instance.conversion, ConversionUtils) From 5d9de5baa35252591443afba2acb155b653727e1 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 15 May 2024 07:39:30 -1000 Subject: [PATCH 040/230] FabricDetails(): run through black, isort, pylint --- plugins/module_utils/fabric/fabric_details.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/fabric/fabric_details.py b/plugins/module_utils/fabric/fabric_details.py index e9ead048e..6b47aae6f 100644 --- a/plugins/module_utils/fabric/fabric_details.py +++ b/plugins/module_utils/fabric/fabric_details.py @@ -22,14 +22,15 @@ import inspect import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.common import \ FabricCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ - EpFabrics + class FabricDetails(FabricCommon): """ From 7265095673aa3a6c65de54d0794c8796671d3e06 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 15 May 2024 08:49:18 -1000 Subject: [PATCH 041/230] FabricConfigDeploy(): Use EpFabricConfigDeploy() endpoint class 1. import EpFabricConfigDeploy, remove import for ApiEndpoints 2. FabricConfigDeploy().__init__(): replace instantiation of self.endpoints with self.ep_config_deploy 3. FabricConfigDeploy().commit() use EpFabricConfigDeploy() class for endpoint info. 4. Update associated unit tests. --- plugins/module_utils/fabric/config_deploy.py | 12 +++--- .../dcnm_fabric/test_fabric_config_deploy.py | 43 ++++++------------- 2 files changed, 18 insertions(+), 37 deletions(-) diff --git a/plugins/module_utils/fabric/config_deploy.py b/plugins/module_utils/fabric/config_deploy.py index c89299c92..4e0958e5e 100644 --- a/plugins/module_utils/fabric/config_deploy.py +++ b/plugins/module_utils/fabric/config_deploy.py @@ -22,6 +22,8 @@ import logging from typing import Dict +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricConfigDeploy from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ @@ -32,8 +34,6 @@ # Used only to verify RestSend instance in rest_send property setter from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints class FabricConfigDeploy: @@ -91,7 +91,7 @@ def __init__(self, params): self._init_properties() self.conversion = ConversionUtils() - self.endpoints = ApiEndpoints() + self.ep_config_deploy = EpFabricConfigDeploy() msg = "ENTERED FabricConfigDeploy(): " msg += f"check_mode: {self.check_mode}, " @@ -254,9 +254,9 @@ def commit(self): return try: - self.endpoints.fabric_name = self.fabric_name - self.path = self.endpoints.fabric_config_deploy.get("path") - self.verb = self.endpoints.fabric_config_deploy.get("verb") + self.ep_config_deploy.fabric_name = self.fabric_name + self.path = self.ep_config_deploy.path + self.verb = self.ep_config_deploy.verb except ValueError as error: raise ValueError(error) from error diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py index 66f0a3823..a6a3bbdbd 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py @@ -32,6 +32,8 @@ import inspect import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricConfigDeploy from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ @@ -76,7 +78,7 @@ def test_fabric_config_deploy_00010(fabric_config_deploy) -> None: assert instance.verb is None assert instance.state == "merged" assert isinstance(instance.conversion, ConversionUtils) - assert isinstance(instance.endpoints, ApiEndpoints) + assert isinstance(instance.ep_config_deploy, EpFabricConfigDeploy) def test_fabric_config_deploy_00011() -> None: @@ -420,49 +422,29 @@ def test_fabric_config_deploy_00200( Summary - Verify that FabricConfigDeploy().commit() - re-raises ``ValueError`` when ApiEndpoints() raises + re-raises ``ValueError`` when EpFabricConfigDeploy() raises ``ValueError``. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" - class MockApiEndpoints: # pylint: disable=too-few-public-methods + class EpFabricConfigDeploy: # pylint: disable=too-few-public-methods """ - Mock the ApiEndpoints.fabric_config_deploy getter property + Mock the EpFabricConfigDeploy.path getter property to raise ``ValueError``. """ - def validate_fabric_name(self, value="MyFabric"): - """ - Mocked method required for test, but not relevant to test result. - """ - @property - def fabric_config_deploy(self): + def path(self): """ - Mocked property getter. - Raise ``ValueError``. """ - msg = "mocked ApiEndpoints().fabric_config_deploy getter exception" + msg = "mocked EpFabricConfigDeploy().path getter exception" raise ValueError(msg) - @property - def fabric_name(self): - """ - - Mocked fabric_config_deploy property getter - """ - return self._fabric_name - - @fabric_name.setter - def fabric_name(self, value): - """ - - Mocked fabric_name property setter - """ - self._fabric_name = value - PATCH_API_ENDPOINTS = "ansible_collections.cisco.dcnm.plugins." - PATCH_API_ENDPOINTS += "module_utils.fabric.endpoints.ApiEndpoints." - PATCH_API_ENDPOINTS += "fabric_config_deploy" + PATCH_API_ENDPOINTS += "module_utils.common.api.v1.rest.control.fabrics" PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" @@ -470,7 +452,6 @@ def fabric_name(self, value): def responses(): yield responses_fabric_summary(key) yield responses_fabric_details_by_name(key) - # yield responses_fabric_config_deploy(key) gen = ResponseGenerator(responses()) @@ -478,8 +459,6 @@ def mock_dcnm_send(*args, **kwargs): item = gen.next return item - match = r"mocked ApiEndpoints\(\)\.fabric_config_deploy getter exception" - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) payload = { @@ -491,7 +470,7 @@ def mock_dcnm_send(*args, **kwargs): with does_not_raise(): instance = fabric_config_deploy - monkeypatch.setattr(instance, "endpoints", MockApiEndpoints()) + monkeypatch.setattr(instance, "ep_config_deploy", EpFabricConfigDeploy()) instance.fabric_details = fabric_details_by_name instance.fabric_details.rest_send = RestSend(MockAnsibleModule()) instance.payload = payload @@ -499,6 +478,8 @@ def mock_dcnm_send(*args, **kwargs): instance.fabric_summary.rest_send = RestSend(MockAnsibleModule()) instance.rest_send = RestSend(MockAnsibleModule()) instance.results = Results() + + match = r"mocked EpFabricConfigDeploy\(\)\.path getter exception" with pytest.raises(ValueError, match=match): instance.commit() From 9cb0e3ecc81cbb63c5f6ef365cb1a16bc57e2fef Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 15 May 2024 09:34:39 -1000 Subject: [PATCH 042/230] FabricConfigSave(): Use EpFabricConfigSave() endpoint class 1. import EpFabricConfigSave, remove import for ApiEndpoints 2. FabricConfigSave().__init__(): replace instantiation of self.endpoints with self.ep_config_save 3. FabricConfigSave().commit() use EpFabricConfigSave() class for endpoint info. 4. Update associated unit tests. 5. test_fabric_config_deploy.py: remove unused imports and update docstrings --- plugins/module_utils/fabric/config_save.py | 12 ++--- .../dcnm_fabric/test_fabric_config_deploy.py | 17 ++---- .../dcnm_fabric/test_fabric_config_save.py | 53 ++++++------------- 3 files changed, 25 insertions(+), 57 deletions(-) diff --git a/plugins/module_utils/fabric/config_save.py b/plugins/module_utils/fabric/config_save.py index eb45b563c..65166fc53 100644 --- a/plugins/module_utils/fabric/config_save.py +++ b/plugins/module_utils/fabric/config_save.py @@ -22,6 +22,8 @@ import logging from typing import Dict +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricConfigSave from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils # Used only to verify RestSend instance in rest_send property setter @@ -30,8 +32,6 @@ # Used only to verify RestSend instance in rest_send property setter from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints class FabricConfigSave: @@ -87,7 +87,7 @@ def __init__(self, params): self._init_properties() self.conversion = ConversionUtils() - self.endpoints = ApiEndpoints() + self.ep_config_save = EpFabricConfigSave() msg = "ENTERED FabricConfigSave(): " msg += f"check_mode: {self.check_mode}, " @@ -162,9 +162,9 @@ def commit(self): return try: - self.endpoints.fabric_name = self.fabric_name - self.path = self.endpoints.fabric_config_save.get("path") - self.verb = self.endpoints.fabric_config_save.get("verb") + self.ep_config_save.fabric_name = self.fabric_name + self.path = self.ep_config_save.path + self.verb = self.ep_config_save.verb except ValueError as error: raise ValueError(error) from error diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py index a6a3bbdbd..fc036cc70 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py @@ -42,12 +42,6 @@ Results from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.config_deploy import \ FabricConfigDeploy -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ - FabricDetailsByName -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_summary import \ - FabricSummary from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( MockAnsibleModule, ResponseGenerator, does_not_raise, fabric_config_deploy_fixture, fabric_details_by_name_fixture, @@ -443,9 +437,6 @@ def path(self): msg = "mocked EpFabricConfigDeploy().path getter exception" raise ValueError(msg) - PATCH_API_ENDPOINTS = "ansible_collections.cisco.dcnm.plugins." - PATCH_API_ENDPOINTS += "module_utils.common.api.v1.rest.control.fabrics" - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" @@ -512,9 +503,9 @@ def test_fabric_config_deploy_00210( - FabricConfigDeploy() properties are set - FabricConfigDeploy.fabric_name is set "f1" - FabricConfigDeploy().commit() is called. - - FabricConfigDeploy().commit() sets ApiEndpoints().fabric_name + - FabricConfigDeploy().commit() sets EpFabricConfigDeploy().fabric_name - FabricConfigDeploy().commit() accesses - ApiEndpoints().fabric_config_deploy to set verb and path + EpFabricConfigDeploy().path/verb to set path and verb - FabricConfigDeploy().commit() calls FabricConfigDeploy()_can_fabric_be_deployed() - FabricConfigDeploy()._can_fabric_be_deployed() calls @@ -635,9 +626,9 @@ def test_fabric_config_deploy_00220( - unit_test == True - FabricConfigDeploy().results is set to Results() class. - FabricConfigDeploy().commit() is called. - - FabricConfigDeploy().commit() sets ApiEndpoints().fabric_name + - FabricConfigDeploy().commit() sets EpFabricConfigDeploy().fabric_name - FabricConfigDeploy().commit() accesses - ApiEndpoints().fabric_config_deploy to set verb and path + EpFabricConfigDeploy().path/verb to set path and verb - FabricConfigDeploy() calls RestSend().commit() which sets RestSend().response_current to a dict with keys: - DATA == {"status": "Configuration deployment failed."} diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py index b7c565257..82bbc4cf1 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py @@ -32,6 +32,8 @@ import inspect import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricConfigSave from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ @@ -40,8 +42,6 @@ Results from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.config_save import \ FabricConfigSave -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( MockAnsibleModule, ResponseGenerator, does_not_raise, fabric_config_save_fixture, params, responses_fabric_config_save) @@ -71,7 +71,7 @@ def test_fabric_config_save_00010(fabric_config_save) -> None: assert instance.verb is None assert instance.state == "merged" assert isinstance(instance.conversion, ConversionUtils) - assert isinstance(instance.endpoints, ApiEndpoints) + assert isinstance(instance.ep_config_save, EpFabricConfigSave) def test_fabric_config_save_00011() -> None: @@ -342,48 +342,25 @@ def test_fabric_config_save_00080(monkeypatch, fabric_config_save) -> None: Summary - Verify that FabricConfigSave().commit() - re-raises ``ValueError`` when ApiEndpoints() raises + re-raises ``ValueError`` when EpFabricConfigSave() raises ``ValueError``. """ - class MockApiEndpoints: # pylint: disable=too-few-public-methods + class MockEpFabricConfigSave: # pylint: disable=too-few-public-methods """ - Mock the ApiEndpoints.fabric_config_save getter property + Mock the EpFabricConfigSave.path getter property to raise ``ValueError``. """ - def validate_fabric_name(self, value="MyFabric"): - """ - Mocked method required for test, but not relevant to test result. - """ - @property - def fabric_config_save(self): + def path(self): """ - Mocked property getter. - Raise ``ValueError``. """ - msg = "mocked ApiEndpoints().fabric_config_save getter exception" + msg = "mocked EpFabricConfigSave().path getter exception" raise ValueError(msg) - @property - def fabric_name(self): - """ - - Mocked fabric_config_save property getter - """ - return self._fabric_name - - @fabric_name.setter - def fabric_name(self, value): - """ - - Mocked fabric_name property setter - """ - self._fabric_name = value - - PATCH_API_ENDPOINTS = "ansible_collections.cisco.dcnm.plugins." - PATCH_API_ENDPOINTS += "module_utils.fabric.endpoints.ApiEndpoints." - PATCH_API_ENDPOINTS += "fabric_config_save" - payload = { "FABRIC_NAME": "f1", "FABRIC_TYPE": "VXLAN_EVPN", @@ -391,14 +368,14 @@ def fabric_name(self, value): "DEPLOY": True, } - match = r"mocked ApiEndpoints\(\)\.fabric_config_save getter exception" - with does_not_raise(): instance = fabric_config_save - monkeypatch.setattr(instance, "endpoints", MockApiEndpoints()) + monkeypatch.setattr(instance, "ep_config_save", MockEpFabricConfigSave()) instance.payload = payload instance.rest_send = RestSend(MockAnsibleModule()) instance.results = Results() + + match = r"mocked EpFabricConfigSave\(\)\.path getter exception" with pytest.raises(ValueError, match=match): instance.commit() @@ -427,9 +404,9 @@ def test_fabric_config_save_00090(monkeypatch, fabric_config_save) -> None: - FabricConfigSave() properties are set - FabricConfigSave.fabric_name is set "f1" - FabricConfigSave().commit() is called. - - FabricConfigSave().commit() sets ApiEndpoints().fabric_name + - FabricConfigSave().commit() sets EpFabricConfigSave().fabric_name - FabricConfigSave().commit() accesses - ApiEndpoints().fabric_config_save to set verb and path + EpFabricConfigSave().path/verb to set verb and path - FabricConfigSave() calls RestSend().commit() which sets RestSend().response_current to a dict with keys: - DATA == {"status": "Configuration deployment completed."} @@ -531,9 +508,9 @@ def test_fabric_config_save_00100(monkeypatch, fabric_config_save) -> None: - unit_test == True - FabricConfigSave().results is set to Results() class. - FabricConfigSave().commit() is called. - - FabricConfigSave().commit() sets ApiEndpoints().fabric_name + - FabricConfigSave().commit() sets EpFabricConfigSave().fabric_name - FabricConfigSave().commit() accesses - ApiEndpoints().fabric_config_save to set verb and path + EpFabricConfigSave().path/verb to set path and verb - FabricConfigSave() calls RestSend().commit() which sets RestSend().response_current to a dict with keys: - DATA == {"status": "Configuration deployment failed."} From 470046e9f84d7dda37eba522532ec2b06250a394 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 15 May 2024 11:07:52 -1000 Subject: [PATCH 043/230] FabricCreateCommon(): Use EpFabricCreate() 1. import EpFabricConfigSave, remove import for ApiEndpoints 2. FabricCreateCommon().__init__(): replace instantiation of self.endpoints with self.ep_fabric_create 3. FabricCreateCommon()._set_fabric_create_endpoint() use EpFabricCreate() class for endpoint info. 4. Update associated unit tests. 5. test_fabric_create_common.py: Add unit tests to bring FabricCreateCommon() UT coverage to 97% 6. test_fabric_config_deploy.py: rename EpFabricConfigDeploy() to MockEpFabricConfigDeploy() --- plugins/module_utils/fabric/create.py | 25 +-- .../fixtures/payloads_FabricCreateCommon.json | 27 +++ .../dcnm_fabric/test_fabric_config_deploy.py | 4 +- .../dcnm_fabric/test_fabric_create_common.py | 182 ++++++++++++++++-- 4 files changed, 212 insertions(+), 26 deletions(-) diff --git a/plugins/module_utils/fabric/create.py b/plugins/module_utils/fabric/create.py index 1d1f785a2..d03984dfe 100644 --- a/plugins/module_utils/fabric/create.py +++ b/plugins/module_utils/fabric/create.py @@ -23,10 +23,10 @@ import json import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricCreate from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.common import \ FabricCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ FabricTypes @@ -45,12 +45,13 @@ def __init__(self, params): self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.endpoints = ApiEndpoints() + self.ep_fabric_create = EpFabricCreate() self.fabric_types = FabricTypes() - # path and verb cannot be defined here because endpoints.fabric name - # must be set first. Set these to None here and define them later in - # the commit() method. + # path and verb cannot be defined here because + # EpFabricCreate().fabric_name must be set first. + # Set these to None here and define them later in + # _set_fabric_create_endpoint(). self.path: str = None self.verb: str = None @@ -97,7 +98,10 @@ def _set_fabric_create_endpoint(self, payload): - raise ``ValueError`` if the fabric_create endpoint assignment fails """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.endpoints.fabric_name = payload.get("FABRIC_NAME") + try: + self.ep_fabric_create.fabric_name = payload.get("FABRIC_NAME") + except ValueError as error: + raise ValueError(error) from error try: self.fabric_type = copy.copy(payload.get("FABRIC_TYPE")) @@ -109,16 +113,15 @@ def _set_fabric_create_endpoint(self, payload): template_name = self.fabric_types.template_name except ValueError as error: raise ValueError(error) from error - self.endpoints.template_name = template_name try: - endpoint = self.endpoints.fabric_create + self.ep_fabric_create.template_name = template_name except ValueError as error: raise ValueError(error) from error payload.pop("FABRIC_TYPE", None) - self.path = endpoint["path"] - self.verb = endpoint["verb"] + self.path = self.ep_fabric_create.path + self.verb = self.ep_fabric_create.verb def _send_payloads(self): """ diff --git a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/payloads_FabricCreateCommon.json b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/payloads_FabricCreateCommon.json index 15b6a85df..288b47356 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/payloads_FabricCreateCommon.json +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/payloads_FabricCreateCommon.json @@ -29,5 +29,32 @@ "DEPLOY": true, "FABRIC_NAME": "f1", "FABRIC_TYPE": "VXLAN_EVPN" + }, + "test_fabric_create_common_00033a": { + "TEST_NOTES": [ + "Valid payload." + ], + "BGP_AS": 65000, + "DEPLOY": true, + "FABRIC_NAME": "f1", + "FABRIC_TYPE": "VXLAN_EVPN" + }, + "test_fabric_create_common_00040a": { + "TEST_NOTES": [ + "Valid payload." + ], + "BGP_AS": 65000, + "DEPLOY": true, + "FABRIC_NAME": "f1", + "FABRIC_TYPE": "VXLAN_EVPN" + }, + "test_fabric_create_common_00050a": { + "TEST_NOTES": [ + "Valid payload." + ], + "BGP_AS": 65000, + "DEPLOY": true, + "FABRIC_NAME": "f1", + "FABRIC_TYPE": "VXLAN_EVPN" } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py index fc036cc70..7d997f8c1 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py @@ -422,7 +422,7 @@ def test_fabric_config_deploy_00200( method_name = inspect.stack()[0][3] key = f"{method_name}a" - class EpFabricConfigDeploy: # pylint: disable=too-few-public-methods + class MockEpFabricConfigDeploy: # pylint: disable=too-few-public-methods """ Mock the EpFabricConfigDeploy.path getter property to raise ``ValueError``. @@ -461,7 +461,7 @@ def mock_dcnm_send(*args, **kwargs): with does_not_raise(): instance = fabric_config_deploy - monkeypatch.setattr(instance, "ep_config_deploy", EpFabricConfigDeploy()) + monkeypatch.setattr(instance, "ep_config_deploy", MockEpFabricConfigDeploy()) instance.fabric_details = fabric_details_by_name instance.fabric_details.rest_send = RestSend(MockAnsibleModule()) instance.payload = payload diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py index 2cfae6d4b..e8997fe4f 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py @@ -32,10 +32,12 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricCreate +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ + RestSend from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - does_not_raise, fabric_create_common_fixture, + MockAnsibleModule, does_not_raise, fabric_create_common_fixture, payloads_fabric_create_common) @@ -54,7 +56,7 @@ def test_fabric_create_common_00010(fabric_create_common) -> None: with does_not_raise(): instance = fabric_create_common instance._build_properties() - assert isinstance(instance.endpoints, ApiEndpoints) + assert isinstance(instance.ep_fabric_create, EpFabricCreate) assert instance.class_name == "FabricCreateCommon" assert instance.action == "create" assert instance.check_mode is False @@ -99,12 +101,12 @@ def test_fabric_create_common_00032(monkeypatch, fabric_create_common) -> None: - FabricCreateCommon - __init__() - _set_fabric_create_endpoint - - endpoints.fabric_create + - ep_fabric_create.fabric_name setter Summary - - ``ValueError`` is raised when endpoints.fabric_create() raises an exception. + - ``ValueError`` is raised when ep_fabric_create.fabric_name raises an exception. - Since ``fabric_name`` and ``template_name`` are already verified in - _set_fabric_create_endpoint, ApiEndpoints().fabric_create() needs + _set_fabric_create_endpoint, EpFabricCreate().fabric_name setter needs to be mocked to raise an exception. """ method_name = inspect.stack()[0][3] @@ -112,23 +114,177 @@ def test_fabric_create_common_00032(monkeypatch, fabric_create_common) -> None: payload = payloads_fabric_create_common(key) - class MockApiEndpoints: # pylint: disable=too-few-public-methods + class MockEpFabricCreate: # pylint: disable=too-few-public-methods """ - Mock the ApiEndpoints.fabric_create() method to raise an exception. + Mock the EpFabricCreate.fabric_name setter property + to raise ``ValueError``. """ @property - def fabric_create(self): + def fabric_name(self): """ Mocked method """ - raise ValueError("mocked exception") + + @fabric_name.setter + def fabric_name(self, value): + """ + Mocked method + """ + msg = "MockEpFabricCreate.fabric_name: mocked exception." + raise ValueError(msg) with does_not_raise(): instance = fabric_create_common - instance.endpoints = MockApiEndpoints() + monkeypatch.setattr(instance, "ep_fabric_create", MockEpFabricCreate()) + instance.ep_fabric_create = MockEpFabricCreate() instance._build_properties() - match = "mocked exception" + match = r"MockEpFabricCreate\.fabric_name: mocked exception\." with pytest.raises(ValueError, match=match): instance._set_fabric_create_endpoint(payload) + + +def test_fabric_create_common_00033(monkeypatch, fabric_create_common) -> None: + """ + Classes and Methods + - FabricCommon + - __init__() + - FabricCreateCommon + - __init__() + - _set_fabric_create_endpoint + - ep_fabric_create.template_name setter + + Summary + - ``ValueError`` is raised when ep_fabric_create.template_name raises an exception. + - Since ``fabric_name`` and ``template_name`` are already verified in + _set_fabric_create_endpoint, EpFabricCreate().template_name setter needs + to be mocked to raise an exception. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + payload = payloads_fabric_create_common(key) + + class MockEpFabricCreate: # pylint: disable=too-few-public-methods + """ + Mock the EpFabricCreate.template_name setter property + to raise ``ValueError``. + """ + + @property + def template_name(self): + """ + Mocked method + """ + + @template_name.setter + def template_name(self, value): + """ + Mocked method + """ + msg = "MockEpFabricCreate.template_name: mocked exception." + raise ValueError(msg) + + with does_not_raise(): + instance = fabric_create_common + monkeypatch.setattr(instance, "ep_fabric_create", MockEpFabricCreate()) + instance.ep_fabric_create = MockEpFabricCreate() + instance._build_properties() + + match = r"MockEpFabricCreate\.template_name: mocked exception\." + with pytest.raises(ValueError, match=match): + instance._set_fabric_create_endpoint(payload) + + +def test_fabric_create_common_00040(monkeypatch, fabric_create_common) -> None: + """ + Classes and Methods + - FabricCommon + - __init__() + - FabricCreateCommon + - __init__() + - _set_fabric_create_endpoint + - fabric_types.template_name getter + + Summary + - ``ValueError`` is raised when fabric_types.template_name getter raises + an exception. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + payload = payloads_fabric_create_common(key) + + class MockFabricTypes: # pylint: disable=too-few-public-methods + """ + Mock the FabricTypes.template_name setter property + to raise ``ValueError``. + """ + + @property + def valid_fabric_types(self): + """ + Return fabric_type matching payload FABRIC_TYPE + """ + return ["VXLAN_EVPN"] + + @property + def template_name(self): + """ + Mocked method + """ + msg = "MockEpFabricCreate.template_name: mocked exception." + raise ValueError(msg) + + with does_not_raise(): + instance = fabric_create_common + monkeypatch.setattr(instance, "fabric_types", MockFabricTypes()) + instance._build_properties() + + match = r"MockEpFabricCreate\.template_name: mocked exception\." + with pytest.raises(ValueError, match=match): + instance._set_fabric_create_endpoint(payload) + + +def test_fabric_create_common_00050(monkeypatch, fabric_create_common) -> None: + """ + Classes and Methods + - FabricCommon + - __init__() + - FabricCreateCommon + - __init__() + - _set_fabric_create_endpoint + - _send_payloads() + + Summary + - _send_payloads() re-raises ``ValueError`` when + _set_fabric_create_endpoint() raises ``ValueError``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + payload = payloads_fabric_create_common(key) + + def mock_set_fabric_create_endpoint( + *args, + ): # pylint: disable=too-few-public-methods + """ + Mock the FabricCreateCommon()._set_fabric_create_endpoint() + to raise ``ValueError``. + """ + msg = "mock_set_fabric_endpoint(): mocked exception." + raise ValueError(msg) + + with does_not_raise(): + instance = fabric_create_common + instance.rest_send = RestSend(MockAnsibleModule()) + monkeypatch.setattr( + instance, "_set_fabric_create_endpoint", mock_set_fabric_create_endpoint + ) + instance._build_properties() + instance._payloads_to_commit = [payload] + + match = r"mock_set_fabric_endpoint\(\): mocked exception\." + with pytest.raises(ValueError, match=match): + instance._send_payloads() From ee58cbe274c2435e629f76f8e3f78777105e304d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 15 May 2024 13:56:48 -1000 Subject: [PATCH 044/230] FabricDelete: use EpFabricDelete() class 1. delete.py: Remove import for ApiEndpoints 2. delete.py: Add import for EpFabricDelete 3. FabricDelete.__init__(): remove self._endpoints instantiation 4. FabricDelete.__init__(): Add self.ep_fabric_delete = EpFabricDelete() 5. FabricDelete._set_fabric_delete_endpoint(): Modify to use self.ep_fabric_delete 6. Modify unit tests to reflect above changes. 7. Add integration test: dcnm_fabric_deleted_basic_ipfm and use to verify the above changes. --- plugins/module_utils/fabric/delete.py | 17 +- .../tests/dcnm_fabric_deleted_basic_ipfm.yaml | 250 ++++++++++++++++++ .../dcnm/dcnm_fabric/test_fabric_delete.py | 42 +-- 3 files changed, 278 insertions(+), 31 deletions(-) create mode 100644 tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_deleted_basic_ipfm.yaml diff --git a/plugins/module_utils/fabric/delete.py b/plugins/module_utils/fabric/delete.py index 9c8b6ddc3..2f9620de0 100644 --- a/plugins/module_utils/fabric/delete.py +++ b/plugins/module_utils/fabric/delete.py @@ -20,6 +20,8 @@ import inspect import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricDelete from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError # Import Results() only for the case where the user has not set Results() @@ -30,8 +32,6 @@ Results from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.common import \ FabricCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints class FabricDelete(FabricCommon): @@ -78,7 +78,7 @@ def __init__(self, params): self._fabrics_to_delete = [] self._build_properties() - self._endpoints = ApiEndpoints() + self.ep_fabric_delete = EpFabricDelete() self._cannot_delete_fabric_reason = None @@ -145,17 +145,12 @@ def _set_fabric_delete_endpoint(self, fabric_name) -> None: - Raise ``ValueError`` if the endpoint assignment fails """ try: - self._endpoints.fabric_name = fabric_name + self.ep_fabric_delete.fabric_name = fabric_name except (ValueError, TypeError) as error: raise ValueError(error) from error - try: - endpoint = self._endpoints.fabric_delete - except ValueError as error: - raise ValueError(error) from error - - self.path = endpoint.get("path") - self.verb = endpoint.get("verb") + self.path = self.ep_fabric_delete.path + self.verb = self.ep_fabric_delete.verb def _validate_commit_parameters(self): """ diff --git a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_deleted_basic_ipfm.yaml b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_deleted_basic_ipfm.yaml new file mode 100644 index 000000000..f023b992b --- /dev/null +++ b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_deleted_basic_ipfm.yaml @@ -0,0 +1,250 @@ +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 00:03.60 +################################################################################ +# DESCRIPTION - BASIC FABRIC DELETED STATE TEST FOR IPFM +# +# Test basic deletion of fabrics verify results. +# - Deletion of populated fabrics not tested here. +# - See dcnm_fabric_deleted_populated.yaml instead. +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 1. The following fabrics must be empty on the controller +# See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml +# - fabric_name_4 +# - fabric_type_4 # VXLAN_EVPN_IPFM +# 2. Delete fabrics under test, if they exist +# - fabric_name_4 +# TEST +# 3. Create fabrics and verify result +# - fabric_name_4 +# 4. Delete fabric_name_4. Verify result +# CLEANUP +# 7. No cleanup required +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_image_policy integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: dcnm_fabric_deleted_basic_ipfm +# fabric_name_4: IPFM_Fabric +# fabric_type_4: VXLAN_EVPN_IPFM +################################################################################ +# SETUP +################################################################################ +- name: DELETED - SETUP - Delete fabrics + cisco.dcnm.dcnm_fabric: + state: deleted + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + register: result +- debug: + var: result +################################################################################ +# DELETED - TEST - Create IPFM Fabric +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "create", +# "check_mode": false, +# "sequence_number": 1, +# "state": "merged" +# } +# ], +# "response": [ +# { +# "DATA": { +# "nvPairs": { +# "FABRIC_NAME": "IPFM_Fabric" +# } +# }, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/Easy_Fabric_IPFM", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: DELETED - SETUP - Create IPFM Fabric and verify + cisco.dcnm.dcnm_fabric: + state: merged + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + FABRIC_TYPE: "{{ fabric_type_4 }}" + DEPLOY: true + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].FABRIC_NAME == fabric_name_4 + - (result.response | length) == 1 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "POST" + - result.response[0].RETURN_CODE == 200 +############################################################################################### +# DELETED - TEST - Delete IPFM Fabric (fabric_name_4) and verify +############################################################################################### +# Expected result +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "delete", +# "check_mode": false, +# "sequence_number": 1, +# "state": "deleted" +# } +# ], +# "response": [ +# { +# "DATA": "Fabric 'IPFM_Fabric' is deleted successfully!", +# "MESSAGE": "OK", +# "METHOD": "DELETE", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +############################################################################################### +- name: DELETED - TEST - Delete IPFM fabric (fabric_name_4) and verify + cisco.dcnm.dcnm_fabric: &fabric_deleted + state: deleted + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].FABRIC_NAME == fabric_name_4 + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "delete" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "deleted" + - (result.response | length) == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "DELETE" + - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 +################################################################################ +# DELETED - TEST - Delete IPFM Fabric (fabric_name_4) and verify idempotence +################################################################################ +# Expected result +# ok: [ndfc1] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "delete", +# "check_mode": false, +# "sequence_number": 1, +# "state": "deleted" +# } +# ], +# "response": [ +# { +# "MESSAGE": "No fabrics to delete", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": false, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: DELETED - TEST - Delete IPFM Fabric (fabric_name_4) and verify idempotence + cisco.dcnm.dcnm_fabric: *fabric_deleted + register: result +- debug: + var: result +- assert: + that: + - result.changed == false + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "delete" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "deleted" + - (result.response | length) == 1 + - result.response[0].MESSAGE == "No fabrics to delete" + - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - result.result[0].changed == false + - result.result[0].success == true + - result.result[0].sequence_number == 1 diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py index 6de2dc84a..5bef60741 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py @@ -32,12 +32,12 @@ import inspect import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricDelete from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ FabricDetailsByName from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_summary import \ @@ -73,7 +73,7 @@ def test_fabric_delete_00010(fabric_delete) -> None: assert instance.path is None assert instance.state == "deleted" assert instance.verb is None - assert isinstance(instance._endpoints, ApiEndpoints) + assert isinstance(instance.ep_fabric_delete, EpFabricDelete) assert isinstance(instance.fabric_details, FabricDetailsByName) @@ -95,7 +95,9 @@ def test_fabric_delete_00020(fabric_delete) -> None: instance = fabric_delete instance.results = Results() instance._set_fabric_delete_endpoint("MyFabric") - assert instance.path == "/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/MyFabric" + path = "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics" + path += "/MyFabric" + assert instance.path == path assert instance.verb == "DELETE" @@ -389,11 +391,13 @@ def test_fabric_delete_00042(monkeypatch, fabric_delete) -> None: - commit() Summary - - Verify unsuccessful fabric delete code path (attempt to set - ``fabric_delete`` endpoint raises ``ValueError``). + - Verify FabricDelete().commit() re-raises ``ValueError`` when + ``EpFabricDelete()._send_requests() re-raises ``ValueError`` when + ``EpFabricDelete()._send_request() re-raises ``ValueError`` when + ``FabricDelete()._set_fabric_delete_endpoint()`` raises ``ValueError``. - The user attempts to delete a fabric and the fabric exists on the controller, and the fabric is empty, but _set_fabric_delete_endpoint() - raises ``ValueError``. + re-raises ``ValueError``. Code Flow - FabricDelete.commit() calls FabricDelete()._validate_commit_parameters() @@ -412,32 +416,30 @@ def test_fabric_delete_00042(monkeypatch, fabric_delete) -> None: - FabricDelete._send_requests() calls FabricDelete._send_request() for each fabric in the FabricDelete()._fabrics_to_delete list. - FabricDelete._send_request() calls FabricDelete._set_fabric_delete_endpoint() - which is mocked to raise ``ValueError``. + which calls EpFabricDelete().fabric_name setter, which is mocked to raise + ``ValueError``. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" - class MockApiEndpoints: # pylint: disable=too-few-public-methods + class MockEpFabricDelete: # pylint: disable=too-few-public-methods """ - Mock the ApiEndpoints.fabric_delete property to raise ``ValueError``. + Mock the EpFabricDelete.path property to raise ``ValueError``. """ @property - def fabric_delete(self): + def fabric_name(self): """ Mocked property getter """ - raise ValueError("mocked ApiEndpoints().fabric_delete getter exception") - @fabric_delete.setter - def fabric_delete(self, value): + @fabric_name.setter + def fabric_name(self, value): """ Mocked property setter """ - raise ValueError("mocked ApiEndpoints().fabric_delete setter exception") - - PATCH_API_ENDPOINTS = "ansible_collections.cisco.dcnm.plugins." - PATCH_API_ENDPOINTS += "module_utils.fabric.endpoints.ApiEndpoints.fabric_delete" + msg = "mocked MockEpFabricDelete().fabric_name setter exception." + raise ValueError(msg) PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" @@ -456,7 +458,7 @@ def mock_dcnm_send(*args, **kwargs): with does_not_raise(): instance = fabric_delete - monkeypatch.setattr(instance, "_endpoints", MockApiEndpoints()) + monkeypatch.setattr(instance, "ep_fabric_delete", MockEpFabricDelete()) instance.fabric_names = ["f1"] instance.fabric_details = FabricDetailsByName(params) @@ -472,7 +474,7 @@ def mock_dcnm_send(*args, **kwargs): instance.results = Results() - match = r"mocked ApiEndpoints\(\)\.fabric_delete getter exception" + match = r"mocked MockEpFabricDelete\(\)\.fabric_name setter exception\." with pytest.raises(ValueError, match=match): instance.commit() From 0c266efc5d52ba4ccfdbfb2413e49df7e83ba523 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 15 May 2024 14:28:02 -1000 Subject: [PATCH 045/230] FabricSummary: Use EpFabricSummary(), more... 1. Add Fabrics().EpFabricSummary() class 2. FabricSummary: use EpFabricSummary() class 3. fabric_summary.py: Remove import for ApiEndpoints 4. fabric_summary.py: Add import for EpFabricSummary 5. FabricSummary.__init__(): remove self.endpoints instantiation 6. FabricSummary.__init__(): Add self.ep_fabric_summary = EpFabricSummary() 7. FabricSummary. _set_fabric_summary_endpoint(): Modify to use self.ep_fabric_summary 8. Modify unit tests to reflect above changes. --- .../common/api/v1/rest/control/fabrics.py | 52 +++++++++++++++++++ plugins/module_utils/fabric/fabric_summary.py | 12 ++--- .../dcnm/dcnm_fabric/test_fabric_summary.py | 33 +++++------- 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py index f2c825a83..04bf98cc7 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -548,6 +548,58 @@ def path(self): return f"{self.path_fabric_name}/freezemode" +class EpFabricSummary(Fabrics): + """ + ## V1 API - Fabrics().EpFabricSummary() + + ### Description + Return the endpoint to query fabric summary information + for ``fabric_name``. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/rest/control/fabrics/summary/{fabric_name}/overview`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricSummary() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return f"{self.path_fabric_name}/overview" + + class EpFabricUpdate(Fabrics): """ ## V1 API - Fabrics().EpFabricUpdate() diff --git a/plugins/module_utils/fabric/fabric_summary.py b/plugins/module_utils/fabric/fabric_summary.py index c54a58808..ef641f04b 100644 --- a/plugins/module_utils/fabric/fabric_summary.py +++ b/plugins/module_utils/fabric/fabric_summary.py @@ -23,6 +23,8 @@ import json import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricSummary from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ @@ -31,8 +33,6 @@ Results from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.common import \ FabricCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints class FabricSummary(FabricCommon): @@ -96,7 +96,7 @@ def __init__(self, params): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.data = None - self.endpoints = ApiEndpoints() + self.ep_fabric_summary = EpFabricSummary() self.conversion = ConversionUtils() # set to True in refresh() after a successful request to the controller @@ -154,9 +154,9 @@ def _set_fabric_summary_endpoint(self): - Raise ``ValueError`` if unable to retrieve the endpoint. """ try: - self.endpoints.fabric_name = self.fabric_name - self.rest_send.path = self.endpoints.fabric_summary.get("path") - self.rest_send.verb = self.endpoints.fabric_summary.get("verb") + self.ep_fabric_summary.fabric_name = self.fabric_name + self.rest_send.path = self.ep_fabric_summary.path + self.rest_send.verb = self.ep_fabric_summary.verb except ValueError as error: msg = "Error retrieving fabric_summary endpoint. " msg += f"Detail: {error}" diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py index 3af75d5de..fe1b262fb 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py @@ -32,6 +32,8 @@ import inspect import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricSummary from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ @@ -40,8 +42,6 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( MockAnsibleModule, ResponseGenerator, does_not_raise, fabric_summary_fixture, responses_fabric_summary) @@ -64,7 +64,7 @@ def test_fabric_summary_00010(fabric_summary) -> None: assert instance.class_name == "FabricSummary" assert instance.data is None assert instance.refreshed is False - assert isinstance(instance.endpoints, ApiEndpoints) + assert isinstance(instance.ep_fabric_summary, EpFabricSummary) assert isinstance(instance.results, Results) assert isinstance(instance.conversion, ConversionUtils) assert instance._properties["border_gateway_count"] == 0 @@ -158,13 +158,13 @@ def test_fabric_summary_00032(monkeypatch, fabric_summary) -> None: Summary - Verify that FabricSummary()._set_fabric_summary_endpoint() - re-raises ``ValueError`` when ApiEndpoints() raises + re-raises ``ValueError`` when EpFabricSummary() raises ``ValueError``. """ - class MockApiEndpoints: # pylint: disable=too-few-public-methods + class MockEpFabricSummary: # pylint: disable=too-few-public-methods """ - Mock the ApiEndpoints.fabric_summary getter property to raise ``ValueError``. + Mock the EpFabricSummary.fabric_name getter property to raise ``ValueError``. """ def validate_fabric_name(self, value="MyFabric"): @@ -172,36 +172,27 @@ def validate_fabric_name(self, value="MyFabric"): Mocked method required for test, but not relevant to test result. """ - @property - def fabric_summary(self): - """ - - Mocked property getter. - - Raise ``ValueError``. - """ - raise ValueError("mocked ApiEndpoints().fabric_summary getter exception") - @property def fabric_name(self): """ - Mocked fabric_name property getter """ - return self._fabric_name @fabric_name.setter def fabric_name(self, value): """ - Mocked fabric_name property setter """ - self._fabric_name = value - - PATCH_API_ENDPOINTS = "ansible_collections.cisco.dcnm.plugins." - PATCH_API_ENDPOINTS += "module_utils.fabric.endpoints.ApiEndpoints.fabric_summary" + msg = "mocked MockEpFabricSummary().fabric_name setter exception." + raise ValueError(msg) - match = r"mocked ApiEndpoints\(\)\.fabric_summary getter exception" + match = r"Error retrieving fabric_summary endpoint\.\s+" + match += r"Detail: mocked MockEpFabricSummary\(\)\.fabric_name\s+" + match += r"setter exception\." with does_not_raise(): instance = fabric_summary - monkeypatch.setattr(instance, "endpoints", MockApiEndpoints()) + monkeypatch.setattr(instance, "ep_fabric_summary", MockEpFabricSummary()) instance.fabric_name = "MyFabric" instance.rest_send = RestSend(MockAnsibleModule()) with pytest.raises(ValueError, match=match): From 1354646f50025aea5927e79317543083fd20574b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 15 May 2024 15:34:57 -1000 Subject: [PATCH 046/230] FabricReplacedCommon: use EpFabricUpdate(), more... 1. FabricReplacedCommon(): use EpFabricUpdate() instead of ApiEndpoints() for endpoint resolution. 2. test_fabric_replaced_bulk.py: Update unit tests to reflect 1 above. 3. test_fabric_summary.py: Fix import of EpFabricSummary 4. fabric_summary.py: Fix import of EpFabricSummary 5. Add integration test: dcnm_fabric_replaced_basic_ipfm 6. Update playbooks/roles/dcnm_fabric/dcnm_tests.yaml --- playbooks/roles/dcnm_fabric/dcnm_tests.yaml | 8 +- plugins/module_utils/fabric/fabric_summary.py | 2 +- plugins/module_utils/fabric/replaced.py | 19 +- .../dcnm_fabric_replaced_basic_ipfm.yaml | 413 ++++++++++++++++++ .../dcnm_fabric/test_fabric_replaced_bulk.py | 6 +- .../dcnm/dcnm_fabric/test_fabric_summary.py | 2 +- 6 files changed, 433 insertions(+), 17 deletions(-) create mode 100644 tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_basic_ipfm.yaml diff --git a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml index 059cca1ac..a3cc72d88 100644 --- a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml +++ b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml @@ -18,12 +18,14 @@ vars: # This testcase field can run any test in the tests directory for the role # testcase: dcnm_fabric_deleted_basic + # testcase: dcnm_fabric_deleted_basic_ipfm # testcase: dcnm_fabric_merged_basic + # testcase: dcnm_fabric_merged_basic_ipfm # testcase: dcnm_fabric_merged_save_deploy + # testcase: dcnm_fabric_merged_save_deploy_ipfm # testcase: dcnm_fabric_replaced_basic + # testcase: dcnm_fabric_replaced_basic_ipfm # testcase: dcnm_fabric_replaced_save_deploy - # testcase: dcnm_fabric_merged_basic_ipfm - # testcase: dcnm_fabric_merged_save_deploy_ipfm # testcase: dcnm_fabric_replaced_save_deploy_ipfm fabric_name_1: VXLAN_EVPN_Fabric fabric_type_1: VXLAN_EVPN @@ -35,6 +37,8 @@ fabric_type_4: IPFM leaf_1: 172.22.150.103 leaf_2: 172.22.150.104 + nxos_username: admin + nxos_password: myNxosPassword roles: - dcnm_fabric diff --git a/plugins/module_utils/fabric/fabric_summary.py b/plugins/module_utils/fabric/fabric_summary.py index ef641f04b..d96ef3d64 100644 --- a/plugins/module_utils/fabric/fabric_summary.py +++ b/plugins/module_utils/fabric/fabric_summary.py @@ -23,7 +23,7 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.switches import \ EpFabricSummary from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/plugins/module_utils/fabric/replaced.py b/plugins/module_utils/fabric/replaced.py index a6ddc8053..153e4daa4 100644 --- a/plugins/module_utils/fabric/replaced.py +++ b/plugins/module_utils/fabric/replaced.py @@ -23,12 +23,12 @@ import json import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricUpdate from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.common import \ FabricCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ FabricTypes from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.param_info import \ @@ -54,7 +54,7 @@ def __init__(self, params): self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.endpoints = ApiEndpoints() + self.ep_fabric_update = EpFabricUpdate() self.fabric_types = FabricTypes() self.param_info = ParamInfo() self.ruleset = RuleSet() @@ -484,26 +484,25 @@ def _set_fabric_update_endpoint(self, payload): - Set the endpoint for the fabric update API call. - raise ``ValueError`` if the enpoint assignment fails """ - self.endpoints.fabric_name = payload.get("FABRIC_NAME") - self.fabric_type = copy.copy(payload.get("FABRIC_TYPE")) try: - self.fabric_types.fabric_type = self.fabric_type + self.ep_fabric_update.fabric_name = payload.get("FABRIC_NAME") except ValueError as error: raise ValueError(error) from error + self.fabric_type = copy.copy(payload.get("FABRIC_TYPE")) try: - self.endpoints.template_name = self.fabric_types.template_name + self.fabric_types.fabric_type = self.fabric_type except ValueError as error: raise ValueError(error) from error try: - endpoint = self.endpoints.fabric_update + self.ep_fabric_update.template_name = self.fabric_types.template_name except ValueError as error: raise ValueError(error) from error payload.pop("FABRIC_TYPE", None) - self.path = endpoint["path"] - self.verb = endpoint["verb"] + self.path = self.ep_fabric_update.path + self.verb = self.ep_fabric_update.verb def _send_payload(self, payload): """ diff --git a/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_basic_ipfm.yaml b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_basic_ipfm.yaml new file mode 100644 index 000000000..5b0c84a15 --- /dev/null +++ b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_basic_ipfm.yaml @@ -0,0 +1,413 @@ +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 00:05.64 +################################################################################ +# DESCRIPTION - BASIC FABRIC REPLACED STATE TEST for IPFM +# +# Test basic replace of new fabric configurations and verify results. +# - config-save and config-deploy not tested here. +# - See dcnm_fabric_replaced_save_deploy_ipfm.yaml instead. +################################################################################ +# STEPS +################################################################################ +# SETUP +# 1. The following fabrics must be empty on the controller +# See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml +# - fabric_name_4 +# - fabric_type_4 # IPFM +# 3. Delete fabrics under test, if they exist +# - fabric_name_4 +# TEST +# 4. Create fabrics with non-default configs and verify result +# - fabric_name_4 +# 5. Replace configs for fabric_4 verify result +# CLEANUP +# 7. Delete fabrics under test +# - fabric_name_4 +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_image_policy integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: deleted +# fabric_name_4: IPFM_Fabric +# fabric_type_4: IPFM +################################################################################ +# REPLACED - SETUP - Delete fabrics +################################################################################ +- name: REPLACED - SETUP - Delete fabrics + cisco.dcnm.dcnm_fabric: + state: deleted + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + register: result +- debug: + var: result +################################################################################ +# REPLACED - TEST - Create IPFM Fabric with non-default configs +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_MTU": 1500, +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# }, +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "create", +# "check_mode": false, +# "sequence_number": 1, +# "state": "merged" +# } +# ], +# "response": [ +# { +# "DATA": { +# "nvPairs": { +# "FABRIC_NAME": "IPFM_Fabric", +# "FABRIC_MTU": "1500" +# } +# }, +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric/Easy_Fabric_IPFM", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: REPLACED - TEST - Create IPFM fabric with non-default config. + cisco.dcnm.dcnm_fabric: + state: merged + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + FABRIC_TYPE: "{{ fabric_type_4 }}" + FABRIC_MTU: 1500 + DEPLOY: true + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].sequence_number == 1 + - result.diff[0].FABRIC_MTU == 1500 + - (result.metadata | length) == 1 + - result.metadata[0].action == "create" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - (result.response | length) == 1 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "POST" + - result.response[0].RETURN_CODE == 200 + - result.response[0].DATA.nvPairs.FABRIC_MTU == "1500" + - result.response[0].DATA.nvPairs.FABRIC_NAME == fabric_name_4 + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 +################################################################################ +# REPLACED - TEST - Replace configs for fabric_4 with default config +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [172.22.150.244] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_MTU": "9216", +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "sequence_number": 3 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "replace", +# "check_mode": false, +# "sequence_number": 1, +# "state": "replaced" +# }, +# { +# "action": "config_save", +# "check_mode": false, +# "sequence_number": 2, +# "state": "replaced" +# }, +# { +# "action": "config_deploy", +# "check_mode": false, +# "sequence_number": 3, +# "state": "replaced" +# } +# ], +# "response": [ +# { +# "DATA": { +# "nvPairs": { +# "FABRIC_MTU": "9216", +# "FABRIC_NAME": "IPFM_Fabric", +# } +# }, +# "MESSAGE": "OK", +# "METHOD": "PUT", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/IPFM_Fabric/Easy_Fabric_IPFM", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# }, +# { +# "MESSAGE": "Fabric IPFM_Fabric DEPLOY is False or None. Skipping config-save.", +# "RETURN_CODE": 200, +# "sequence_number": 2 +# }, +# { +# "MESSAGE": "Fabric IPFM_Fabric DEPLOY is False or None. Skipping config-deploy.", +# "RETURN_CODE": 200, +# "sequence_number": 3 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# }, +# { +# "changed": true, +# "sequence_number": 2, +# "success": true +# }, +# { +# "changed": true, +# "sequence_number": 3, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: REPLACED - TEST - Replace configs for fabric_4 with default config + cisco.dcnm.dcnm_fabric: &replace_fabric_4 + state: replaced + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + FABRIC_TYPE: "{{ fabric_type_4 }}" + DEPLOY: false + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 3 + - result.diff[0].FABRIC_MTU == "9216" + - result.diff[0].sequence_number == 1 + - result.diff[1].sequence_number == 2 + - result.diff[2].sequence_number == 3 + - (result.metadata | length) == 3 + - result.metadata[0].action == "replace" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "replaced" + - result.metadata[1].action == "config_save" + - result.metadata[1].check_mode == False + - result.metadata[1].sequence_number == 2 + - result.metadata[1].state == "replaced" + - result.metadata[2].action == "config_deploy" + - result.metadata[2].check_mode == False + - result.metadata[2].sequence_number == 3 + - result.metadata[2].state == "replaced" + - (result.response | length) == 3 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "PUT" + - result.response[0].RETURN_CODE == 200 + - result.response[0].DATA.nvPairs.FABRIC_MTU == "9216" + - result.response[0].DATA.nvPairs.FABRIC_NAME == "IPFM_Fabric" + - result.response[1].sequence_number == 2 + - result.response[1].MESSAGE is match '.*Skipping config-save.*' + - result.response[1].RETURN_CODE == 200 + - result.response[2].sequence_number == 3 + - result.response[2].MESSAGE is match '.*Skipping config-deploy.*' + - result.response[2].RETURN_CODE == 200 + - (result.result | length) == 3 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 + - result.result[1].changed == true + - result.result[1].success == true + - result.result[1].sequence_number == 2 + - result.result[2].changed == true + - result.result[2].success == true + - result.result[2].sequence_number == 3 +################################################################################ +# REPLACED - TEST - Replace config for fabric_4 with default config omnipotence +################################################################################ +# Expected result +# - All untested nvPairs removed for brevity. +# - Fabric global keys in DATA removed for brevity. +# ok: [ndfc1] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "replace", +# "check_mode": false, +# "sequence_number": 1, +# "state": "replaced" +# } +# ], +# "response": [ +# { +# "MESSAGE": "No fabrics to update for replaced state.", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": false, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: REPLACED - TEST - Replace config for fabric_4 with default config omnipotence + cisco.dcnm.dcnm_fabric: *replace_fabric_4 + register: result +- debug: + var: result +- assert: + that: + - result.changed == false + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "replace" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "replaced" + - (result.response | length) == 1 + - result.response[0].sequence_number == 1 + - result.response[0].MESSAGE == "No fabrics to update for replaced state." + - result.response[0].RETURN_CODE == 200 + - (result.result | length) == 1 + - result.result[0].changed == false + - result.result[0].success == true + - result.result[0].sequence_number == 1 +################################################################################ +# REPLACED - CLEANUP - Delete the fabrics +################################################################################ +# Expected result +# ok: [ndfc1] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "FABRIC_NAME": "IPFM_Fabric", +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "delete", +# "check_mode": false, +# "sequence_number": 1, +# "state": "deleted" +# } +# ], +# "response": [ +# { +# "DATA": "Fabric 'IPFM_Fabric' is deleted successfully!", +# "MESSAGE": "OK", +# "METHOD": "DELETE", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics/IPFM_Fabric", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ +- name: MERGED - CLEANUP - Delete the fabrics + cisco.dcnm.dcnm_fabric: + state: deleted + config: + - FABRIC_NAME: "{{ fabric_name_4 }}" + register: result +- debug: + var: result +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].FABRIC_NAME == fabric_name_4 + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "delete" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "deleted" + - (result.response | length) == 1 + - result.response[0].DATA is match '.*deleted successfully.*' + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "DELETE" + - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py index 73f319955..268420fdf 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py @@ -32,12 +32,12 @@ import inspect import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ + EpFabricUpdate from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ FabricDetailsByName from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_summary import \ @@ -81,7 +81,7 @@ def test_fabric_replaced_bulk_00010(fabric_replaced_bulk) -> None: assert instance.path is None assert instance.verb is None assert instance.state == "replaced" - assert isinstance(instance.endpoints, ApiEndpoints) + assert isinstance(instance.ep_fabric_update, EpFabricUpdate) assert isinstance(instance.fabric_details, FabricDetailsByName) assert isinstance(instance.fabric_summary, FabricSummary) assert isinstance(instance.fabric_types, FabricTypes) diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py index fe1b262fb..7aaa0f9a9 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.switches import \ EpFabricSummary from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils From ff4b37293b6c343f0a93afb8bfc32c1ea5083baa Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 15 May 2024 15:50:45 -1000 Subject: [PATCH 047/230] Fabrics(): Remove EpFabricSummary() This class is already in Switches() where is properly belongs. Added a comment in /rest/control/fabrics.py directing future maintainers to /rest/control/switches.py --- .../common/api/v1/rest/control/fabrics.py | 51 +------------------ 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics.py index 04bf98cc7..986e4ea5f 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics.py @@ -548,56 +548,7 @@ def path(self): return f"{self.path_fabric_name}/freezemode" -class EpFabricSummary(Fabrics): - """ - ## V1 API - Fabrics().EpFabricSummary() - - ### Description - Return the endpoint to query fabric summary information - for ``fabric_name``. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/rest/control/fabrics/summary/{fabric_name}/overview`` - - ### Verb - - GET - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricSummary() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - return f"{self.path_fabric_name}/overview" +# class EpFabricSummary() See module_utils/common/api/v1/rest/control/switches.py class EpFabricUpdate(Fabrics): From 73f2c89f348781c8c7b9254f41aec01da0b16018 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 16 May 2024 15:12:28 -1000 Subject: [PATCH 048/230] Align api.v1.* with NDFC REST API documentation Modify endpoint classes to align hierarchically with NFDC REST API docs. We have taken a couple liberties with class names for naming consistency, but the directory structure is now identical to the REST API docs. Modify dcnm_fabric modules and unit tests to import the classes from the new locations. --- plugins/module_utils/common/api/__init__.py | 65 ++ plugins/module_utils/common/api/common.py | 65 -- .../module_utils/common/api/v1/__init__.py | 41 ++ .../__init__.py} | 12 +- .../api/v1/configtemplate/rest/__init__.py | 49 ++ .../v1/configtemplate/rest/config/__init__.py | 49 ++ .../rest/config/templates/__init__.py} | 67 +- .../common/api/v1/{fm.py => fm/__init__.py} | 48 +- .../__init__.py} | 10 +- .../api/v1/imagemanagement/rest/__init__.py | 49 ++ .../rest/imagemgnt/__init__.py} | 27 +- .../rest/imageupgrade/__init__.py} | 60 +- .../rest/policymgnt/__init__.py} | 110 +-- .../rest/stagingmanagement/__init__.py} | 69 +- .../{lan_fabric.py => lan_fabric/__init__.py} | 14 +- .../rest/__init__.py} | 19 +- .../v1/lan_fabric/rest/control/__init__.py | 43 ++ .../rest/control/fabrics/__init__.py | 665 ++++++++++++++++++ .../rest/control/switches/__init__.py | 141 ++++ .../common/api/v1/rest/__init__.py | 49 ++ .../common/api/v1/rest/control/__init__.py | 49 ++ .../{fabrics.py => fabrics/__init__.py} | 20 +- .../{switches.py => switches/__init__.py} | 20 +- plugins/module_utils/fabric/config_deploy.py | 2 +- plugins/module_utils/fabric/config_save.py | 2 +- plugins/module_utils/fabric/create.py | 2 +- plugins/module_utils/fabric/delete.py | 2 +- plugins/module_utils/fabric/fabric_details.py | 2 +- plugins/module_utils/fabric/fabric_summary.py | 2 +- plugins/module_utils/fabric/replaced.py | 2 +- .../common/api/test_v1_api_fabrics.py | 2 +- .../common/api/test_v1_api_image_mgnt.py | 2 +- .../api/test_v1_api_image_upgrade_ep.py | 2 +- .../common/api/test_v1_api_policy_mgnt.py | 2 +- .../api/test_v1_api_staging_management.py | 2 +- .../common/api/test_v1_api_switches.py | 2 +- .../common/api/test_v1_api_templates.py | 2 +- .../dcnm_fabric/test_fabric_config_deploy.py | 2 +- .../dcnm_fabric/test_fabric_config_save.py | 2 +- .../dcnm_fabric/test_fabric_create_common.py | 2 +- .../dcnm/dcnm_fabric/test_fabric_delete.py | 2 +- .../dcnm/dcnm_fabric/test_fabric_details.py | 2 +- .../test_fabric_details_by_name.py | 2 +- .../test_fabric_details_by_nv_pair.py | 2 +- .../dcnm_fabric/test_fabric_replaced_bulk.py | 2 +- .../dcnm/dcnm_fabric/test_fabric_summary.py | 2 +- 46 files changed, 1498 insertions(+), 289 deletions(-) delete mode 100644 plugins/module_utils/common/api/common.py rename plugins/module_utils/common/api/v1/{config_template.py => configtemplate/__init__.py} (80%) create mode 100644 plugins/module_utils/common/api/v1/configtemplate/rest/__init__.py create mode 100644 plugins/module_utils/common/api/v1/configtemplate/rest/config/__init__.py rename plugins/module_utils/common/api/v1/{rest/config/templates.py => configtemplate/rest/config/templates/__init__.py} (77%) rename plugins/module_utils/common/api/v1/{fm.py => fm/__init__.py} (73%) rename plugins/module_utils/common/api/v1/{image_management.py => imagemanagement/__init__.py} (85%) create mode 100644 plugins/module_utils/common/api/v1/imagemanagement/rest/__init__.py rename plugins/module_utils/common/api/v1/{rest/image_mgnt.py => imagemanagement/rest/imagemgnt/__init__.py} (76%) rename plugins/module_utils/common/api/v1/{rest/image_upgrade.py => imagemanagement/rest/imageupgrade/__init__.py} (68%) rename plugins/module_utils/common/api/v1/{rest/policy_mgnt.py => imagemanagement/rest/policymgnt/__init__.py} (74%) rename plugins/module_utils/common/api/v1/{rest/staging_management.py => imagemanagement/rest/stagingmanagement/__init__.py} (65%) rename plugins/module_utils/common/api/v1/{lan_fabric.py => lan_fabric/__init__.py} (79%) rename plugins/module_utils/common/api/v1/{common_v1.py => lan_fabric/rest/__init__.py} (72%) create mode 100644 plugins/module_utils/common/api/v1/lan_fabric/rest/control/__init__.py create mode 100644 plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/__init__.py create mode 100644 plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/__init__.py rename plugins/module_utils/common/api/v1/rest/control/{fabrics.py => fabrics/__init__.py} (97%) rename plugins/module_utils/common/api/v1/rest/control/{switches.py => switches/__init__.py} (88%) diff --git a/plugins/module_utils/common/api/__init__.py b/plugins/module_utils/common/api/__init__.py index e69de29bb..e56077a5c 100644 --- a/plugins/module_utils/common/api/__init__.py +++ b/plugins/module_utils/common/api/__init__.py @@ -0,0 +1,65 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils + + +class Api: + """ + ## API endpoints - Api() + + ### Description + Common methods and properties for Api() subclasses. + + ### Path + ``/appcenter/cisco/ndfc/api`` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.conversion = ConversionUtils() + # Popuate in subclasses to indicate which properties + # are mandatory for the subclass. + self.required_properties = set() + self.log.debug("ENTERED api.Api()") + self.api = "/appcenter/cisco/ndfc/api" + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["path"] = None + self.properties["verb"] = None + + @property + def path(self): + """ + Return the endpoint path. + """ + return self.properties["path"] + + @property + def verb(self): + """ + Return the endpoint verb. + """ + return self.properties["verb"] diff --git a/plugins/module_utils/common/api/common.py b/plugins/module_utils/common/api/common.py deleted file mode 100644 index 1530e5f97..000000000 --- a/plugins/module_utils/common/api/common.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ - ConversionUtils - - -class Common: - """ - ## API endpoints - Common - - ### Description - Common methods and properties for subclasses. - - ### Path - ``/appcenter/cisco/ndfc/api`` - """ - - def __init__(self): - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.conversion = ConversionUtils() - # Popuate in subclasses to indicate which properties - # are mandatory for the subclass. - self.required_properties = set() - self.log.debug("ENTERED api.CommonApi()") - self.api = "/appcenter/cisco/ndfc/api" - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["path"] = None - self.properties["verb"] = None - - @property - def path(self): - """ - Return the endpoint path. - """ - return self.properties["path"] - - @property - def verb(self): - """ - Return the endpoint verb. - """ - return self.properties["verb"] diff --git a/plugins/module_utils/common/api/v1/__init__.py b/plugins/module_utils/common/api/v1/__init__.py index e69de29bb..06e73ad73 100644 --- a/plugins/module_utils/common/api/v1/__init__.py +++ b/plugins/module_utils/common/api/v1/__init__.py @@ -0,0 +1,41 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api import Api + + +class V1(Api): + """ + ## v1 API enpoints - Api().V1() + + ### Description + Common methods and properties for API v1 subclasses. + + ### Path + ``/appcenter/cisco/ndfc/api/v1/`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.V1()") + self.v1 = f"{self.api}/v1" diff --git a/plugins/module_utils/common/api/v1/config_template.py b/plugins/module_utils/common/api/v1/configtemplate/__init__.py similarity index 80% rename from plugins/module_utils/common/api/v1/config_template.py rename to plugins/module_utils/common/api/v1/configtemplate/__init__.py index 657f573bd..eabbebb04 100644 --- a/plugins/module_utils/common/api/v1/config_template.py +++ b/plugins/module_utils/common/api/v1/configtemplate/__init__.py @@ -19,16 +19,16 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common_v1 import \ - CommonV1 +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1 import \ + V1 -class ConfigTemplate(CommonV1): +class ConfigTemplate(V1): """ ## V1 API - ConfigTemplate() ### Description - Common methods and properties for CommonV1().ConfigTemplate() subclasses + Common methods and properties for api.v1.ConfigTemplate() subclasses ### Path ``/appcenter/cisco/ndfc/api/v1/configtemplate`` @@ -38,5 +38,5 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.config_template = f"{self.api_v1}/configtemplate" - self.log.debug("ENTERED api.v1.ConfigTemplate()") + self.configtemplate = f"{self.v1}/configtemplate" + self.log.debug("ENTERED api.v1.configtemplate.ConfigTemplate()") diff --git a/plugins/module_utils/common/api/v1/configtemplate/rest/__init__.py b/plugins/module_utils/common/api/v1/configtemplate/rest/__init__.py new file mode 100644 index 000000000..f28751037 --- /dev/null +++ b/plugins/module_utils/common/api/v1/configtemplate/rest/__init__.py @@ -0,0 +1,49 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate import \ + ConfigTemplate + + +class Rest(ConfigTemplate): + """ + ## V1 API ConfigTemplate() - api.v1.configtemplate.rest.Rest() + + ### Description + Common methods and properties for api.v1.configtemplate.rest.Rest() subclasses. + + ### Path + - ``/api/v1/configtemplate/rest`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.rest = f"{self.configtemplate}/rest" + msg = f"ENTERED api.v1.configtemplate.rest.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Populate class-specific properties. + """ diff --git a/plugins/module_utils/common/api/v1/configtemplate/rest/config/__init__.py b/plugins/module_utils/common/api/v1/configtemplate/rest/config/__init__.py new file mode 100644 index 000000000..e6a0171a0 --- /dev/null +++ b/plugins/module_utils/common/api/v1/configtemplate/rest/config/__init__.py @@ -0,0 +1,49 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest import \ + Rest + + +class Config(Rest): + """ + ## V1 API Config() - api.v1.configtemplate.rest.config.Config() + + ### Description + Common methods and properties for api.v1.configtemplate.rest.config.Config() subclasses. + + ### Path + - ``/api/v1/configtemplate/rest/config`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.config = f"{self.rest}/config" + msg = f"ENTERED api.v1.rest.config.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Populate class-specific properties. + """ diff --git a/plugins/module_utils/common/api/v1/rest/config/templates.py b/plugins/module_utils/common/api/v1/configtemplate/rest/config/templates/__init__.py similarity index 77% rename from plugins/module_utils/common/api/v1/rest/config/templates.py rename to plugins/module_utils/common/api/v1/configtemplate/rest/config/templates/__init__.py index c4cf5f04f..ee6def39e 100644 --- a/plugins/module_utils/common/api/v1/rest/config/templates.py +++ b/plugins/module_utils/common/api/v1/configtemplate/rest/config/templates/__init__.py @@ -20,21 +20,21 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.config_template import \ - ConfigTemplate +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest.config import \ + Config from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ FabricTypes -class Templates(ConfigTemplate): +class Templates(Config): """ - ## V1 API Fabrics - ConfigTemplate().Templates() + ## api.v1.configtemplate.rest.config.templates.Templates() ### Description Common methods and properties for Templates() subclasses. ### Path - - ``/configtemplate/rest/config/templates`` + - ``/api/v1/configtemplate/rest/config/templates`` """ def __init__(self): @@ -43,16 +43,11 @@ def __init__(self): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.fabric_types = FabricTypes() - self.rest_config_templates = f"{self.config_template}/rest/config/templates" - msg = f"ENTERED api.v1.ConfigTemplate.{self.class_name}" + self.templates = f"{self.config}/templates" + self._template_name = None + msg = "ENTERED api.v1.configtemplate.rest.config." + msg += f"templates.{self.class_name}" self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Set the fabric_name property. - """ - self.properties["template_name"] = None @property def path_template_name(self): @@ -65,7 +60,7 @@ def path_template_name(self): msg = f"{self.class_name}.{method_name}: " msg += "template_name must be set prior to accessing path." raise ValueError(msg) - return f"{self.rest_config_templates}/{self.template_name}" + return f"{self.templates}/{self.template_name}" @property def template_name(self): @@ -74,7 +69,7 @@ def template_name(self): - setter: Set the template_name. - setter: Raise ``ValueError`` if template_name is not a string. """ - return self.properties["template_name"] + return self._template_name @template_name.setter def template_name(self, value): @@ -85,7 +80,7 @@ def template_name(self, value): msg += "Expected one of: " msg += f"{', '.join(self.fabric_types.valid_fabric_template_names)}." raise ValueError(msg) - self.properties["template_name"] = value + self._template_name = value class EpTemplate(Templates): @@ -100,7 +95,7 @@ class EpTemplate(Templates): - ``ValueError``: If template_name is not a valid fabric template name. ### Path - - ``/rest/config/templates/{template_name}`` + - ``/api/v1/configtemplates/rest/config/templates/{template_name}`` ### Verb - GET @@ -126,14 +121,10 @@ def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.required_properties.add("template_name") - self._build_properties() - msg = f"ENTERED api.v1.ConfigTemplate.Templates.{self.class_name}" + msg = "ENTERED api.v1.configtemplate.rest.config." + msg += f"templates.Templates.{self.class_name}" self.log.debug(msg) - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - @property def path(self): """ @@ -142,6 +133,13 @@ def path(self): """ return self.path_template_name + @property + def verb(self): + """ + - Return the verb for the endpoint. + """ + return "GET" + class EpTemplates(Templates): """ @@ -154,7 +152,7 @@ class EpTemplates(Templates): - None ### Path - - ``/rest/config/templates`` + - ``/api/v1/configtemplates/rest/config/templates`` ### Verb - GET @@ -176,17 +174,20 @@ def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self._build_properties() - msg = f"ENTERED api.v1.ConfigTemplate.Templates.{self.class_name}" + msg = "ENTERED api.v1.configtemplate.rest.config." + msg += f"templates.Templates.{self.class_name}" self.log.debug(msg) - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - @property def path(self): """ - - Endpoint for template retrieval. - - Raise ``ValueError`` if template_name is not set. + - Return the path for the endpoint. + """ + return self.templates + + @property + def verb(self): + """ + - Return the verb for the endpoint. """ - return self.rest_config_templates + return "GET" diff --git a/plugins/module_utils/common/api/v1/fm.py b/plugins/module_utils/common/api/v1/fm/__init__.py similarity index 73% rename from plugins/module_utils/common/api/v1/fm.py rename to plugins/module_utils/common/api/v1/fm/__init__.py index 26d9da9a9..5eb69d5c3 100644 --- a/plugins/module_utils/common/api/v1/fm.py +++ b/plugins/module_utils/common/api/v1/fm/__init__.py @@ -19,16 +19,18 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common_v1 import \ - CommonV1 +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1 import \ + V1 -class FM(CommonV1): +class FM(V1): """ - ## V1 API Feature Manager (FM) - CommonV1().FM() + ## api.v1.fm.FM() ### Description Common methods and properties for FM() subclasses + + ### Path ``/appcenter/cisco/ndfc/api/v1/fm`` """ @@ -36,13 +38,13 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.fm = f"{self.api_v1}/fm" - self.log.debug("ENTERED api.v1.CommonV1()") + self.fm = f"{self.v1}/fm" + self.log.debug("ENTERED api.v1.fm.FM()") class EpFeatures(FM): """ - ## V1 API Feature Manager (FM) - FM().EpFeatures() + ## api.v1.fm.EpFeatures() ### Description Return endpoint information. @@ -51,7 +53,7 @@ class EpFeatures(FM): - None ### Path - ``/fm/features`` + ``/api/v1/fm/features`` ### Verb - GET @@ -72,17 +74,20 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._build_properties() - self.log.debug("ENTERED api.v1.fm.Features()") + self.log.debug("ENTERED api.v1.fm.EpFeatures()") - def _build_properties(self): - self.properties["path"] = f"{self.fm}/features" - self.properties["verb"] = "GET" + @property + def path(self): + return f"{self.fm}/features" + + @property + def verb(self): + return "GET" class EpVersion(FM): """ - ## V1 API Feature Manager (FM) about/version. + ## api.v1.fm.EpVersion() ### Description Return endpoint information. @@ -91,7 +96,7 @@ class EpVersion(FM): - None ### Path - ``/fm/about/version`` + ``/api/v1/fm/about/version`` ### Verb - GET @@ -112,9 +117,12 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._build_properties() - self.log.debug("ENTERED api.v1.fm.Version()") + self.log.debug("ENTERED api.v1.fm.EpVersion()") + + @property + def path(self): + return f"{self.fm}/about/version" - def _build_properties(self): - self.properties["path"] = f"{self.fm}/about/version" - self.properties["verb"] = "GET" + @property + def verb(self): + return "GET" diff --git a/plugins/module_utils/common/api/v1/image_management.py b/plugins/module_utils/common/api/v1/imagemanagement/__init__.py similarity index 85% rename from plugins/module_utils/common/api/v1/image_management.py rename to plugins/module_utils/common/api/v1/imagemanagement/__init__.py index 1a5ace0d2..8af146f9b 100644 --- a/plugins/module_utils/common/api/v1/image_management.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/__init__.py @@ -19,11 +19,11 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common_v1 import \ - CommonV1 +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1 import \ + V1 -class ImageManagement(CommonV1): +class ImageManagement(V1): """ ## V1 API - ImageManagement() @@ -38,5 +38,5 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.image_management = f"{self.api_v1}/imagemanagement" - self.log.debug("ENTERED api.v1.ImageManagement()") + self.imagemanagement = f"{self.v1}/imagemanagement" + self.log.debug("ENTERED api.v1.imagemanagement.ImageManagement()") diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/__init__.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/__init__.py new file mode 100644 index 000000000..b36e6d938 --- /dev/null +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/__init__.py @@ -0,0 +1,49 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement import \ + ImageManagement + + +class Rest(ImageManagement): + """ + ## api.v1.imagemanagement.rest.Rest() + + ### Description + Common methods and properties api.v1.imagemanagement.rest subclasses. + + ### Path + - ``/api/v1/imagemanagement/rest`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.rest = f"{self.imagemanagement}/rest" + msg = f"ENTERED api.v1.imagemanagement.rest.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Populate properties specific to this class and its subclasses. + """ diff --git a/plugins/module_utils/common/api/v1/rest/image_mgnt.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/__init__.py similarity index 76% rename from plugins/module_utils/common/api/v1/rest/image_mgnt.py rename to plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/__init__.py index e4e2706d8..1231a3cf6 100644 --- a/plugins/module_utils/common/api/v1/rest/image_mgnt.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/__init__.py @@ -19,13 +19,13 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.image_management import \ - ImageManagement +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest import \ + Rest -class ImageMgnt(ImageManagement): +class ImageMgnt(Rest): """ - ## V1 API - ImageManagement().ImageMgnt() + ## api.v1.imagemanagement.rest.imagemgt.ImageMgnt() ### Description Common methods and properties for ImageMgnt() subclasses @@ -38,13 +38,13 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.image_mgmt = f"{self.image_management}/rest/imagemgnt" - self.log.debug("ENTERED api.v1.ImageMgmt()") + self.image_mgmt = f"{self.rest}/imagemgnt" + self.log.debug("ENTERED api.v1.imagemanagement.rest.imagemgnt.ImageMgnt()") class EpBootFlashInfo(ImageMgnt): """ - ## V1 API - ImageMgnt().EpBootFlashInfo() + ## api.v1.imagemanagement.rest.imagemgnt.EpBootFlashInfo() ### Description Return endpoint information for bootflash-info. @@ -53,7 +53,7 @@ class EpBootFlashInfo(ImageMgnt): - None ### Path - - ``/rest/imagemgnt/bootFlash/bootflash-info`` + - ``/api/imagemanagement/rest/imagemgnt/bootFlash/bootflash-info`` ### Verb - GET @@ -75,8 +75,11 @@ def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED api.v1.ImageMgnt.EpBootFlash()") - self._build_properties() - def _build_properties(self): - self.properties["path"] = f"{self.image_mgmt}/bootFlash/bootflash-info" - self.properties["verb"] = "GET" + @property + def path(self): + return f"{self.image_mgmt}/bootFlash/bootflash-info" + + @property + def verb(self): + return "GET" diff --git a/plugins/module_utils/common/api/v1/rest/image_upgrade.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/imageupgrade/__init__.py similarity index 68% rename from plugins/module_utils/common/api/v1/rest/image_upgrade.py rename to plugins/module_utils/common/api/v1/imagemanagement/rest/imageupgrade/__init__.py index 94b4c0e55..1fe516d51 100644 --- a/plugins/module_utils/common/api/v1/rest/image_upgrade.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/imageupgrade/__init__.py @@ -19,27 +19,27 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.image_management import \ - ImageManagement +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest import \ + Rest -class ImageUpgrade(ImageManagement): +class ImageUpgrade(Rest): """ - ## V1 API Fabrics - ImageManagement().ImageUpgrade() + ## api.v1.imagemanagement.rest.imageupgrade.ImageUpgrade() ### Description Common methods and properties for ImageUpgrade() subclasses. ### Path - - ``/imagemanagement/rest/imageupgrade`` + - ``/api/v1/imagemanagement/rest/imageupgrade`` """ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.rest_image_upgrade = f"{self.image_management}/rest/imageupgrade" - msg = f"ENTERED api.v1.ImageManagement.{self.class_name}" + self.imageupgrade = f"{self.rest}/imageupgrade" + msg = f"ENTERED api.v1.imagemanagement.rest.{self.class_name}" self.log.debug(msg) self._build_properties() @@ -60,7 +60,7 @@ class EpInstallOptions(ImageUpgrade): - None ### Path - - ``/rest/imageupgrade/install-options`` + - ``/api/v1/imagemanagement/rest/imageupgrade/install-options`` ### Verb - POST @@ -81,14 +81,23 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._build_properties() - msg = f"ENTERED api.v1.ImageUpgrade.{self.class_name}" + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"imageupgrade.{self.class_name}" self.log.debug(msg) - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "POST" - self.properties["path"] = f"{self.rest_image_upgrade}/install-options" + @property + def path(self): + """ + - Return the path for the endpoint. + """ + return f"{self.imageupgrade}/install-options" + + @property + def verb(self): + """ + - Return the verb for the endpoint. + """ + return "POST" class EpUpgradeImage(ImageUpgrade): @@ -102,7 +111,7 @@ class EpUpgradeImage(ImageUpgrade): - None ### Path - - ``/rest/imageupgrade/upgrade-image`` + - ``/api/v1/imagemanagement/rest/imageupgrade/upgrade-image`` ### Verb - POST @@ -123,11 +132,20 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._build_properties() - msg = f"ENTERED api.v1.ImageUpgrade.{self.class_name}" + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"imageupgrade.{self.class_name}" self.log.debug(msg) - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "POST" - self.properties["path"] = f"{self.rest_image_upgrade}/upgrade-image" + @property + def path(self): + """ + - Return the path for the endpoint. + """ + return f"{self.imageupgrade}/upgrade-image" + + @property + def verb(self): + """ + - Return the verb for the endpoint. + """ + return "POST" diff --git a/plugins/module_utils/common/api/v1/rest/policy_mgnt.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/__init__.py similarity index 74% rename from plugins/module_utils/common/api/v1/rest/policy_mgnt.py rename to plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/__init__.py index 58ce994e8..e1cf739c9 100644 --- a/plugins/module_utils/common/api/v1/rest/policy_mgnt.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/__init__.py @@ -20,13 +20,13 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.image_management import \ - ImageManagement +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest import \ + Rest -class PolicyMgnt(ImageManagement): +class PolicyMgnt(Rest): """ - ## V1 API - ImageManagement().PolicyMgnt() + ## api.v1.imagemanagement.rest.policymgnt.PolicyMgnt() ### Description Common methods and properties for PolicyMgnt() subclasses @@ -39,13 +39,13 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.policy_mgmt = f"{self.image_management}/rest/policymgnt" + self.policymgnt = f"{self.rest}/policymgnt" self.log.debug("ENTERED api.v1.PolicyMgnt()") class EpPolicies(PolicyMgnt): """ - ## V1 API - PolicyMgnt().EpPolicies() + ## api.v1.imagemanagement.rest.policymgnt.EpPolicies() ### Description Return endpoint information. @@ -54,7 +54,7 @@ class EpPolicies(PolicyMgnt): - None ### Path - - ``/rest/policymgnt/policies`` + - ``/api/v1/imagemanagement/rest/policymgnt/policies`` ### Verb - GET @@ -75,12 +75,17 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.PolicyMgnt.EpPolicies()") - self._build_properties() + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) - def _build_properties(self): - self.properties["path"] = f"{self.policy_mgmt}/policies" - self.properties["verb"] = "GET" + @property + def path(self): + return f"{self.policymgnt}/policies" + + @property + def verb(self): + return "GET" class EpPoliciesAllAttached(PolicyMgnt): @@ -115,12 +120,17 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.PolicyMgnt.EpPoliciesAllAttached()") - self._build_properties() + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.policymgnt}/all-attached-policies" - def _build_properties(self): - self.properties["path"] = f"{self.policy_mgmt}/all-attached-policies" - self.properties["verb"] = "GET" + @property + def verb(self): + return "GET" class EpPolicyAttach(PolicyMgnt): @@ -155,12 +165,17 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.PolicyMgnt.EpPolicyAttach()") - self._build_properties() + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.policymgnt}/attach-policy" - def _build_properties(self): - self.properties["path"] = f"{self.policy_mgmt}/attach-policy" - self.properties["verb"] = "POST" + @property + def verb(self): + return "POST" class EpPolicyCreate(PolicyMgnt): @@ -195,12 +210,17 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.PolicyMgnt.EpPolicyCreate()") - self._build_properties() + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) - def _build_properties(self): - self.properties["path"] = f"{self.policy_mgmt}/platform-policy" - self.properties["verb"] = "POST" + @property + def path(self): + return f"{self.policymgnt}/platform-policy" + + @property + def verb(self): + return "POST" class EpPolicyDetach(PolicyMgnt): @@ -235,12 +255,17 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.PolicyMgnt.EpPolicyDetach()") - self._build_properties() + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) - def _build_properties(self): - self.properties["path"] = f"{self.policy_mgmt}/detach-policy" - self.properties["verb"] = "DELETE" + @property + def path(self): + return f"{self.policymgnt}/detach-policy" + + @property + def verb(self): + return "DELETE" class EpPolicyInfo(PolicyMgnt): @@ -279,13 +304,10 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.PolicyMgnt.EpPolicyDetach()") - self._build_properties() - - def _build_properties(self): - self.properties["policy_name"] = None - self.properties["path"] = f"{self.policy_mgmt}/image-policy" - self.properties["verb"] = "GET" + self._policy_name = None + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) @property def path(self): @@ -295,7 +317,11 @@ def path(self): msg += f"{self.class_name}.policy_name must be set before " msg += f"accessing {method_name}." raise ValueError(msg) - return f"{self.properties['path']}/{self.policy_name}" + return f"{self.policymgnt}/image-policy/{self.policy_name}" + + @property + def verb(self): + return "GET" @property def policy_name(self): @@ -303,8 +329,8 @@ def policy_name(self): - getter: Return the policy_name. - setter: Set the policy_name. """ - return self.properties["policy_name"] + return self._policy_name @policy_name.setter def policy_name(self, value): - self.properties["policy_name"] = value + self._policy_name = value diff --git a/plugins/module_utils/common/api/v1/rest/staging_management.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/stagingmanagement/__init__.py similarity index 65% rename from plugins/module_utils/common/api/v1/rest/staging_management.py rename to plugins/module_utils/common/api/v1/imagemanagement/rest/stagingmanagement/__init__.py index faf2e2994..8d59b6264 100644 --- a/plugins/module_utils/common/api/v1/rest/staging_management.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/stagingmanagement/__init__.py @@ -19,32 +19,34 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.image_management import \ - ImageManagement +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest import \ + Rest -class StagingManagement(ImageManagement): +class StagingManagement(Rest): """ - ## V1 API - ImageManagement().StagingManagement() + ## api.v1.imagemanagement.rest.stagingmanagement.StagingManagement() ### Description Common methods and properties for StagingManagement() subclasses ### Path - ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/stagingmanagement`` + ``/api/v1/imagemanagement/rest/stagingmanagement`` """ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.staging_management = f"{self.image_management}/rest/stagingmanagement" - self.log.debug("ENTERED api.v1.StagingManagement()") + self.stagingmanagement = f"{self.rest}/stagingmanagement" + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"stagingmanagement.{self.class_name}" + self.log.debug(msg) class EpImageStage(StagingManagement): """ - ## V1 API - StagingManagement().EpImageStage() + ## api.v1.imagemanagement.rest.stagingmanagement.EpImageStage() ### Description Return endpoint information. @@ -53,7 +55,7 @@ class EpImageStage(StagingManagement): - None ### Path - - ``/rest/stagingmanagement/stage-image`` + - ``/api/v1/imagemanagement/rest/stagingmanagement/stage-image`` ### Verb - POST @@ -74,12 +76,17 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.StagingManagement.EpImageStage()") - self._build_properties() + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"stagingmanagement.{self.class_name}" + self.log.debug(msg) - def _build_properties(self): - self.properties["path"] = f"{self.staging_management}/stage-image" - self.properties["verb"] = "POST" + @property + def path(self): + return f"{self.stagingmanagement}/stage-image" + + @property + def verb(self): + return "POST" class EpImageValidate(StagingManagement): @@ -93,7 +100,7 @@ class EpImageValidate(StagingManagement): - None ### Path - - ``/rest/stagingmanagement/validate-image`` + - ``/api/v1/imagemanagement/rest/stagingmanagement/validate-image`` ### Verb - POST @@ -114,12 +121,17 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.StagingManagement.EpImageValidate()") - self._build_properties() + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"stagingmanagement.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.stagingmanagement}/validate-image" - def _build_properties(self): - self.properties["path"] = f"{self.staging_management}/validate-image" - self.properties["verb"] = "POST" + @property + def verb(self): + return "POST" class EpStageInfo(StagingManagement): @@ -133,7 +145,7 @@ class EpStageInfo(StagingManagement): - None ### Path - - ``/rest/stagingmanagement/stage-info`` + - ``/api/v1/imagemanagement/rest/stagingmanagement/stage-info`` ### Verb - GET @@ -154,9 +166,14 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.StagingManagement.EpStageInfo()") - self._build_properties() + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"stagingmanagement.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.stagingmanagement}/stage-info" - def _build_properties(self): - self.properties["path"] = f"{self.staging_management}/stage-info" - self.properties["verb"] = "GET" + @property + def verb(self): + return "GET" diff --git a/plugins/module_utils/common/api/v1/lan_fabric.py b/plugins/module_utils/common/api/v1/lan_fabric/__init__.py similarity index 79% rename from plugins/module_utils/common/api/v1/lan_fabric.py rename to plugins/module_utils/common/api/v1/lan_fabric/__init__.py index 2bbc95f61..1f1478656 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/__init__.py @@ -19,16 +19,16 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.common_v1 import \ - CommonV1 +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1 import \ + V1 -class LanFabric(CommonV1): +class LanFabric(V1): """ - ## V1 API - LanFabric() + ## api.v1.lan-fabric.LanFabric() ### Description - Common methods and properties for CommonV1().LanFabric() subclasses + Common methods and properties for api.v1.lan-fabric.LanFabric() subclasses ### Path ``/appcenter/cisco/ndfc/api/v1/lan-fabric`` @@ -38,5 +38,5 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.lan_fabric = f"{self.api_v1}/lan-fabric" - self.log.debug("ENTERED api.v1.LanFabric()") + self.lan_fabric = f"{self.v1}/lan-fabric" + self.log.debug("ENTERED api.v1.lan-fabric.LanFabric()") diff --git a/plugins/module_utils/common/api/v1/common_v1.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/__init__.py similarity index 72% rename from plugins/module_utils/common/api/v1/common_v1.py rename to plugins/module_utils/common/api/v1/lan_fabric/rest/__init__.py index ef3251384..4d4a9db9e 100644 --- a/plugins/module_utils/common/api/v1/common_v1.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/__init__.py @@ -11,7 +11,7 @@ # 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. - +# pylint: disable=line-too-long from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -19,24 +19,25 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.common import \ - Common +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric import \ + LanFabric -class CommonV1(Common): +class Rest(LanFabric): """ - ## v1 API enpoints - Common().CommonV1() + ## api.v1.lan_fabric.rest.Rest() ### Description - Common methods and properties for API v1 subclasses. + Common methods and properties for api.v1.lan_fabric.rest.Rest() subclasses. ### Path - ``/appcenter/cisco/ndfc/api/v1/`` + - ``/api/v1/lan-fabric/rest`` """ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.CommonV1()") - self.api_v1 = f"{self.api}/v1" + self.rest = f"{self.lan_fabric}/rest" + msg = f"ENTERED api.v1.lan_fabric.rest.{self.class_name}" + self.log.debug(msg) diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/__init__.py new file mode 100644 index 000000000..e77793236 --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/__init__.py @@ -0,0 +1,43 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest import \ + Rest + + +class Control(Rest): + """ + ## api.v1.lan_fabric.rest.control.Control() + + ### Description + Common methods and properties for Control() subclasses. + + ### Path + - ``/api/v1/lan-fabric/rest/control`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.control = f"{self.rest}/control" + msg = f"ENTERED api.v1.lan_fabric.rest.control.{self.class_name}" + self.log.debug(msg) diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/__init__.py new file mode 100644 index 000000000..47f34af53 --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/__init__.py @@ -0,0 +1,665 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control import \ + Control +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ + FabricTypes + + +class Fabrics(Control): + """ + ## api.v1.lan-fabric.rest.control.fabrics.Fabrics() + + ### Description + Common methods and properties for Fabrics() subclasses. + + ### Path + - ``/api/v1/lan-fabric/rest/control/fabrics`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fabric_types = FabricTypes() + self.fabrics = f"{self.control}/fabrics" + msg = f"ENTERED api.v1.lan_fabric.rest.control.fabrics.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Set the fabric_name property. + """ + self.properties["fabric_name"] = None + self.properties["template_name"] = None + + @property + def fabric_name(self): + """ + - getter: Return the fabric_name. + - setter: Set the fabric_name. + - setter: Raise ``ValueError`` if fabric_name is not valid. + """ + return self.properties["fabric_name"] + + @fabric_name.setter + def fabric_name(self, value): + method_name = inspect.stack()[0][3] + try: + self.conversion.validate_fabric_name(value) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) from error + self.properties["fabric_name"] = value + + @property + def path_fabric_name(self): + """ + - Endpoint path property, including fabric_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.fabrics}/{self.fabric_name}" + + @property + def path_fabric_name_template_name(self): + """ + - Endpoint path property, including fabric_name and template_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + - Raise ``ValueError`` if template_name is not set and + ``self.required_properties`` contains "template_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + if self.template_name is None and "template_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "template_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.fabrics}/{self.fabric_name}/{self.template_name}" + + @property + def template_name(self): + """ + - getter: Return the template_name. + - setter: Set the template_name. + - setter: Raise ``ValueError`` if template_name is not a string. + """ + return self.properties["template_name"] + + @template_name.setter + def template_name(self, value): + method_name = inspect.stack()[0][3] + if value not in self.fabric_types.valid_fabric_template_names: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid template_name: {value}. " + msg += "Expected one of: " + msg += f"{', '.join(self.fabric_types.valid_fabric_template_names)}." + raise ValueError(msg) + self.properties["template_name"] = value + + +class EpFabricConfigDeploy(Fabrics): + """ + ## api.v1.lan-fabric.rest.control.fabrics.EpFabricConfigDeploy() + + ### Description + Return endpoint to initiate config-deploy on fabric_name. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If force_show_run is not boolean. + - ``ValueError``: If include_all_msd_switches is not boolean. + + ### Path + - ``/fabrics/{fabric_name}/config-deploy`` + - ``/fabrics/{fabric_name}/config-deploy?forceShowRun={force_show_run}`` + - ``/fabrics/{fabric_name}/config-deploy?inclAllMSDSwitches={include_all_msd_switches}`` + + ### Verb + - POST + + ### Parameters + - force_show_run: boolean + - set the ``forceShowRun`` value + - default: False + - include_all_msd_switches: boolean + - set the ``inclAllMSDSwitches`` value + - default: False + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricConfigDeploy() + instance.fabric_name = "MyFabric" + instance.force_show_run = True + instance.include_all_msd_switches = True + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + self.properties["force_show_run"] = False + self.properties["include_all_msd_switches"] = False + + @property + def force_show_run(self): + """ + - getter: Return the force_show_run value. + - setter: Set the force_show_run value. + - setter: Raise ``ValueError`` if force_show_run is not a boolean. + - Default: False + """ + return self.properties["force_show_run"] + + @force_show_run.setter + def force_show_run(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected boolean for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["force_show_run"] = value + + @property + def include_all_msd_switches(self): + """ + - getter: Return the include_all_msd_switches. + - setter: Set the include_all_msd_switches. + - setter: Raise ``ValueError`` if include_all_msd_switches is a boolean. + - Default: False + """ + return self.properties["include_all_msd_switches"] + + @include_all_msd_switches.setter + def include_all_msd_switches(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected boolean for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["include_all_msd_switches"] = value + + @property + def path(self): + """ + - Override the path property to mandate fabric_name is set. + - Raise ``ValueError`` if fabric_name is not set. + """ + _path = self.path_fabric_name + _path += "/config-deploy?" + _path += f"forceShowRun={self.force_show_run}" + _path += f"&inclAllMSDSwitches={self.include_all_msd_switches}" + return _path + + +class EpFabricConfigSave(Fabrics): + """ + ## V1 API - Fabrics().EpFabricConfigSave() + + ### Description + Return endpoint to initiate config-save on fabric_name. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If ticket_id is not a string. + + ### Path + - ``/fabrics/{fabric_name}/config-save`` + - ``/fabrics/{fabric_name}/config-save?ticketId={ticket_id}`` + + ### Verb + - POST + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - ticket_id: string + - optional unless Change Control is enabled + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricConfigSave() + instance.fabric_name = "MyFabric" + instance.ticket_id = "MyTicket1234" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + self.properties["ticket_id"] = None + + @property + def ticket_id(self): + """ + - getter: Return the ticket_id. + - setter: Set the ticket_id. + - setter: Raise ``ValueError`` if ticket_id is not a string. + - Default: None + - Note: ticket_id is optional unless Change Control is enabled. + """ + return self.properties["ticket_id"] + + @ticket_id.setter + def ticket_id(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected string for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["ticket_id"] = value + + @property + def path(self): + """ + - Endpoint for config-save. + - Set self.ticket_id if Change Control is enabled. + - Raise ``ValueError`` if fabric_name is not set. + """ + _path = self.path_fabric_name + _path += "/config-save" + if self.ticket_id: + _path += f"?ticketId={self.ticket_id}" + return _path + + +class EpFabricCreate(Fabrics): + """ + ## V1 API - Fabrics().EpFabricCreate() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If template_name is not set. + - ``ValueError``: If template_name is not a valid fabric template name. + + ### Path + - ``/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` + + ### Verb + - POST + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - template_name: string + - set the ``template_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricCreate() + instance.fabric_name = "MyFabric" + instance.template_name = "Easy_Fabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self.required_properties.add("template_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + + @property + def path(self): + """ + - Endpoint for fabric create. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name_template_name + + +class EpFabricDelete(Fabrics): + """ + ## V1 API - Fabrics().EpFabricDelete() + + ### Description + Return endpoint to delete ``fabric_name``. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}`` + + ### Verb + - DELETE + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "DELETE" + + @property + def path(self): + """ + - Endpoint for fabric delete. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name + + +class EpFabricDetails(Fabrics): + """ + ## V1 API - Fabrics().EpFabricDetails() + + ### Description + Return the endpoint to query ``fabric_name`` details. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return self.path_fabric_name + + +class EpFabricFreezeMode(Fabrics): + """ + ## V1 API - Fabrics().EpFabricFreezeMode() + + ### Description + Return the endpoint to query ``fabric_name`` freezemode status. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}/freezemode`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return f"{self.path_fabric_name}/freezemode" + + +# class EpFabricSummary() See module_utils/common/api/v1/rest/control/switches.py + + +class EpFabricUpdate(Fabrics): + """ + ## V1 API - Fabrics().EpFabricUpdate() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If template_name is not set. + - ``ValueError``: If template_name is not a valid fabric template name. + + ### Path + ``/api/v1/lan-fabric/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` + + ### Verb + - PUT + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - template_name: string + - set the ``template_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricUpdate() + instance.fabric_name = "MyFabric" + instance.template_name = "Easy_Fabric_IPFM" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self.required_properties.add("template_name") + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + """ + - Endpoint for fabric create. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name_template_name + + @property + def verb(self): + return "PUT" + + +class EpFabrics(Fabrics): + """ + ## V1 API - Fabrics().EpFabrics() + + ### Description + Return the endpoint to query fabrics. + + ### Raises + - None + + ### Path + - ``/api/v1/lan-fabric/rest/control/fabrics`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabrics() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return self.fabrics diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/__init__.py new file mode 100644 index 000000000..450c3eb7d --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/__init__.py @@ -0,0 +1,141 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control import \ + Control + + +class Switches(Control): + """ + ## api.v1.lan_fabric.rest.control.switches.Switches() + + ### Description + Common methods and properties for Switches() subclasses. + + ### Path + - ``/api/v1/lan-fabric/rest/control/switches/{fabric_name}`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.switches = f"{self.control}/switches" + msg = f"ENTERED api.v1.lan_fabric.rest.control.switches.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + Populate properties specific to this class and its subclasses. + """ + self.properties["fabric_name"] = None + + @property + def fabric_name(self): + """ + - getter: Return the fabric_name. + - setter: Set the fabric_name. + - setter: Raise ``ValueError`` if fabric_name is not valid. + """ + return self.properties["fabric_name"] + + @fabric_name.setter + def fabric_name(self, value): + method_name = inspect.stack()[0][3] + try: + self.conversion.validate_fabric_name(value) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) from error + self.properties["fabric_name"] = value + + @property + def path_fabric_name(self): + """ + - Endpoint path property, including fabric_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.switches}/{self.fabric_name}" + + +class EpFabricSummary(Switches): + """ + ##api.v1.lan_fabric.rest.control.switches.EpFabricSummary() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/api/v1/lan-fabric/rest/control/switches/{fabric_name}/overview`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricSummary() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.switches." + msg += f"{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + """ + - Override the path property to mandate fabric_name is set. + - Raise ``ValueError`` if fabric_name is not set. + """ + return f"{self.path_fabric_name}/overview" diff --git a/plugins/module_utils/common/api/v1/rest/__init__.py b/plugins/module_utils/common/api/v1/rest/__init__.py index e69de29bb..6036e1ead 100644 --- a/plugins/module_utils/common/api/v1/rest/__init__.py +++ b/plugins/module_utils/common/api/v1/rest/__init__.py @@ -0,0 +1,49 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1 import \ + V1 + + +class Rest(V1): + """ + ## V1 API Rest() - api.v1.rest.Rest() + + ### Description + Common methods and properties for Rest() subclasses. + + ### Path + - ``/api/v1/rest`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.rest = f"{self.v1}/rest" + msg = f"ENTERED api.v1.rest.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Populate class-specific properties. + """ diff --git a/plugins/module_utils/common/api/v1/rest/control/__init__.py b/plugins/module_utils/common/api/v1/rest/control/__init__.py index e69de29bb..0c77bea28 100644 --- a/plugins/module_utils/common/api/v1/rest/control/__init__.py +++ b/plugins/module_utils/common/api/v1/rest/control/__init__.py @@ -0,0 +1,49 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest import \ + Rest + + +class Control(Rest): + """ + ## V1 API Control() - api.v1.rest.control.Control() + + ### Description + Common methods and properties for Control() subclasses. + + ### Path + - ``/api/v1/rest/control`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.control = f"{self.rest}/control" + msg = f"ENTERED api.v1.LanFabric.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Set the fabric_name property. + """ diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics/__init__.py similarity index 97% rename from plugins/module_utils/common/api/v1/rest/control/fabrics.py rename to plugins/module_utils/common/api/v1/rest/control/fabrics/__init__.py index 986e4ea5f..bdfa06605 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics/__init__.py @@ -20,21 +20,21 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric import \ - LanFabric +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control import \ + Control from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ FabricTypes -class Fabrics(LanFabric): +class Fabrics(Control): """ - ## V1 API Fabrics - LanFabric().Fabrics() + ## V1 API Fabrics - api.v1.rest.control.fabrics.Fabrics() ### Description Common methods and properties for Fabrics() subclasses. ### Path - - ``/lan-fabric/rest/control/fabrics/{fabric_name}`` + - ``/rest/control/fabrics`` """ def __init__(self): @@ -42,8 +42,8 @@ def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.fabric_types = FabricTypes() - self.rest_control_fabrics = f"{self.lan_fabric}/rest/control/fabrics" - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.fabrics = f"{self.control}/fabrics" + msg = f"ENTERED api.v1.rest.control.fabrics.{self.class_name}" self.log.debug(msg) self._build_properties() @@ -86,7 +86,7 @@ def path_fabric_name(self): msg = f"{self.class_name}.{method_name}: " msg += "fabric_name must be set prior to accessing path." raise ValueError(msg) - return f"{self.rest_control_fabrics}/{self.fabric_name}" + return f"{self.fabrics}/{self.fabric_name}" @property def path_fabric_name_template_name(self): @@ -106,7 +106,7 @@ def path_fabric_name_template_name(self): msg = f"{self.class_name}.{method_name}: " msg += "template_name must be set prior to accessing path." raise ValueError(msg) - return f"{self.rest_control_fabrics}/{self.fabric_name}/{self.template_name}" + return f"{self.fabrics}/{self.fabric_name}/{self.template_name}" @property def template_name(self): @@ -655,4 +655,4 @@ def _build_properties(self): @property def path(self): - return self.rest_control_fabrics + return self.fabrics diff --git a/plugins/module_utils/common/api/v1/rest/control/switches.py b/plugins/module_utils/common/api/v1/rest/control/switches/__init__.py similarity index 88% rename from plugins/module_utils/common/api/v1/rest/control/switches.py rename to plugins/module_utils/common/api/v1/rest/control/switches/__init__.py index 1d67d383d..7da36b32c 100644 --- a/plugins/module_utils/common/api/v1/rest/control/switches.py +++ b/plugins/module_utils/common/api/v1/rest/control/switches/__init__.py @@ -20,13 +20,13 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric import \ - LanFabric +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control import \ + Control -class Switches(LanFabric): +class Switches(Control): """ - ## V1 API Fabrics - LanFabric().Switches() + ## V1 API Switches() - api.v1.rest.control.switches.Switches() ### Description Common methods and properties for Fabrics() subclasses. @@ -39,14 +39,14 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.rest_control_switches = f"{self.lan_fabric}/rest/control/switches" - msg = f"ENTERED api.v1.LanFabric.Switches.{self.class_name}" + self.switches = f"{self.control}/switches" + msg = f"ENTERED api.v1.rest.control.switches.{self.class_name}" self.log.debug(msg) self._build_properties() def _build_properties(self): """ - - Set the fabric_name property. + Populate properties specific to this class and its subclasses. """ self.properties["fabric_name"] = None @@ -82,12 +82,12 @@ def path_fabric_name(self): msg = f"{self.class_name}.{method_name}: " msg += "fabric_name must be set prior to accessing path." raise ValueError(msg) - return f"{self.rest_control_switches}/{self.fabric_name}" + return f"{self.switches}/{self.fabric_name}" class EpFabricSummary(Switches): """ - ## V1 API - Switches().EpFabricSummary() + ## V1 API - api.v1.rest.control.switches.EpFabricSummary() ### Description Return endpoint information. @@ -124,7 +124,7 @@ def __init__(self): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.required_properties.add("fabric_name") self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Switches.{self.class_name}" + msg = f"ENTERED api.v1.rest.control.switches.{self.class_name}" self.log.debug(msg) def _build_properties(self): diff --git a/plugins/module_utils/fabric/config_deploy.py b/plugins/module_utils/fabric/config_deploy.py index 4e0958e5e..9d5975c37 100644 --- a/plugins/module_utils/fabric/config_deploy.py +++ b/plugins/module_utils/fabric/config_deploy.py @@ -22,7 +22,7 @@ import logging from typing import Dict -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabricConfigDeploy from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/plugins/module_utils/fabric/config_save.py b/plugins/module_utils/fabric/config_save.py index 65166fc53..9c0b65fe8 100644 --- a/plugins/module_utils/fabric/config_save.py +++ b/plugins/module_utils/fabric/config_save.py @@ -22,7 +22,7 @@ import logging from typing import Dict -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabricConfigSave from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/plugins/module_utils/fabric/create.py b/plugins/module_utils/fabric/create.py index d03984dfe..33f11d2f3 100644 --- a/plugins/module_utils/fabric/create.py +++ b/plugins/module_utils/fabric/create.py @@ -23,7 +23,7 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabricCreate from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.common import \ FabricCommon diff --git a/plugins/module_utils/fabric/delete.py b/plugins/module_utils/fabric/delete.py index 2f9620de0..96c992b54 100644 --- a/plugins/module_utils/fabric/delete.py +++ b/plugins/module_utils/fabric/delete.py @@ -20,7 +20,7 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabricDelete from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError diff --git a/plugins/module_utils/fabric/fabric_details.py b/plugins/module_utils/fabric/fabric_details.py index 6b47aae6f..23a1416d1 100644 --- a/plugins/module_utils/fabric/fabric_details.py +++ b/plugins/module_utils/fabric/fabric_details.py @@ -22,7 +22,7 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/plugins/module_utils/fabric/fabric_summary.py b/plugins/module_utils/fabric/fabric_summary.py index d96ef3d64..0cd82f210 100644 --- a/plugins/module_utils/fabric/fabric_summary.py +++ b/plugins/module_utils/fabric/fabric_summary.py @@ -23,7 +23,7 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.switches import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.switches import \ EpFabricSummary from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/plugins/module_utils/fabric/replaced.py b/plugins/module_utils/fabric/replaced.py index 153e4daa4..2c72b4e3f 100644 --- a/plugins/module_utils/fabric/replaced.py +++ b/plugins/module_utils/fabric/replaced.py @@ -23,7 +23,7 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabricUpdate from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError diff --git a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py index d48f2ed6e..be35238b1 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py +++ b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py @@ -18,7 +18,7 @@ import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import ( EpFabricConfigDeploy, EpFabricConfigSave, EpFabricCreate, EpFabricDelete, EpFabricDetails, EpFabricFreezeMode, EpFabricUpdate) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ diff --git a/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py b/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py index 4c408d275..63e0b515d 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py +++ b/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py @@ -17,7 +17,7 @@ __metaclass__ = type -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.image_mgnt import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.imagemgnt import \ EpBootFlashInfo from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise diff --git a/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py b/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py index 424f72509..2556322d2 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py +++ b/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py @@ -17,7 +17,7 @@ __metaclass__ = type -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.image_upgrade import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.imageupgrade import ( EpInstallOptions, EpUpgradeImage) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise diff --git a/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py b/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py index e6d6a79a2..72608daf8 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py +++ b/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py @@ -18,7 +18,7 @@ import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.policy_mgnt import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.policymgnt import ( EpPolicies, EpPoliciesAllAttached, EpPolicyAttach, EpPolicyCreate, EpPolicyDetach, EpPolicyInfo) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ diff --git a/tests/unit/module_utils/common/api/test_v1_api_staging_management.py b/tests/unit/module_utils/common/api/test_v1_api_staging_management.py index 5000cd500..8635a0d59 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_staging_management.py +++ b/tests/unit/module_utils/common/api/test_v1_api_staging_management.py @@ -17,7 +17,7 @@ __metaclass__ = type -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.staging_management import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.stagingmanagement import ( EpImageStage, EpImageValidate, EpStageInfo) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise diff --git a/tests/unit/module_utils/common/api/test_v1_api_switches.py b/tests/unit/module_utils/common/api/test_v1_api_switches.py index 3ec0e2c70..85d1f3161 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_switches.py +++ b/tests/unit/module_utils/common/api/test_v1_api_switches.py @@ -18,7 +18,7 @@ import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.switches import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.switches import \ EpFabricSummary from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise diff --git a/tests/unit/module_utils/common/api/test_v1_api_templates.py b/tests/unit/module_utils/common/api/test_v1_api_templates.py index 42080e826..4728677b5 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_templates.py +++ b/tests/unit/module_utils/common/api/test_v1_api_templates.py @@ -18,7 +18,7 @@ import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.config.templates import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest.config.templates import ( EpTemplate, EpTemplates) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py index 7d997f8c1..e1b75e8f5 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabricConfigDeploy from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py index 82bbc4cf1..bbbe55d11 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabricConfigSave from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py index e8997fe4f..6327982fa 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabricCreate from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py index 5bef60741..564b82f08 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabricDelete from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py index 263a66f73..0b54e58e1 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py index 33e37a07f..fb4b19b37 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py index 6a3c6ea28..305a9bb48 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py index 268420fdf..fdbf6264b 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ EpFabricUpdate from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py index 7aaa0f9a9..a929dd62f 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.switches import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.switches import \ EpFabricSummary from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils From bfd3cae0d977d94a7b7830cbaaf07b2c24c56d98 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 16 May 2024 15:48:52 -1000 Subject: [PATCH 049/230] Fix empy-init errors --- plugins/module_utils/common/api/__init__.py | 65 -- plugins/module_utils/common/api/api.py | 65 ++ .../module_utils/common/api/v1/__init__.py | 41 -- .../common/api/v1/configtemplate/__init__.py | 42 -- .../api/v1/configtemplate/configtemplate.py | 42 ++ .../api/v1/configtemplate/rest/__init__.py | 49 -- .../v1/configtemplate/rest/config/__init__.py | 49 -- .../v1/configtemplate/rest/config/config.py | 49 ++ .../rest/config/templates/__init__.py | 193 ----- .../rest/config/templates/templates.py | 193 +++++ .../common/api/v1/configtemplate/rest/rest.py | 49 ++ .../module_utils/common/api/v1/fm/__init__.py | 128 ---- plugins/module_utils/common/api/v1/fm/fm.py | 128 ++++ .../common/api/v1/imagemanagement/__init__.py | 42 -- .../api/v1/imagemanagement/imagemanagement.py | 42 ++ .../api/v1/imagemanagement/rest/__init__.py | 49 -- .../rest/imagemgnt/__init__.py | 85 --- .../rest/imagemgnt/imagemgnt.py | 85 +++ .../rest/imageupgrade/__init__.py | 151 ---- .../rest/imageupgrade/imageupgrade.py | 151 ++++ .../rest/policymgnt/__init__.py | 336 --------- .../rest/policymgnt/policymgnt.py | 336 +++++++++ .../api/v1/imagemanagement/rest/rest.py | 49 ++ .../rest/stagingmanagement/__init__.py | 179 ----- .../stagingmanagement/stagingmanagement.py | 179 +++++ .../common/api/v1/lan_fabric/__init__.py | 42 -- .../common/api/v1/lan_fabric/lan_fabric.py | 42 ++ .../common/api/v1/lan_fabric/rest/__init__.py | 43 -- .../v1/lan_fabric/rest/control/__init__.py | 43 -- .../api/v1/lan_fabric/rest/control/control.py | 43 ++ .../rest/control/fabrics/__init__.py | 665 ------------------ .../rest/control/fabrics/fabrics.py | 665 ++++++++++++++++++ .../rest/control/switches/__init__.py | 141 ---- .../rest/control/switches/switches.py | 141 ++++ .../common/api/v1/lan_fabric/rest/rest.py | 43 ++ .../common/api/v1/rest/__init__.py | 49 -- .../common/api/v1/rest/control/__init__.py | 49 -- .../common/api/v1/rest/control/control.py | 49 ++ .../api/v1/rest/control/fabrics/__init__.py | 658 ----------------- .../api/v1/rest/control/fabrics/fabrics.py | 658 +++++++++++++++++ .../api/v1/rest/control/switches/__init__.py | 140 ---- .../api/v1/rest/control/switches/switches.py | 140 ++++ .../module_utils/common/api/v1/rest/rest.py | 49 ++ plugins/module_utils/common/api/v1/v1.py | 41 ++ .../common/controller_features.py | 2 +- plugins/module_utils/fabric/config_deploy.py | 2 +- plugins/module_utils/fabric/config_save.py | 2 +- plugins/module_utils/fabric/create.py | 2 +- plugins/module_utils/fabric/delete.py | 2 +- plugins/module_utils/fabric/fabric_details.py | 2 +- plugins/module_utils/fabric/fabric_summary.py | 2 +- plugins/module_utils/fabric/replaced.py | 2 +- .../common/api/test_v1_api_fabrics.py | 2 +- .../common/api/test_v1_api_image_mgnt.py | 2 +- .../api/test_v1_api_image_upgrade_ep.py | 2 +- .../common/api/test_v1_api_policy_mgnt.py | 2 +- .../api/test_v1_api_staging_management.py | 2 +- .../common/api/test_v1_api_switches.py | 2 +- .../common/api/test_v1_api_templates.py | 2 +- .../common/test_controller_features.py | 2 +- .../dcnm_fabric/test_fabric_config_deploy.py | 2 +- .../dcnm_fabric/test_fabric_config_save.py | 2 +- .../dcnm_fabric/test_fabric_create_common.py | 2 +- .../dcnm/dcnm_fabric/test_fabric_delete.py | 2 +- .../dcnm/dcnm_fabric/test_fabric_details.py | 2 +- .../test_fabric_details_by_name.py | 2 +- .../test_fabric_details_by_nv_pair.py | 2 +- .../dcnm_fabric/test_fabric_replaced_bulk.py | 2 +- .../dcnm/dcnm_fabric/test_fabric_summary.py | 2 +- 69 files changed, 3264 insertions(+), 3264 deletions(-) create mode 100644 plugins/module_utils/common/api/api.py create mode 100644 plugins/module_utils/common/api/v1/configtemplate/configtemplate.py create mode 100644 plugins/module_utils/common/api/v1/configtemplate/rest/config/config.py create mode 100644 plugins/module_utils/common/api/v1/configtemplate/rest/config/templates/templates.py create mode 100644 plugins/module_utils/common/api/v1/configtemplate/rest/rest.py create mode 100644 plugins/module_utils/common/api/v1/fm/fm.py create mode 100644 plugins/module_utils/common/api/v1/imagemanagement/imagemanagement.py create mode 100644 plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/imagemgnt.py create mode 100644 plugins/module_utils/common/api/v1/imagemanagement/rest/imageupgrade/imageupgrade.py create mode 100644 plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py create mode 100644 plugins/module_utils/common/api/v1/imagemanagement/rest/rest.py create mode 100644 plugins/module_utils/common/api/v1/imagemanagement/rest/stagingmanagement/stagingmanagement.py create mode 100644 plugins/module_utils/common/api/v1/lan_fabric/lan_fabric.py create mode 100644 plugins/module_utils/common/api/v1/lan_fabric/rest/control/control.py create mode 100644 plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py create mode 100644 plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/switches.py create mode 100644 plugins/module_utils/common/api/v1/lan_fabric/rest/rest.py create mode 100644 plugins/module_utils/common/api/v1/rest/control/control.py create mode 100644 plugins/module_utils/common/api/v1/rest/control/fabrics/fabrics.py create mode 100644 plugins/module_utils/common/api/v1/rest/control/switches/switches.py create mode 100644 plugins/module_utils/common/api/v1/rest/rest.py create mode 100644 plugins/module_utils/common/api/v1/v1.py diff --git a/plugins/module_utils/common/api/__init__.py b/plugins/module_utils/common/api/__init__.py index e56077a5c..e69de29bb 100644 --- a/plugins/module_utils/common/api/__init__.py +++ b/plugins/module_utils/common/api/__init__.py @@ -1,65 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ - ConversionUtils - - -class Api: - """ - ## API endpoints - Api() - - ### Description - Common methods and properties for Api() subclasses. - - ### Path - ``/appcenter/cisco/ndfc/api`` - """ - - def __init__(self): - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.conversion = ConversionUtils() - # Popuate in subclasses to indicate which properties - # are mandatory for the subclass. - self.required_properties = set() - self.log.debug("ENTERED api.Api()") - self.api = "/appcenter/cisco/ndfc/api" - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["path"] = None - self.properties["verb"] = None - - @property - def path(self): - """ - Return the endpoint path. - """ - return self.properties["path"] - - @property - def verb(self): - """ - Return the endpoint verb. - """ - return self.properties["verb"] diff --git a/plugins/module_utils/common/api/api.py b/plugins/module_utils/common/api/api.py new file mode 100644 index 000000000..e56077a5c --- /dev/null +++ b/plugins/module_utils/common/api/api.py @@ -0,0 +1,65 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils + + +class Api: + """ + ## API endpoints - Api() + + ### Description + Common methods and properties for Api() subclasses. + + ### Path + ``/appcenter/cisco/ndfc/api`` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.conversion = ConversionUtils() + # Popuate in subclasses to indicate which properties + # are mandatory for the subclass. + self.required_properties = set() + self.log.debug("ENTERED api.Api()") + self.api = "/appcenter/cisco/ndfc/api" + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["path"] = None + self.properties["verb"] = None + + @property + def path(self): + """ + Return the endpoint path. + """ + return self.properties["path"] + + @property + def verb(self): + """ + Return the endpoint verb. + """ + return self.properties["verb"] diff --git a/plugins/module_utils/common/api/v1/__init__.py b/plugins/module_utils/common/api/v1/__init__.py index 06e73ad73..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/__init__.py +++ b/plugins/module_utils/common/api/v1/__init__.py @@ -1,41 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api import Api - - -class V1(Api): - """ - ## v1 API enpoints - Api().V1() - - ### Description - Common methods and properties for API v1 subclasses. - - ### Path - ``/appcenter/cisco/ndfc/api/v1/`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.V1()") - self.v1 = f"{self.api}/v1" diff --git a/plugins/module_utils/common/api/v1/configtemplate/__init__.py b/plugins/module_utils/common/api/v1/configtemplate/__init__.py index eabbebb04..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/configtemplate/__init__.py +++ b/plugins/module_utils/common/api/v1/configtemplate/__init__.py @@ -1,42 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1 import \ - V1 - - -class ConfigTemplate(V1): - """ - ## V1 API - ConfigTemplate() - - ### Description - Common methods and properties for api.v1.ConfigTemplate() subclasses - - ### Path - ``/appcenter/cisco/ndfc/api/v1/configtemplate`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.configtemplate = f"{self.v1}/configtemplate" - self.log.debug("ENTERED api.v1.configtemplate.ConfigTemplate()") diff --git a/plugins/module_utils/common/api/v1/configtemplate/configtemplate.py b/plugins/module_utils/common/api/v1/configtemplate/configtemplate.py new file mode 100644 index 000000000..cd7ddc91e --- /dev/null +++ b/plugins/module_utils/common/api/v1/configtemplate/configtemplate.py @@ -0,0 +1,42 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.v1 import \ + V1 + + +class ConfigTemplate(V1): + """ + ## V1 API - ConfigTemplate() + + ### Description + Common methods and properties for api.v1.ConfigTemplate() subclasses + + ### Path + ``/appcenter/cisco/ndfc/api/v1/configtemplate`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.configtemplate = f"{self.v1}/configtemplate" + self.log.debug("ENTERED api.v1.configtemplate.ConfigTemplate()") diff --git a/plugins/module_utils/common/api/v1/configtemplate/rest/__init__.py b/plugins/module_utils/common/api/v1/configtemplate/rest/__init__.py index f28751037..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/configtemplate/rest/__init__.py +++ b/plugins/module_utils/common/api/v1/configtemplate/rest/__init__.py @@ -1,49 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate import \ - ConfigTemplate - - -class Rest(ConfigTemplate): - """ - ## V1 API ConfigTemplate() - api.v1.configtemplate.rest.Rest() - - ### Description - Common methods and properties for api.v1.configtemplate.rest.Rest() subclasses. - - ### Path - - ``/api/v1/configtemplate/rest`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.rest = f"{self.configtemplate}/rest" - msg = f"ENTERED api.v1.configtemplate.rest.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Populate class-specific properties. - """ diff --git a/plugins/module_utils/common/api/v1/configtemplate/rest/config/__init__.py b/plugins/module_utils/common/api/v1/configtemplate/rest/config/__init__.py index e6a0171a0..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/configtemplate/rest/config/__init__.py +++ b/plugins/module_utils/common/api/v1/configtemplate/rest/config/__init__.py @@ -1,49 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest import \ - Rest - - -class Config(Rest): - """ - ## V1 API Config() - api.v1.configtemplate.rest.config.Config() - - ### Description - Common methods and properties for api.v1.configtemplate.rest.config.Config() subclasses. - - ### Path - - ``/api/v1/configtemplate/rest/config`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.config = f"{self.rest}/config" - msg = f"ENTERED api.v1.rest.config.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Populate class-specific properties. - """ diff --git a/plugins/module_utils/common/api/v1/configtemplate/rest/config/config.py b/plugins/module_utils/common/api/v1/configtemplate/rest/config/config.py new file mode 100644 index 000000000..1ae9b93c1 --- /dev/null +++ b/plugins/module_utils/common/api/v1/configtemplate/rest/config/config.py @@ -0,0 +1,49 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest.rest import \ + Rest + + +class Config(Rest): + """ + ## V1 API Config() - api.v1.configtemplate.rest.config.Config() + + ### Description + Common methods and properties for api.v1.configtemplate.rest.config.Config() subclasses. + + ### Path + - ``/api/v1/configtemplate/rest/config`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.config = f"{self.rest}/config" + msg = f"ENTERED api.v1.rest.config.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Populate class-specific properties. + """ diff --git a/plugins/module_utils/common/api/v1/configtemplate/rest/config/templates/__init__.py b/plugins/module_utils/common/api/v1/configtemplate/rest/config/templates/__init__.py index ee6def39e..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/configtemplate/rest/config/templates/__init__.py +++ b/plugins/module_utils/common/api/v1/configtemplate/rest/config/templates/__init__.py @@ -1,193 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import inspect -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest.config import \ - Config -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ - FabricTypes - - -class Templates(Config): - """ - ## api.v1.configtemplate.rest.config.templates.Templates() - - ### Description - Common methods and properties for Templates() subclasses. - - ### Path - - ``/api/v1/configtemplate/rest/config/templates`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.fabric_types = FabricTypes() - - self.templates = f"{self.config}/templates" - self._template_name = None - msg = "ENTERED api.v1.configtemplate.rest.config." - msg += f"templates.{self.class_name}" - self.log.debug(msg) - - @property - def path_template_name(self): - """ - - Endpoint for template retrieval. - - Raise ``ValueError`` if template_name is not set. - """ - method_name = inspect.stack()[0][3] - if self.template_name is None and "template_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "template_name must be set prior to accessing path." - raise ValueError(msg) - return f"{self.templates}/{self.template_name}" - - @property - def template_name(self): - """ - - getter: Return the template_name. - - setter: Set the template_name. - - setter: Raise ``ValueError`` if template_name is not a string. - """ - return self._template_name - - @template_name.setter - def template_name(self, value): - method_name = inspect.stack()[0][3] - if value not in self.fabric_types.valid_fabric_template_names: - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid template_name: {value}. " - msg += "Expected one of: " - msg += f"{', '.join(self.fabric_types.valid_fabric_template_names)}." - raise ValueError(msg) - self._template_name = value - - -class EpTemplate(Templates): - """ - ## V1 API - Templates().EpTemplate() - - ### Description - Return endpoint information. - - ### Raises - - ``ValueError``: If template_name is not set. - - ``ValueError``: If template_name is not a valid fabric template name. - - ### Path - - ``/api/v1/configtemplates/rest/config/templates/{template_name}`` - - ### Verb - - GET - - ### Parameters - - template_name: string - - set the ``template_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpTemplate() - instance.template_name = "Easy_Fabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("template_name") - msg = "ENTERED api.v1.configtemplate.rest.config." - msg += f"templates.Templates.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - """ - - Endpoint for template retrieval. - - Raise ``ValueError`` if template_name is not set. - """ - return self.path_template_name - - @property - def verb(self): - """ - - Return the verb for the endpoint. - """ - return "GET" - - -class EpTemplates(Templates): - """ - ## V1 API - Templates().EpTemplates() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - - ``/api/v1/configtemplates/rest/config/templates`` - - ### Verb - - GET - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpTemplates() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._build_properties() - msg = "ENTERED api.v1.configtemplate.rest.config." - msg += f"templates.Templates.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - """ - - Return the path for the endpoint. - """ - return self.templates - - @property - def verb(self): - """ - - Return the verb for the endpoint. - """ - return "GET" diff --git a/plugins/module_utils/common/api/v1/configtemplate/rest/config/templates/templates.py b/plugins/module_utils/common/api/v1/configtemplate/rest/config/templates/templates.py new file mode 100644 index 000000000..bbc6a3291 --- /dev/null +++ b/plugins/module_utils/common/api/v1/configtemplate/rest/config/templates/templates.py @@ -0,0 +1,193 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest.config.config import \ + Config +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ + FabricTypes + + +class Templates(Config): + """ + ## api.v1.configtemplate.rest.config.templates.Templates() + + ### Description + Common methods and properties for Templates() subclasses. + + ### Path + - ``/api/v1/configtemplate/rest/config/templates`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fabric_types = FabricTypes() + + self.templates = f"{self.config}/templates" + self._template_name = None + msg = "ENTERED api.v1.configtemplate.rest.config." + msg += f"templates.{self.class_name}" + self.log.debug(msg) + + @property + def path_template_name(self): + """ + - Endpoint for template retrieval. + - Raise ``ValueError`` if template_name is not set. + """ + method_name = inspect.stack()[0][3] + if self.template_name is None and "template_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "template_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.templates}/{self.template_name}" + + @property + def template_name(self): + """ + - getter: Return the template_name. + - setter: Set the template_name. + - setter: Raise ``ValueError`` if template_name is not a string. + """ + return self._template_name + + @template_name.setter + def template_name(self, value): + method_name = inspect.stack()[0][3] + if value not in self.fabric_types.valid_fabric_template_names: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid template_name: {value}. " + msg += "Expected one of: " + msg += f"{', '.join(self.fabric_types.valid_fabric_template_names)}." + raise ValueError(msg) + self._template_name = value + + +class EpTemplate(Templates): + """ + ## V1 API - Templates().EpTemplate() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If template_name is not set. + - ``ValueError``: If template_name is not a valid fabric template name. + + ### Path + - ``/api/v1/configtemplates/rest/config/templates/{template_name}`` + + ### Verb + - GET + + ### Parameters + - template_name: string + - set the ``template_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpTemplate() + instance.template_name = "Easy_Fabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("template_name") + msg = "ENTERED api.v1.configtemplate.rest.config." + msg += f"templates.Templates.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + """ + - Endpoint for template retrieval. + - Raise ``ValueError`` if template_name is not set. + """ + return self.path_template_name + + @property + def verb(self): + """ + - Return the verb for the endpoint. + """ + return "GET" + + +class EpTemplates(Templates): + """ + ## V1 API - Templates().EpTemplates() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/api/v1/configtemplates/rest/config/templates`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpTemplates() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + msg = "ENTERED api.v1.configtemplate.rest.config." + msg += f"templates.Templates.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + """ + - Return the path for the endpoint. + """ + return self.templates + + @property + def verb(self): + """ + - Return the verb for the endpoint. + """ + return "GET" diff --git a/plugins/module_utils/common/api/v1/configtemplate/rest/rest.py b/plugins/module_utils/common/api/v1/configtemplate/rest/rest.py new file mode 100644 index 000000000..9534bd12c --- /dev/null +++ b/plugins/module_utils/common/api/v1/configtemplate/rest/rest.py @@ -0,0 +1,49 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.configtemplate import \ + ConfigTemplate + + +class Rest(ConfigTemplate): + """ + ## V1 API ConfigTemplate() - api.v1.configtemplate.rest.Rest() + + ### Description + Common methods and properties for api.v1.configtemplate.rest.Rest() subclasses. + + ### Path + - ``/api/v1/configtemplate/rest`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.rest = f"{self.configtemplate}/rest" + msg = f"ENTERED api.v1.configtemplate.rest.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Populate class-specific properties. + """ diff --git a/plugins/module_utils/common/api/v1/fm/__init__.py b/plugins/module_utils/common/api/v1/fm/__init__.py index 5eb69d5c3..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/fm/__init__.py +++ b/plugins/module_utils/common/api/v1/fm/__init__.py @@ -1,128 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1 import \ - V1 - - -class FM(V1): - """ - ## api.v1.fm.FM() - - ### Description - Common methods and properties for FM() subclasses - - ### Path - ``/appcenter/cisco/ndfc/api/v1/fm`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.fm = f"{self.v1}/fm" - self.log.debug("ENTERED api.v1.fm.FM()") - - -class EpFeatures(FM): - """ - ## api.v1.fm.EpFeatures() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - ``/api/v1/fm/features`` - - ### Verb - - GET - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFeatures() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.fm.EpFeatures()") - - @property - def path(self): - return f"{self.fm}/features" - - @property - def verb(self): - return "GET" - - -class EpVersion(FM): - """ - ## api.v1.fm.EpVersion() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - ``/api/v1/fm/about/version`` - - ### Verb - - GET - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpVersion() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.fm.EpVersion()") - - @property - def path(self): - return f"{self.fm}/about/version" - - @property - def verb(self): - return "GET" diff --git a/plugins/module_utils/common/api/v1/fm/fm.py b/plugins/module_utils/common/api/v1/fm/fm.py new file mode 100644 index 000000000..7a6608bf3 --- /dev/null +++ b/plugins/module_utils/common/api/v1/fm/fm.py @@ -0,0 +1,128 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.v1 import \ + V1 + + +class FM(V1): + """ + ## api.v1.fm.FM() + + ### Description + Common methods and properties for FM() subclasses + + ### Path + ``/appcenter/cisco/ndfc/api/v1/fm`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fm = f"{self.v1}/fm" + self.log.debug("ENTERED api.v1.fm.FM()") + + +class EpFeatures(FM): + """ + ## api.v1.fm.EpFeatures() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + ``/api/v1/fm/features`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFeatures() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.fm.EpFeatures()") + + @property + def path(self): + return f"{self.fm}/features" + + @property + def verb(self): + return "GET" + + +class EpVersion(FM): + """ + ## api.v1.fm.EpVersion() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + ``/api/v1/fm/about/version`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpVersion() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.fm.EpVersion()") + + @property + def path(self): + return f"{self.fm}/about/version" + + @property + def verb(self): + return "GET" diff --git a/plugins/module_utils/common/api/v1/imagemanagement/__init__.py b/plugins/module_utils/common/api/v1/imagemanagement/__init__.py index 8af146f9b..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/imagemanagement/__init__.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/__init__.py @@ -1,42 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1 import \ - V1 - - -class ImageManagement(V1): - """ - ## V1 API - ImageManagement() - - ### Description - Common methods and properties for CommonV1().ImageManagement() subclasses - - ### Path - ``/appcenter/cisco/ndfc/api/v1/imagemanagement`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.imagemanagement = f"{self.v1}/imagemanagement" - self.log.debug("ENTERED api.v1.imagemanagement.ImageManagement()") diff --git a/plugins/module_utils/common/api/v1/imagemanagement/imagemanagement.py b/plugins/module_utils/common/api/v1/imagemanagement/imagemanagement.py new file mode 100644 index 000000000..7d3fdda39 --- /dev/null +++ b/plugins/module_utils/common/api/v1/imagemanagement/imagemanagement.py @@ -0,0 +1,42 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.v1 import \ + V1 + + +class ImageManagement(V1): + """ + ## V1 API - ImageManagement() + + ### Description + Common methods and properties for CommonV1().ImageManagement() subclasses + + ### Path + ``/appcenter/cisco/ndfc/api/v1/imagemanagement`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.imagemanagement = f"{self.v1}/imagemanagement" + self.log.debug("ENTERED api.v1.imagemanagement.ImageManagement()") diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/__init__.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/__init__.py index b36e6d938..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/imagemanagement/rest/__init__.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/__init__.py @@ -1,49 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement import \ - ImageManagement - - -class Rest(ImageManagement): - """ - ## api.v1.imagemanagement.rest.Rest() - - ### Description - Common methods and properties api.v1.imagemanagement.rest subclasses. - - ### Path - - ``/api/v1/imagemanagement/rest`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.rest = f"{self.imagemanagement}/rest" - msg = f"ENTERED api.v1.imagemanagement.rest.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Populate properties specific to this class and its subclasses. - """ diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/__init__.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/__init__.py index 1231a3cf6..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/__init__.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/__init__.py @@ -1,85 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest import \ - Rest - - -class ImageMgnt(Rest): - """ - ## api.v1.imagemanagement.rest.imagemgt.ImageMgnt() - - ### Description - Common methods and properties for ImageMgnt() subclasses - - ### Path - ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imagemgnt`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.image_mgmt = f"{self.rest}/imagemgnt" - self.log.debug("ENTERED api.v1.imagemanagement.rest.imagemgnt.ImageMgnt()") - - -class EpBootFlashInfo(ImageMgnt): - """ - ## api.v1.imagemanagement.rest.imagemgnt.EpBootFlashInfo() - - ### Description - Return endpoint information for bootflash-info. - - ### Raises - - None - - ### Path - - ``/api/imagemanagement/rest/imagemgnt/bootFlash/bootflash-info`` - - ### Verb - - GET - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpBootFlashInfo() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED api.v1.ImageMgnt.EpBootFlash()") - - @property - def path(self): - return f"{self.image_mgmt}/bootFlash/bootflash-info" - - @property - def verb(self): - return "GET" diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/imagemgnt.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/imagemgnt.py new file mode 100644 index 000000000..2ced72e4b --- /dev/null +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/imagemgnt.py @@ -0,0 +1,85 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.rest import \ + Rest + + +class ImageMgnt(Rest): + """ + ## api.v1.imagemanagement.rest.imagemgt.ImageMgnt() + + ### Description + Common methods and properties for ImageMgnt() subclasses + + ### Path + ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/imagemgnt`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.image_mgmt = f"{self.rest}/imagemgnt" + self.log.debug("ENTERED api.v1.imagemanagement.rest.imagemgnt.ImageMgnt()") + + +class EpBootFlashInfo(ImageMgnt): + """ + ## api.v1.imagemanagement.rest.imagemgnt.EpBootFlashInfo() + + ### Description + Return endpoint information for bootflash-info. + + ### Raises + - None + + ### Path + - ``/api/imagemanagement/rest/imagemgnt/bootFlash/bootflash-info`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpBootFlashInfo() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.ImageMgnt.EpBootFlash()") + + @property + def path(self): + return f"{self.image_mgmt}/bootFlash/bootflash-info" + + @property + def verb(self): + return "GET" diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/imageupgrade/__init__.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/imageupgrade/__init__.py index 1fe516d51..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/imagemanagement/rest/imageupgrade/__init__.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/imageupgrade/__init__.py @@ -1,151 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest import \ - Rest - - -class ImageUpgrade(Rest): - """ - ## api.v1.imagemanagement.rest.imageupgrade.ImageUpgrade() - - ### Description - Common methods and properties for ImageUpgrade() subclasses. - - ### Path - - ``/api/v1/imagemanagement/rest/imageupgrade`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.imageupgrade = f"{self.rest}/imageupgrade" - msg = f"ENTERED api.v1.imagemanagement.rest.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Add any class-specific properties to self.properties. - """ - - -class EpInstallOptions(ImageUpgrade): - """ - ## V1 API - Fabrics().EpInstallOptions() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - - ``/api/v1/imagemanagement/rest/imageupgrade/install-options`` - - ### Verb - - POST - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - ep_install_options = EpInstallOptions() - path = ep_install_options.path - verb = ep_install_options.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"imageupgrade.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - """ - - Return the path for the endpoint. - """ - return f"{self.imageupgrade}/install-options" - - @property - def verb(self): - """ - - Return the verb for the endpoint. - """ - return "POST" - - -class EpUpgradeImage(ImageUpgrade): - """ - ## V1 API - Fabrics().EpUpgradeImage() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - - ``/api/v1/imagemanagement/rest/imageupgrade/upgrade-image`` - - ### Verb - - POST - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - ep_upgrade_image = EpUpgradeImage() - path = ep_upgrade_image.path - verb = ep_upgrade_image.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"imageupgrade.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - """ - - Return the path for the endpoint. - """ - return f"{self.imageupgrade}/upgrade-image" - - @property - def verb(self): - """ - - Return the verb for the endpoint. - """ - return "POST" diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/imageupgrade/imageupgrade.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/imageupgrade/imageupgrade.py new file mode 100644 index 000000000..f4a4c5b9c --- /dev/null +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/imageupgrade/imageupgrade.py @@ -0,0 +1,151 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.rest import \ + Rest + + +class ImageUpgrade(Rest): + """ + ## api.v1.imagemanagement.rest.imageupgrade.ImageUpgrade() + + ### Description + Common methods and properties for ImageUpgrade() subclasses. + + ### Path + - ``/api/v1/imagemanagement/rest/imageupgrade`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.imageupgrade = f"{self.rest}/imageupgrade" + msg = f"ENTERED api.v1.imagemanagement.rest.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Add any class-specific properties to self.properties. + """ + + +class EpInstallOptions(ImageUpgrade): + """ + ## V1 API - Fabrics().EpInstallOptions() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/api/v1/imagemanagement/rest/imageupgrade/install-options`` + + ### Verb + - POST + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + ep_install_options = EpInstallOptions() + path = ep_install_options.path + verb = ep_install_options.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"imageupgrade.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + """ + - Return the path for the endpoint. + """ + return f"{self.imageupgrade}/install-options" + + @property + def verb(self): + """ + - Return the verb for the endpoint. + """ + return "POST" + + +class EpUpgradeImage(ImageUpgrade): + """ + ## V1 API - Fabrics().EpUpgradeImage() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/api/v1/imagemanagement/rest/imageupgrade/upgrade-image`` + + ### Verb + - POST + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + ep_upgrade_image = EpUpgradeImage() + path = ep_upgrade_image.path + verb = ep_upgrade_image.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"imageupgrade.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + """ + - Return the path for the endpoint. + """ + return f"{self.imageupgrade}/upgrade-image" + + @property + def verb(self): + """ + - Return the verb for the endpoint. + """ + return "POST" diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/__init__.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/__init__.py index e1cf739c9..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/__init__.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/__init__.py @@ -1,336 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import inspect -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest import \ - Rest - - -class PolicyMgnt(Rest): - """ - ## api.v1.imagemanagement.rest.policymgnt.PolicyMgnt() - - ### Description - Common methods and properties for PolicyMgnt() subclasses - - ### Path - ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.policymgnt = f"{self.rest}/policymgnt" - self.log.debug("ENTERED api.v1.PolicyMgnt()") - - -class EpPolicies(PolicyMgnt): - """ - ## api.v1.imagemanagement.rest.policymgnt.EpPolicies() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - - ``/api/v1/imagemanagement/rest/policymgnt/policies`` - - ### Verb - - GET - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpPolicies() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"policymgnt.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - return f"{self.policymgnt}/policies" - - @property - def verb(self): - return "GET" - - -class EpPoliciesAllAttached(PolicyMgnt): - """ - ## V1 API - PolicyMgnt().EpPoliciesAllAttached() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - - ``/rest/policymgnt/all-attached-policies`` - - ### Verb - - GET - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpPoliciesAllAttached() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"policymgnt.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - return f"{self.policymgnt}/all-attached-policies" - - @property - def verb(self): - return "GET" - - -class EpPolicyAttach(PolicyMgnt): - """ - ## V1 API - PolicyMgnt().EpPolicyAttach() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - - ``/rest/policymgnt/attach-policy`` - - ### Verb - - POST - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpPolicyAttach() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"policymgnt.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - return f"{self.policymgnt}/attach-policy" - - @property - def verb(self): - return "POST" - - -class EpPolicyCreate(PolicyMgnt): - """ - ## V1 API - PolicyMgnt().EpPolicyCreate() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - - ``/rest/policymgnt/platform-policy`` - - ### Verb - - POST - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpPolicyCreate() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"policymgnt.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - return f"{self.policymgnt}/platform-policy" - - @property - def verb(self): - return "POST" - - -class EpPolicyDetach(PolicyMgnt): - """ - ## V1 API - PolicyMgnt().EpPolicyDetach() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - - ``/rest/policymgnt/detach-policy`` - - ### Verb - - DELETE - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpPolicyDetach() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"policymgnt.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - return f"{self.policymgnt}/detach-policy" - - @property - def verb(self): - return "DELETE" - - -class EpPolicyInfo(PolicyMgnt): - """ - ## V1 API - PolicyMgnt().EpPolicyInfo() - - ### Description - Return endpoint information. - - ### Raises - - ``ValueError``: If path is accessed before setting policy_name. - - ### Path - - ``/rest/policymgnt/image-policy/{policy_name}`` - - ### Verb - - GET - - ### Parameters - - policy_name: str - - set the policy_name - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpPolicyInfo() - instance.policy_name = "MyPolicy" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._policy_name = None - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"policymgnt.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - method_name = inspect.stack()[0][3] - if self.policy_name is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.policy_name must be set before " - msg += f"accessing {method_name}." - raise ValueError(msg) - return f"{self.policymgnt}/image-policy/{self.policy_name}" - - @property - def verb(self): - return "GET" - - @property - def policy_name(self): - """ - - getter: Return the policy_name. - - setter: Set the policy_name. - """ - return self._policy_name - - @policy_name.setter - def policy_name(self, value): - self._policy_name = value diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py new file mode 100644 index 000000000..cfa68834d --- /dev/null +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py @@ -0,0 +1,336 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.rest import \ + Rest + + +class PolicyMgnt(Rest): + """ + ## api.v1.imagemanagement.rest.policymgnt.PolicyMgnt() + + ### Description + Common methods and properties for PolicyMgnt() subclasses + + ### Path + ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.policymgnt = f"{self.rest}/policymgnt" + self.log.debug("ENTERED api.v1.PolicyMgnt()") + + +class EpPolicies(PolicyMgnt): + """ + ## api.v1.imagemanagement.rest.policymgnt.EpPolicies() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/api/v1/imagemanagement/rest/policymgnt/policies`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPolicies() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.policymgnt}/policies" + + @property + def verb(self): + return "GET" + + +class EpPoliciesAllAttached(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPoliciesAllAttached() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/rest/policymgnt/all-attached-policies`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPoliciesAllAttached() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.policymgnt}/all-attached-policies" + + @property + def verb(self): + return "GET" + + +class EpPolicyAttach(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPolicyAttach() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/rest/policymgnt/attach-policy`` + + ### Verb + - POST + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPolicyAttach() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.policymgnt}/attach-policy" + + @property + def verb(self): + return "POST" + + +class EpPolicyCreate(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPolicyCreate() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/rest/policymgnt/platform-policy`` + + ### Verb + - POST + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPolicyCreate() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.policymgnt}/platform-policy" + + @property + def verb(self): + return "POST" + + +class EpPolicyDetach(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPolicyDetach() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/rest/policymgnt/detach-policy`` + + ### Verb + - DELETE + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPolicyDetach() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.policymgnt}/detach-policy" + + @property + def verb(self): + return "DELETE" + + +class EpPolicyInfo(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPolicyInfo() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If path is accessed before setting policy_name. + + ### Path + - ``/rest/policymgnt/image-policy/{policy_name}`` + + ### Verb + - GET + + ### Parameters + - policy_name: str + - set the policy_name + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpPolicyInfo() + instance.policy_name = "MyPolicy" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._policy_name = None + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + method_name = inspect.stack()[0][3] + if self.policy_name is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.policy_name must be set before " + msg += f"accessing {method_name}." + raise ValueError(msg) + return f"{self.policymgnt}/image-policy/{self.policy_name}" + + @property + def verb(self): + return "GET" + + @property + def policy_name(self): + """ + - getter: Return the policy_name. + - setter: Set the policy_name. + """ + return self._policy_name + + @policy_name.setter + def policy_name(self, value): + self._policy_name = value diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/rest.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/rest.py new file mode 100644 index 000000000..3c5933d9b --- /dev/null +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/rest.py @@ -0,0 +1,49 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.imagemanagement import \ + ImageManagement + + +class Rest(ImageManagement): + """ + ## api.v1.imagemanagement.rest.Rest() + + ### Description + Common methods and properties api.v1.imagemanagement.rest subclasses. + + ### Path + - ``/api/v1/imagemanagement/rest`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.rest = f"{self.imagemanagement}/rest" + msg = f"ENTERED api.v1.imagemanagement.rest.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Populate properties specific to this class and its subclasses. + """ diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/stagingmanagement/__init__.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/stagingmanagement/__init__.py index 8d59b6264..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/imagemanagement/rest/stagingmanagement/__init__.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/stagingmanagement/__init__.py @@ -1,179 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest import \ - Rest - - -class StagingManagement(Rest): - """ - ## api.v1.imagemanagement.rest.stagingmanagement.StagingManagement() - - ### Description - Common methods and properties for StagingManagement() subclasses - - ### Path - ``/api/v1/imagemanagement/rest/stagingmanagement`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.stagingmanagement = f"{self.rest}/stagingmanagement" - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"stagingmanagement.{self.class_name}" - self.log.debug(msg) - - -class EpImageStage(StagingManagement): - """ - ## api.v1.imagemanagement.rest.stagingmanagement.EpImageStage() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - - ``/api/v1/imagemanagement/rest/stagingmanagement/stage-image`` - - ### Verb - - POST - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpImageStage() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"stagingmanagement.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - return f"{self.stagingmanagement}/stage-image" - - @property - def verb(self): - return "POST" - - -class EpImageValidate(StagingManagement): - """ - ## V1 API - StagingManagement().EpImageValidate() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - - ``/api/v1/imagemanagement/rest/stagingmanagement/validate-image`` - - ### Verb - - POST - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpImageValidate() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"stagingmanagement.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - return f"{self.stagingmanagement}/validate-image" - - @property - def verb(self): - return "POST" - - -class EpStageInfo(StagingManagement): - """ - ## V1 API - StagingManagement().EpStageInfo() - - ### Description - Return endpoint information. - - ### Raises - - None - - ### Path - - ``/api/v1/imagemanagement/rest/stagingmanagement/stage-info`` - - ### Verb - - GET - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpStageInfo() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED api.v1.imagemanagement.rest." - msg += f"stagingmanagement.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - return f"{self.stagingmanagement}/stage-info" - - @property - def verb(self): - return "GET" diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/stagingmanagement/stagingmanagement.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/stagingmanagement/stagingmanagement.py new file mode 100644 index 000000000..b639bae13 --- /dev/null +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/stagingmanagement/stagingmanagement.py @@ -0,0 +1,179 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.rest import \ + Rest + + +class StagingManagement(Rest): + """ + ## api.v1.imagemanagement.rest.stagingmanagement.StagingManagement() + + ### Description + Common methods and properties for StagingManagement() subclasses + + ### Path + ``/api/v1/imagemanagement/rest/stagingmanagement`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.stagingmanagement = f"{self.rest}/stagingmanagement" + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"stagingmanagement.{self.class_name}" + self.log.debug(msg) + + +class EpImageStage(StagingManagement): + """ + ## api.v1.imagemanagement.rest.stagingmanagement.EpImageStage() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/api/v1/imagemanagement/rest/stagingmanagement/stage-image`` + + ### Verb + - POST + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpImageStage() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"stagingmanagement.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.stagingmanagement}/stage-image" + + @property + def verb(self): + return "POST" + + +class EpImageValidate(StagingManagement): + """ + ## V1 API - StagingManagement().EpImageValidate() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/api/v1/imagemanagement/rest/stagingmanagement/validate-image`` + + ### Verb + - POST + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpImageValidate() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"stagingmanagement.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.stagingmanagement}/validate-image" + + @property + def verb(self): + return "POST" + + +class EpStageInfo(StagingManagement): + """ + ## V1 API - StagingManagement().EpStageInfo() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/api/v1/imagemanagement/rest/stagingmanagement/stage-info`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpStageInfo() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"stagingmanagement.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.stagingmanagement}/stage-info" + + @property + def verb(self): + return "GET" diff --git a/plugins/module_utils/common/api/v1/lan_fabric/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/__init__.py index 1f1478656..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/__init__.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/__init__.py @@ -1,42 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1 import \ - V1 - - -class LanFabric(V1): - """ - ## api.v1.lan-fabric.LanFabric() - - ### Description - Common methods and properties for api.v1.lan-fabric.LanFabric() subclasses - - ### Path - ``/appcenter/cisco/ndfc/api/v1/lan-fabric`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.lan_fabric = f"{self.v1}/lan-fabric" - self.log.debug("ENTERED api.v1.lan-fabric.LanFabric()") diff --git a/plugins/module_utils/common/api/v1/lan_fabric/lan_fabric.py b/plugins/module_utils/common/api/v1/lan_fabric/lan_fabric.py new file mode 100644 index 000000000..9c20ab186 --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/lan_fabric.py @@ -0,0 +1,42 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.v1 import \ + V1 + + +class LanFabric(V1): + """ + ## api.v1.lan-fabric.LanFabric() + + ### Description + Common methods and properties for api.v1.lan-fabric.LanFabric() subclasses + + ### Path + ``/appcenter/cisco/ndfc/api/v1/lan-fabric`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.lan_fabric = f"{self.v1}/lan-fabric" + self.log.debug("ENTERED api.v1.lan-fabric.LanFabric()") diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/__init__.py index 4d4a9db9e..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/rest/__init__.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/__init__.py @@ -1,43 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric import \ - LanFabric - - -class Rest(LanFabric): - """ - ## api.v1.lan_fabric.rest.Rest() - - ### Description - Common methods and properties for api.v1.lan_fabric.rest.Rest() subclasses. - - ### Path - - ``/api/v1/lan-fabric/rest`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.rest = f"{self.lan_fabric}/rest" - msg = f"ENTERED api.v1.lan_fabric.rest.{self.class_name}" - self.log.debug(msg) diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/__init__.py index e77793236..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/__init__.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/__init__.py @@ -1,43 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest import \ - Rest - - -class Control(Rest): - """ - ## api.v1.lan_fabric.rest.control.Control() - - ### Description - Common methods and properties for Control() subclasses. - - ### Path - - ``/api/v1/lan-fabric/rest/control`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.control = f"{self.rest}/control" - msg = f"ENTERED api.v1.lan_fabric.rest.control.{self.class_name}" - self.log.debug(msg) diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/control.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/control.py new file mode 100644 index 000000000..672dd317c --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/control.py @@ -0,0 +1,43 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.rest import \ + Rest + + +class Control(Rest): + """ + ## api.v1.lan_fabric.rest.control.Control() + + ### Description + Common methods and properties for Control() subclasses. + + ### Path + - ``/api/v1/lan-fabric/rest/control`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.control = f"{self.rest}/control" + msg = f"ENTERED api.v1.lan_fabric.rest.control.{self.class_name}" + self.log.debug(msg) diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/__init__.py index 47f34af53..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/__init__.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/__init__.py @@ -1,665 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import inspect -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control import \ - Control -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ - FabricTypes - - -class Fabrics(Control): - """ - ## api.v1.lan-fabric.rest.control.fabrics.Fabrics() - - ### Description - Common methods and properties for Fabrics() subclasses. - - ### Path - - ``/api/v1/lan-fabric/rest/control/fabrics`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.fabric_types = FabricTypes() - self.fabrics = f"{self.control}/fabrics" - msg = f"ENTERED api.v1.lan_fabric.rest.control.fabrics.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Set the fabric_name property. - """ - self.properties["fabric_name"] = None - self.properties["template_name"] = None - - @property - def fabric_name(self): - """ - - getter: Return the fabric_name. - - setter: Set the fabric_name. - - setter: Raise ``ValueError`` if fabric_name is not valid. - """ - return self.properties["fabric_name"] - - @fabric_name.setter - def fabric_name(self, value): - method_name = inspect.stack()[0][3] - try: - self.conversion.validate_fabric_name(value) - except (TypeError, ValueError) as error: - msg = f"{self.class_name}.{method_name}: " - msg += f"{error}" - raise ValueError(msg) from error - self.properties["fabric_name"] = value - - @property - def path_fabric_name(self): - """ - - Endpoint path property, including fabric_name. - - Raise ``ValueError`` if fabric_name is not set and - ``self.required_properties`` contains "fabric_name". - """ - method_name = inspect.stack()[0][3] - if self.fabric_name is None and "fabric_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be set prior to accessing path." - raise ValueError(msg) - return f"{self.fabrics}/{self.fabric_name}" - - @property - def path_fabric_name_template_name(self): - """ - - Endpoint path property, including fabric_name and template_name. - - Raise ``ValueError`` if fabric_name is not set and - ``self.required_properties`` contains "fabric_name". - - Raise ``ValueError`` if template_name is not set and - ``self.required_properties`` contains "template_name". - """ - method_name = inspect.stack()[0][3] - if self.fabric_name is None and "fabric_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be set prior to accessing path." - raise ValueError(msg) - if self.template_name is None and "template_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "template_name must be set prior to accessing path." - raise ValueError(msg) - return f"{self.fabrics}/{self.fabric_name}/{self.template_name}" - - @property - def template_name(self): - """ - - getter: Return the template_name. - - setter: Set the template_name. - - setter: Raise ``ValueError`` if template_name is not a string. - """ - return self.properties["template_name"] - - @template_name.setter - def template_name(self, value): - method_name = inspect.stack()[0][3] - if value not in self.fabric_types.valid_fabric_template_names: - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid template_name: {value}. " - msg += "Expected one of: " - msg += f"{', '.join(self.fabric_types.valid_fabric_template_names)}." - raise ValueError(msg) - self.properties["template_name"] = value - - -class EpFabricConfigDeploy(Fabrics): - """ - ## api.v1.lan-fabric.rest.control.fabrics.EpFabricConfigDeploy() - - ### Description - Return endpoint to initiate config-deploy on fabric_name. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If force_show_run is not boolean. - - ``ValueError``: If include_all_msd_switches is not boolean. - - ### Path - - ``/fabrics/{fabric_name}/config-deploy`` - - ``/fabrics/{fabric_name}/config-deploy?forceShowRun={force_show_run}`` - - ``/fabrics/{fabric_name}/config-deploy?inclAllMSDSwitches={include_all_msd_switches}`` - - ### Verb - - POST - - ### Parameters - - force_show_run: boolean - - set the ``forceShowRun`` value - - default: False - - include_all_msd_switches: boolean - - set the ``inclAllMSDSwitches`` value - - default: False - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricConfigDeploy() - instance.fabric_name = "MyFabric" - instance.force_show_run = True - instance.include_all_msd_switches = True - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." - msg += f"Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "POST" - self.properties["force_show_run"] = False - self.properties["include_all_msd_switches"] = False - - @property - def force_show_run(self): - """ - - getter: Return the force_show_run value. - - setter: Set the force_show_run value. - - setter: Raise ``ValueError`` if force_show_run is not a boolean. - - Default: False - """ - return self.properties["force_show_run"] - - @force_show_run.setter - def force_show_run(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += f"Expected boolean for {method_name}. " - msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) - self.properties["force_show_run"] = value - - @property - def include_all_msd_switches(self): - """ - - getter: Return the include_all_msd_switches. - - setter: Set the include_all_msd_switches. - - setter: Raise ``ValueError`` if include_all_msd_switches is a boolean. - - Default: False - """ - return self.properties["include_all_msd_switches"] - - @include_all_msd_switches.setter - def include_all_msd_switches(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += f"Expected boolean for {method_name}. " - msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) - self.properties["include_all_msd_switches"] = value - - @property - def path(self): - """ - - Override the path property to mandate fabric_name is set. - - Raise ``ValueError`` if fabric_name is not set. - """ - _path = self.path_fabric_name - _path += "/config-deploy?" - _path += f"forceShowRun={self.force_show_run}" - _path += f"&inclAllMSDSwitches={self.include_all_msd_switches}" - return _path - - -class EpFabricConfigSave(Fabrics): - """ - ## V1 API - Fabrics().EpFabricConfigSave() - - ### Description - Return endpoint to initiate config-save on fabric_name. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If ticket_id is not a string. - - ### Path - - ``/fabrics/{fabric_name}/config-save`` - - ``/fabrics/{fabric_name}/config-save?ticketId={ticket_id}`` - - ### Verb - - POST - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - ticket_id: string - - optional unless Change Control is enabled - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricConfigSave() - instance.fabric_name = "MyFabric" - instance.ticket_id = "MyTicket1234" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." - msg += f"Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "POST" - self.properties["ticket_id"] = None - - @property - def ticket_id(self): - """ - - getter: Return the ticket_id. - - setter: Set the ticket_id. - - setter: Raise ``ValueError`` if ticket_id is not a string. - - Default: None - - Note: ticket_id is optional unless Change Control is enabled. - """ - return self.properties["ticket_id"] - - @ticket_id.setter - def ticket_id(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, str): - msg = f"{self.class_name}.{method_name}: " - msg += f"Expected string for {method_name}. " - msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) - self.properties["ticket_id"] = value - - @property - def path(self): - """ - - Endpoint for config-save. - - Set self.ticket_id if Change Control is enabled. - - Raise ``ValueError`` if fabric_name is not set. - """ - _path = self.path_fabric_name - _path += "/config-save" - if self.ticket_id: - _path += f"?ticketId={self.ticket_id}" - return _path - - -class EpFabricCreate(Fabrics): - """ - ## V1 API - Fabrics().EpFabricCreate() - - ### Description - Return endpoint information. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If template_name is not set. - - ``ValueError``: If template_name is not a valid fabric template name. - - ### Path - - ``/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` - - ### Verb - - POST - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - template_name: string - - set the ``template_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricCreate() - instance.fabric_name = "MyFabric" - instance.template_name = "Easy_Fabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self.required_properties.add("template_name") - self._build_properties() - msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." - msg += f"Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "POST" - - @property - def path(self): - """ - - Endpoint for fabric create. - - Raise ``ValueError`` if fabric_name is not set. - """ - return self.path_fabric_name_template_name - - -class EpFabricDelete(Fabrics): - """ - ## V1 API - Fabrics().EpFabricDelete() - - ### Description - Return endpoint to delete ``fabric_name``. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/fabrics/{fabric_name}`` - - ### Verb - - DELETE - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricDelete() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." - msg += f"Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "DELETE" - - @property - def path(self): - """ - - Endpoint for fabric delete. - - Raise ``ValueError`` if fabric_name is not set. - """ - return self.path_fabric_name - - -class EpFabricDetails(Fabrics): - """ - ## V1 API - Fabrics().EpFabricDetails() - - ### Description - Return the endpoint to query ``fabric_name`` details. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/fabrics/{fabric_name}`` - - ### Verb - - GET - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricDelete() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." - msg += f"Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - return self.path_fabric_name - - -class EpFabricFreezeMode(Fabrics): - """ - ## V1 API - Fabrics().EpFabricFreezeMode() - - ### Description - Return the endpoint to query ``fabric_name`` freezemode status. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/fabrics/{fabric_name}/freezemode`` - - ### Verb - - GET - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricDelete() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." - msg += f"Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - return f"{self.path_fabric_name}/freezemode" - - -# class EpFabricSummary() See module_utils/common/api/v1/rest/control/switches.py - - -class EpFabricUpdate(Fabrics): - """ - ## V1 API - Fabrics().EpFabricUpdate() - - ### Description - Return endpoint information. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If template_name is not set. - - ``ValueError``: If template_name is not a valid fabric template name. - - ### Path - ``/api/v1/lan-fabric/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` - - ### Verb - - PUT - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - template_name: string - - set the ``template_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricUpdate() - instance.fabric_name = "MyFabric" - instance.template_name = "Easy_Fabric_IPFM" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self.required_properties.add("template_name") - msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." - msg += f"Fabrics.{self.class_name}" - self.log.debug(msg) - - @property - def path(self): - """ - - Endpoint for fabric create. - - Raise ``ValueError`` if fabric_name is not set. - """ - return self.path_fabric_name_template_name - - @property - def verb(self): - return "PUT" - - -class EpFabrics(Fabrics): - """ - ## V1 API - Fabrics().EpFabrics() - - ### Description - Return the endpoint to query fabrics. - - ### Raises - - None - - ### Path - - ``/api/v1/lan-fabric/rest/control/fabrics`` - - ### Verb - - GET - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabrics() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._build_properties() - msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." - msg += f"Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - return self.fabrics diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py new file mode 100644 index 000000000..889afd2a1 --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py @@ -0,0 +1,665 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.control import \ + Control +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ + FabricTypes + + +class Fabrics(Control): + """ + ## api.v1.lan-fabric.rest.control.fabrics.Fabrics() + + ### Description + Common methods and properties for Fabrics() subclasses. + + ### Path + - ``/api/v1/lan-fabric/rest/control/fabrics`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fabric_types = FabricTypes() + self.fabrics = f"{self.control}/fabrics" + msg = f"ENTERED api.v1.lan_fabric.rest.control.fabrics.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Set the fabric_name property. + """ + self.properties["fabric_name"] = None + self.properties["template_name"] = None + + @property + def fabric_name(self): + """ + - getter: Return the fabric_name. + - setter: Set the fabric_name. + - setter: Raise ``ValueError`` if fabric_name is not valid. + """ + return self.properties["fabric_name"] + + @fabric_name.setter + def fabric_name(self, value): + method_name = inspect.stack()[0][3] + try: + self.conversion.validate_fabric_name(value) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) from error + self.properties["fabric_name"] = value + + @property + def path_fabric_name(self): + """ + - Endpoint path property, including fabric_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.fabrics}/{self.fabric_name}" + + @property + def path_fabric_name_template_name(self): + """ + - Endpoint path property, including fabric_name and template_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + - Raise ``ValueError`` if template_name is not set and + ``self.required_properties`` contains "template_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + if self.template_name is None and "template_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "template_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.fabrics}/{self.fabric_name}/{self.template_name}" + + @property + def template_name(self): + """ + - getter: Return the template_name. + - setter: Set the template_name. + - setter: Raise ``ValueError`` if template_name is not a string. + """ + return self.properties["template_name"] + + @template_name.setter + def template_name(self, value): + method_name = inspect.stack()[0][3] + if value not in self.fabric_types.valid_fabric_template_names: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid template_name: {value}. " + msg += "Expected one of: " + msg += f"{', '.join(self.fabric_types.valid_fabric_template_names)}." + raise ValueError(msg) + self.properties["template_name"] = value + + +class EpFabricConfigDeploy(Fabrics): + """ + ## api.v1.lan-fabric.rest.control.fabrics.EpFabricConfigDeploy() + + ### Description + Return endpoint to initiate config-deploy on fabric_name. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If force_show_run is not boolean. + - ``ValueError``: If include_all_msd_switches is not boolean. + + ### Path + - ``/fabrics/{fabric_name}/config-deploy`` + - ``/fabrics/{fabric_name}/config-deploy?forceShowRun={force_show_run}`` + - ``/fabrics/{fabric_name}/config-deploy?inclAllMSDSwitches={include_all_msd_switches}`` + + ### Verb + - POST + + ### Parameters + - force_show_run: boolean + - set the ``forceShowRun`` value + - default: False + - include_all_msd_switches: boolean + - set the ``inclAllMSDSwitches`` value + - default: False + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricConfigDeploy() + instance.fabric_name = "MyFabric" + instance.force_show_run = True + instance.include_all_msd_switches = True + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + self.properties["force_show_run"] = False + self.properties["include_all_msd_switches"] = False + + @property + def force_show_run(self): + """ + - getter: Return the force_show_run value. + - setter: Set the force_show_run value. + - setter: Raise ``ValueError`` if force_show_run is not a boolean. + - Default: False + """ + return self.properties["force_show_run"] + + @force_show_run.setter + def force_show_run(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected boolean for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["force_show_run"] = value + + @property + def include_all_msd_switches(self): + """ + - getter: Return the include_all_msd_switches. + - setter: Set the include_all_msd_switches. + - setter: Raise ``ValueError`` if include_all_msd_switches is a boolean. + - Default: False + """ + return self.properties["include_all_msd_switches"] + + @include_all_msd_switches.setter + def include_all_msd_switches(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected boolean for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["include_all_msd_switches"] = value + + @property + def path(self): + """ + - Override the path property to mandate fabric_name is set. + - Raise ``ValueError`` if fabric_name is not set. + """ + _path = self.path_fabric_name + _path += "/config-deploy?" + _path += f"forceShowRun={self.force_show_run}" + _path += f"&inclAllMSDSwitches={self.include_all_msd_switches}" + return _path + + +class EpFabricConfigSave(Fabrics): + """ + ## V1 API - Fabrics().EpFabricConfigSave() + + ### Description + Return endpoint to initiate config-save on fabric_name. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If ticket_id is not a string. + + ### Path + - ``/fabrics/{fabric_name}/config-save`` + - ``/fabrics/{fabric_name}/config-save?ticketId={ticket_id}`` + + ### Verb + - POST + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - ticket_id: string + - optional unless Change Control is enabled + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricConfigSave() + instance.fabric_name = "MyFabric" + instance.ticket_id = "MyTicket1234" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + self.properties["ticket_id"] = None + + @property + def ticket_id(self): + """ + - getter: Return the ticket_id. + - setter: Set the ticket_id. + - setter: Raise ``ValueError`` if ticket_id is not a string. + - Default: None + - Note: ticket_id is optional unless Change Control is enabled. + """ + return self.properties["ticket_id"] + + @ticket_id.setter + def ticket_id(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected string for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["ticket_id"] = value + + @property + def path(self): + """ + - Endpoint for config-save. + - Set self.ticket_id if Change Control is enabled. + - Raise ``ValueError`` if fabric_name is not set. + """ + _path = self.path_fabric_name + _path += "/config-save" + if self.ticket_id: + _path += f"?ticketId={self.ticket_id}" + return _path + + +class EpFabricCreate(Fabrics): + """ + ## V1 API - Fabrics().EpFabricCreate() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If template_name is not set. + - ``ValueError``: If template_name is not a valid fabric template name. + + ### Path + - ``/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` + + ### Verb + - POST + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - template_name: string + - set the ``template_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricCreate() + instance.fabric_name = "MyFabric" + instance.template_name = "Easy_Fabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self.required_properties.add("template_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + + @property + def path(self): + """ + - Endpoint for fabric create. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name_template_name + + +class EpFabricDelete(Fabrics): + """ + ## V1 API - Fabrics().EpFabricDelete() + + ### Description + Return endpoint to delete ``fabric_name``. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}`` + + ### Verb + - DELETE + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "DELETE" + + @property + def path(self): + """ + - Endpoint for fabric delete. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name + + +class EpFabricDetails(Fabrics): + """ + ## V1 API - Fabrics().EpFabricDetails() + + ### Description + Return the endpoint to query ``fabric_name`` details. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return self.path_fabric_name + + +class EpFabricFreezeMode(Fabrics): + """ + ## V1 API - Fabrics().EpFabricFreezeMode() + + ### Description + Return the endpoint to query ``fabric_name`` freezemode status. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}/freezemode`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return f"{self.path_fabric_name}/freezemode" + + +# class EpFabricSummary() See module_utils/common/api/v1/rest/control/switches.py + + +class EpFabricUpdate(Fabrics): + """ + ## V1 API - Fabrics().EpFabricUpdate() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If template_name is not set. + - ``ValueError``: If template_name is not a valid fabric template name. + + ### Path + ``/api/v1/lan-fabric/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` + + ### Verb + - PUT + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - template_name: string + - set the ``template_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricUpdate() + instance.fabric_name = "MyFabric" + instance.template_name = "Easy_Fabric_IPFM" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self.required_properties.add("template_name") + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + """ + - Endpoint for fabric create. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name_template_name + + @property + def verb(self): + return "PUT" + + +class EpFabrics(Fabrics): + """ + ## V1 API - Fabrics().EpFabrics() + + ### Description + Return the endpoint to query fabrics. + + ### Raises + - None + + ### Path + - ``/api/v1/lan-fabric/rest/control/fabrics`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabrics() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return self.fabrics diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/__init__.py index 450c3eb7d..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/__init__.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/__init__.py @@ -1,141 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import inspect -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control import \ - Control - - -class Switches(Control): - """ - ## api.v1.lan_fabric.rest.control.switches.Switches() - - ### Description - Common methods and properties for Switches() subclasses. - - ### Path - - ``/api/v1/lan-fabric/rest/control/switches/{fabric_name}`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.switches = f"{self.control}/switches" - msg = f"ENTERED api.v1.lan_fabric.rest.control.switches.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - Populate properties specific to this class and its subclasses. - """ - self.properties["fabric_name"] = None - - @property - def fabric_name(self): - """ - - getter: Return the fabric_name. - - setter: Set the fabric_name. - - setter: Raise ``ValueError`` if fabric_name is not valid. - """ - return self.properties["fabric_name"] - - @fabric_name.setter - def fabric_name(self, value): - method_name = inspect.stack()[0][3] - try: - self.conversion.validate_fabric_name(value) - except (TypeError, ValueError) as error: - msg = f"{self.class_name}.{method_name}: " - msg += f"{error}" - raise ValueError(msg) from error - self.properties["fabric_name"] = value - - @property - def path_fabric_name(self): - """ - - Endpoint path property, including fabric_name. - - Raise ``ValueError`` if fabric_name is not set and - ``self.required_properties`` contains "fabric_name". - """ - method_name = inspect.stack()[0][3] - if self.fabric_name is None and "fabric_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be set prior to accessing path." - raise ValueError(msg) - return f"{self.switches}/{self.fabric_name}" - - -class EpFabricSummary(Switches): - """ - ##api.v1.lan_fabric.rest.control.switches.EpFabricSummary() - - ### Description - Return endpoint information. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/api/v1/lan-fabric/rest/control/switches/{fabric_name}/overview`` - - ### Verb - - GET - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricSummary() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = "ENTERED api.v1.lan_fabric.rest.control.switches." - msg += f"{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - """ - - Override the path property to mandate fabric_name is set. - - Raise ``ValueError`` if fabric_name is not set. - """ - return f"{self.path_fabric_name}/overview" diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/switches.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/switches.py new file mode 100644 index 000000000..cac9e8836 --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/switches.py @@ -0,0 +1,141 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.control import \ + Control + + +class Switches(Control): + """ + ## api.v1.lan_fabric.rest.control.switches.Switches() + + ### Description + Common methods and properties for Switches() subclasses. + + ### Path + - ``/api/v1/lan-fabric/rest/control/switches/{fabric_name}`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.switches = f"{self.control}/switches" + msg = f"ENTERED api.v1.lan_fabric.rest.control.switches.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + Populate properties specific to this class and its subclasses. + """ + self.properties["fabric_name"] = None + + @property + def fabric_name(self): + """ + - getter: Return the fabric_name. + - setter: Set the fabric_name. + - setter: Raise ``ValueError`` if fabric_name is not valid. + """ + return self.properties["fabric_name"] + + @fabric_name.setter + def fabric_name(self, value): + method_name = inspect.stack()[0][3] + try: + self.conversion.validate_fabric_name(value) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) from error + self.properties["fabric_name"] = value + + @property + def path_fabric_name(self): + """ + - Endpoint path property, including fabric_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.switches}/{self.fabric_name}" + + +class EpFabricSummary(Switches): + """ + ##api.v1.lan_fabric.rest.control.switches.EpFabricSummary() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/api/v1/lan-fabric/rest/control/switches/{fabric_name}/overview`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricSummary() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.control.switches." + msg += f"{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + """ + - Override the path property to mandate fabric_name is set. + - Raise ``ValueError`` if fabric_name is not set. + """ + return f"{self.path_fabric_name}/overview" diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/rest.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/rest.py new file mode 100644 index 000000000..9f0ad2c0a --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/rest.py @@ -0,0 +1,43 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.lan_fabric import \ + LanFabric + + +class Rest(LanFabric): + """ + ## api.v1.lan_fabric.rest.Rest() + + ### Description + Common methods and properties for api.v1.lan_fabric.rest.Rest() subclasses. + + ### Path + - ``/api/v1/lan-fabric/rest`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.rest = f"{self.lan_fabric}/rest" + msg = f"ENTERED api.v1.lan_fabric.rest.{self.class_name}" + self.log.debug(msg) diff --git a/plugins/module_utils/common/api/v1/rest/__init__.py b/plugins/module_utils/common/api/v1/rest/__init__.py index 6036e1ead..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/rest/__init__.py +++ b/plugins/module_utils/common/api/v1/rest/__init__.py @@ -1,49 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1 import \ - V1 - - -class Rest(V1): - """ - ## V1 API Rest() - api.v1.rest.Rest() - - ### Description - Common methods and properties for Rest() subclasses. - - ### Path - - ``/api/v1/rest`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.rest = f"{self.v1}/rest" - msg = f"ENTERED api.v1.rest.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Populate class-specific properties. - """ diff --git a/plugins/module_utils/common/api/v1/rest/control/__init__.py b/plugins/module_utils/common/api/v1/rest/control/__init__.py index 0c77bea28..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/rest/control/__init__.py +++ b/plugins/module_utils/common/api/v1/rest/control/__init__.py @@ -1,49 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest import \ - Rest - - -class Control(Rest): - """ - ## V1 API Control() - api.v1.rest.control.Control() - - ### Description - Common methods and properties for Control() subclasses. - - ### Path - - ``/api/v1/rest/control`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.control = f"{self.rest}/control" - msg = f"ENTERED api.v1.LanFabric.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Set the fabric_name property. - """ diff --git a/plugins/module_utils/common/api/v1/rest/control/control.py b/plugins/module_utils/common/api/v1/rest/control/control.py new file mode 100644 index 000000000..84091d2c1 --- /dev/null +++ b/plugins/module_utils/common/api/v1/rest/control/control.py @@ -0,0 +1,49 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.rest import \ + Rest + + +class Control(Rest): + """ + ## V1 API Control() - api.v1.rest.control.Control() + + ### Description + Common methods and properties for Control() subclasses. + + ### Path + - ``/api/v1/rest/control`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.control = f"{self.rest}/control" + msg = f"ENTERED api.v1.LanFabric.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Set the fabric_name property. + """ diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics/__init__.py b/plugins/module_utils/common/api/v1/rest/control/fabrics/__init__.py index bdfa06605..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics/__init__.py +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics/__init__.py @@ -1,658 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import inspect -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control import \ - Control -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ - FabricTypes - - -class Fabrics(Control): - """ - ## V1 API Fabrics - api.v1.rest.control.fabrics.Fabrics() - - ### Description - Common methods and properties for Fabrics() subclasses. - - ### Path - - ``/rest/control/fabrics`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.fabric_types = FabricTypes() - self.fabrics = f"{self.control}/fabrics" - msg = f"ENTERED api.v1.rest.control.fabrics.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Set the fabric_name property. - """ - self.properties["fabric_name"] = None - self.properties["template_name"] = None - - @property - def fabric_name(self): - """ - - getter: Return the fabric_name. - - setter: Set the fabric_name. - - setter: Raise ``ValueError`` if fabric_name is not valid. - """ - return self.properties["fabric_name"] - - @fabric_name.setter - def fabric_name(self, value): - method_name = inspect.stack()[0][3] - try: - self.conversion.validate_fabric_name(value) - except (TypeError, ValueError) as error: - msg = f"{self.class_name}.{method_name}: " - msg += f"{error}" - raise ValueError(msg) from error - self.properties["fabric_name"] = value - - @property - def path_fabric_name(self): - """ - - Endpoint path property, including fabric_name. - - Raise ``ValueError`` if fabric_name is not set and - ``self.required_properties`` contains "fabric_name". - """ - method_name = inspect.stack()[0][3] - if self.fabric_name is None and "fabric_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be set prior to accessing path." - raise ValueError(msg) - return f"{self.fabrics}/{self.fabric_name}" - - @property - def path_fabric_name_template_name(self): - """ - - Endpoint path property, including fabric_name and template_name. - - Raise ``ValueError`` if fabric_name is not set and - ``self.required_properties`` contains "fabric_name". - - Raise ``ValueError`` if template_name is not set and - ``self.required_properties`` contains "template_name". - """ - method_name = inspect.stack()[0][3] - if self.fabric_name is None and "fabric_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be set prior to accessing path." - raise ValueError(msg) - if self.template_name is None and "template_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "template_name must be set prior to accessing path." - raise ValueError(msg) - return f"{self.fabrics}/{self.fabric_name}/{self.template_name}" - - @property - def template_name(self): - """ - - getter: Return the template_name. - - setter: Set the template_name. - - setter: Raise ``ValueError`` if template_name is not a string. - """ - return self.properties["template_name"] - - @template_name.setter - def template_name(self, value): - method_name = inspect.stack()[0][3] - if value not in self.fabric_types.valid_fabric_template_names: - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid template_name: {value}. " - msg += "Expected one of: " - msg += f"{', '.join(self.fabric_types.valid_fabric_template_names)}." - raise ValueError(msg) - self.properties["template_name"] = value - - -class EpFabricConfigDeploy(Fabrics): - """ - ## V1 API - Fabrics().EpFabricConfigDeploy() - - ### Description - Return endpoint to initiate config-deploy on fabric_name. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If force_show_run is not boolean. - - ``ValueError``: If include_all_msd_switches is not boolean. - - ### Path - - ``/fabrics/{fabric_name}/config-deploy`` - - ``/fabrics/{fabric_name}/config-deploy?forceShowRun={force_show_run}`` - - ``/fabrics/{fabric_name}/config-deploy?inclAllMSDSwitches={include_all_msd_switches}`` - - ### Verb - - POST - - ### Parameters - - force_show_run: boolean - - set the ``forceShowRun`` value - - default: False - - include_all_msd_switches: boolean - - set the ``inclAllMSDSwitches`` value - - default: False - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricConfigDeploy() - instance.fabric_name = "MyFabric" - instance.force_show_run = True - instance.include_all_msd_switches = True - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "POST" - self.properties["force_show_run"] = False - self.properties["include_all_msd_switches"] = False - - @property - def force_show_run(self): - """ - - getter: Return the force_show_run value. - - setter: Set the force_show_run value. - - setter: Raise ``ValueError`` if force_show_run is not a boolean. - - Default: False - """ - return self.properties["force_show_run"] - - @force_show_run.setter - def force_show_run(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += f"Expected boolean for {method_name}. " - msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) - self.properties["force_show_run"] = value - - @property - def include_all_msd_switches(self): - """ - - getter: Return the include_all_msd_switches. - - setter: Set the include_all_msd_switches. - - setter: Raise ``ValueError`` if include_all_msd_switches is a boolean. - - Default: False - """ - return self.properties["include_all_msd_switches"] - - @include_all_msd_switches.setter - def include_all_msd_switches(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += f"Expected boolean for {method_name}. " - msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) - self.properties["include_all_msd_switches"] = value - - @property - def path(self): - """ - - Override the path property to mandate fabric_name is set. - - Raise ``ValueError`` if fabric_name is not set. - """ - _path = self.path_fabric_name - _path += "/config-deploy?" - _path += f"forceShowRun={self.force_show_run}" - _path += f"&inclAllMSDSwitches={self.include_all_msd_switches}" - return _path - - -class EpFabricConfigSave(Fabrics): - """ - ## V1 API - Fabrics().EpFabricConfigSave() - - ### Description - Return endpoint to initiate config-save on fabric_name. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If ticket_id is not a string. - - ### Path - - ``/fabrics/{fabric_name}/config-save`` - - ``/fabrics/{fabric_name}/config-save?ticketId={ticket_id}`` - - ### Verb - - POST - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - ticket_id: string - - optional unless Change Control is enabled - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricConfigSave() - instance.fabric_name = "MyFabric" - instance.ticket_id = "MyTicket1234" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "POST" - self.properties["ticket_id"] = None - - @property - def ticket_id(self): - """ - - getter: Return the ticket_id. - - setter: Set the ticket_id. - - setter: Raise ``ValueError`` if ticket_id is not a string. - - Default: None - - Note: ticket_id is optional unless Change Control is enabled. - """ - return self.properties["ticket_id"] - - @ticket_id.setter - def ticket_id(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, str): - msg = f"{self.class_name}.{method_name}: " - msg += f"Expected string for {method_name}. " - msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) - self.properties["ticket_id"] = value - - @property - def path(self): - """ - - Endpoint for config-save. - - Set self.ticket_id if Change Control is enabled. - - Raise ``ValueError`` if fabric_name is not set. - """ - _path = self.path_fabric_name - _path += "/config-save" - if self.ticket_id: - _path += f"?ticketId={self.ticket_id}" - return _path - - -class EpFabricCreate(Fabrics): - """ - ## V1 API - Fabrics().EpFabricCreate() - - ### Description - Return endpoint information. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If template_name is not set. - - ``ValueError``: If template_name is not a valid fabric template name. - - ### Path - - ``/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` - - ### Verb - - POST - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - template_name: string - - set the ``template_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricCreate() - instance.fabric_name = "MyFabric" - instance.template_name = "Easy_Fabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self.required_properties.add("template_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "POST" - - @property - def path(self): - """ - - Endpoint for fabric create. - - Raise ``ValueError`` if fabric_name is not set. - """ - return self.path_fabric_name_template_name - - -class EpFabricDelete(Fabrics): - """ - ## V1 API - Fabrics().EpFabricDelete() - - ### Description - Return endpoint to delete ``fabric_name``. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/fabrics/{fabric_name}`` - - ### Verb - - DELETE - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricDelete() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "DELETE" - - @property - def path(self): - """ - - Endpoint for fabric delete. - - Raise ``ValueError`` if fabric_name is not set. - """ - return self.path_fabric_name - - -class EpFabricDetails(Fabrics): - """ - ## V1 API - Fabrics().EpFabricDetails() - - ### Description - Return the endpoint to query ``fabric_name`` details. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/fabrics/{fabric_name}`` - - ### Verb - - GET - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricDelete() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - return self.path_fabric_name - - -class EpFabricFreezeMode(Fabrics): - """ - ## V1 API - Fabrics().EpFabricFreezeMode() - - ### Description - Return the endpoint to query ``fabric_name`` freezemode status. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/fabrics/{fabric_name}/freezemode`` - - ### Verb - - GET - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricDelete() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - return f"{self.path_fabric_name}/freezemode" - - -# class EpFabricSummary() See module_utils/common/api/v1/rest/control/switches.py - - -class EpFabricUpdate(Fabrics): - """ - ## V1 API - Fabrics().EpFabricUpdate() - - ### Description - Return endpoint information. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If template_name is not set. - - ``ValueError``: If template_name is not a valid fabric template name. - - ### Path - - ``/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` - - ### Verb - - PUT - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - template_name: string - - set the ``template_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricUpdate() - instance.fabric_name = "MyFabric" - instance.template_name = "Easy_Fabric_IPFM" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self.required_properties.add("template_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "PUT" - - @property - def path(self): - """ - - Endpoint for fabric create. - - Raise ``ValueError`` if fabric_name is not set. - """ - return self.path_fabric_name_template_name - - -class EpFabrics(Fabrics): - """ - ## V1 API - Fabrics().EpFabrics() - - ### Description - Return the endpoint to query fabrics. - - ### Raises - - None - - ### Path - - ``/rest/control/fabrics`` - - ### Verb - - GET - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabrics() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - return self.fabrics diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics/fabrics.py new file mode 100644 index 000000000..332c5758b --- /dev/null +++ b/plugins/module_utils/common/api/v1/rest/control/fabrics/fabrics.py @@ -0,0 +1,658 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.control import \ + Control +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ + FabricTypes + + +class Fabrics(Control): + """ + ## V1 API Fabrics - api.v1.rest.control.fabrics.Fabrics() + + ### Description + Common methods and properties for Fabrics() subclasses. + + ### Path + - ``/rest/control/fabrics`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fabric_types = FabricTypes() + self.fabrics = f"{self.control}/fabrics" + msg = f"ENTERED api.v1.rest.control.fabrics.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Set the fabric_name property. + """ + self.properties["fabric_name"] = None + self.properties["template_name"] = None + + @property + def fabric_name(self): + """ + - getter: Return the fabric_name. + - setter: Set the fabric_name. + - setter: Raise ``ValueError`` if fabric_name is not valid. + """ + return self.properties["fabric_name"] + + @fabric_name.setter + def fabric_name(self, value): + method_name = inspect.stack()[0][3] + try: + self.conversion.validate_fabric_name(value) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) from error + self.properties["fabric_name"] = value + + @property + def path_fabric_name(self): + """ + - Endpoint path property, including fabric_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.fabrics}/{self.fabric_name}" + + @property + def path_fabric_name_template_name(self): + """ + - Endpoint path property, including fabric_name and template_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + - Raise ``ValueError`` if template_name is not set and + ``self.required_properties`` contains "template_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + if self.template_name is None and "template_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "template_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.fabrics}/{self.fabric_name}/{self.template_name}" + + @property + def template_name(self): + """ + - getter: Return the template_name. + - setter: Set the template_name. + - setter: Raise ``ValueError`` if template_name is not a string. + """ + return self.properties["template_name"] + + @template_name.setter + def template_name(self, value): + method_name = inspect.stack()[0][3] + if value not in self.fabric_types.valid_fabric_template_names: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid template_name: {value}. " + msg += "Expected one of: " + msg += f"{', '.join(self.fabric_types.valid_fabric_template_names)}." + raise ValueError(msg) + self.properties["template_name"] = value + + +class EpFabricConfigDeploy(Fabrics): + """ + ## V1 API - Fabrics().EpFabricConfigDeploy() + + ### Description + Return endpoint to initiate config-deploy on fabric_name. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If force_show_run is not boolean. + - ``ValueError``: If include_all_msd_switches is not boolean. + + ### Path + - ``/fabrics/{fabric_name}/config-deploy`` + - ``/fabrics/{fabric_name}/config-deploy?forceShowRun={force_show_run}`` + - ``/fabrics/{fabric_name}/config-deploy?inclAllMSDSwitches={include_all_msd_switches}`` + + ### Verb + - POST + + ### Parameters + - force_show_run: boolean + - set the ``forceShowRun`` value + - default: False + - include_all_msd_switches: boolean + - set the ``inclAllMSDSwitches`` value + - default: False + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricConfigDeploy() + instance.fabric_name = "MyFabric" + instance.force_show_run = True + instance.include_all_msd_switches = True + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + self.properties["force_show_run"] = False + self.properties["include_all_msd_switches"] = False + + @property + def force_show_run(self): + """ + - getter: Return the force_show_run value. + - setter: Set the force_show_run value. + - setter: Raise ``ValueError`` if force_show_run is not a boolean. + - Default: False + """ + return self.properties["force_show_run"] + + @force_show_run.setter + def force_show_run(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected boolean for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["force_show_run"] = value + + @property + def include_all_msd_switches(self): + """ + - getter: Return the include_all_msd_switches. + - setter: Set the include_all_msd_switches. + - setter: Raise ``ValueError`` if include_all_msd_switches is a boolean. + - Default: False + """ + return self.properties["include_all_msd_switches"] + + @include_all_msd_switches.setter + def include_all_msd_switches(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected boolean for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["include_all_msd_switches"] = value + + @property + def path(self): + """ + - Override the path property to mandate fabric_name is set. + - Raise ``ValueError`` if fabric_name is not set. + """ + _path = self.path_fabric_name + _path += "/config-deploy?" + _path += f"forceShowRun={self.force_show_run}" + _path += f"&inclAllMSDSwitches={self.include_all_msd_switches}" + return _path + + +class EpFabricConfigSave(Fabrics): + """ + ## V1 API - Fabrics().EpFabricConfigSave() + + ### Description + Return endpoint to initiate config-save on fabric_name. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If ticket_id is not a string. + + ### Path + - ``/fabrics/{fabric_name}/config-save`` + - ``/fabrics/{fabric_name}/config-save?ticketId={ticket_id}`` + + ### Verb + - POST + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - ticket_id: string + - optional unless Change Control is enabled + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricConfigSave() + instance.fabric_name = "MyFabric" + instance.ticket_id = "MyTicket1234" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + self.properties["ticket_id"] = None + + @property + def ticket_id(self): + """ + - getter: Return the ticket_id. + - setter: Set the ticket_id. + - setter: Raise ``ValueError`` if ticket_id is not a string. + - Default: None + - Note: ticket_id is optional unless Change Control is enabled. + """ + return self.properties["ticket_id"] + + @ticket_id.setter + def ticket_id(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected string for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["ticket_id"] = value + + @property + def path(self): + """ + - Endpoint for config-save. + - Set self.ticket_id if Change Control is enabled. + - Raise ``ValueError`` if fabric_name is not set. + """ + _path = self.path_fabric_name + _path += "/config-save" + if self.ticket_id: + _path += f"?ticketId={self.ticket_id}" + return _path + + +class EpFabricCreate(Fabrics): + """ + ## V1 API - Fabrics().EpFabricCreate() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If template_name is not set. + - ``ValueError``: If template_name is not a valid fabric template name. + + ### Path + - ``/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` + + ### Verb + - POST + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - template_name: string + - set the ``template_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricCreate() + instance.fabric_name = "MyFabric" + instance.template_name = "Easy_Fabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self.required_properties.add("template_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "POST" + + @property + def path(self): + """ + - Endpoint for fabric create. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name_template_name + + +class EpFabricDelete(Fabrics): + """ + ## V1 API - Fabrics().EpFabricDelete() + + ### Description + Return endpoint to delete ``fabric_name``. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}`` + + ### Verb + - DELETE + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "DELETE" + + @property + def path(self): + """ + - Endpoint for fabric delete. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name + + +class EpFabricDetails(Fabrics): + """ + ## V1 API - Fabrics().EpFabricDetails() + + ### Description + Return the endpoint to query ``fabric_name`` details. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return self.path_fabric_name + + +class EpFabricFreezeMode(Fabrics): + """ + ## V1 API - Fabrics().EpFabricFreezeMode() + + ### Description + Return the endpoint to query ``fabric_name`` freezemode status. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/fabrics/{fabric_name}/freezemode`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricDelete() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return f"{self.path_fabric_name}/freezemode" + + +# class EpFabricSummary() See module_utils/common/api/v1/rest/control/switches.py + + +class EpFabricUpdate(Fabrics): + """ + ## V1 API - Fabrics().EpFabricUpdate() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If template_name is not set. + - ``ValueError``: If template_name is not a valid fabric template name. + + ### Path + - ``/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` + + ### Verb + - PUT + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - template_name: string + - set the ``template_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricUpdate() + instance.fabric_name = "MyFabric" + instance.template_name = "Easy_Fabric_IPFM" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self.required_properties.add("template_name") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "PUT" + + @property + def path(self): + """ + - Endpoint for fabric create. + - Raise ``ValueError`` if fabric_name is not set. + """ + return self.path_fabric_name_template_name + + +class EpFabrics(Fabrics): + """ + ## V1 API - Fabrics().EpFabrics() + + ### Description + Return the endpoint to query fabrics. + + ### Raises + - None + + ### Path + - ``/rest/control/fabrics`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabrics() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + return self.fabrics diff --git a/plugins/module_utils/common/api/v1/rest/control/switches/__init__.py b/plugins/module_utils/common/api/v1/rest/control/switches/__init__.py index 7da36b32c..e69de29bb 100644 --- a/plugins/module_utils/common/api/v1/rest/control/switches/__init__.py +++ b/plugins/module_utils/common/api/v1/rest/control/switches/__init__.py @@ -1,140 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import inspect -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control import \ - Control - - -class Switches(Control): - """ - ## V1 API Switches() - api.v1.rest.control.switches.Switches() - - ### Description - Common methods and properties for Fabrics() subclasses. - - ### Path - - ``/lan-fabric/rest/control/switches/{fabric_name}`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.switches = f"{self.control}/switches" - msg = f"ENTERED api.v1.rest.control.switches.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - Populate properties specific to this class and its subclasses. - """ - self.properties["fabric_name"] = None - - @property - def fabric_name(self): - """ - - getter: Return the fabric_name. - - setter: Set the fabric_name. - - setter: Raise ``ValueError`` if fabric_name is not valid. - """ - return self.properties["fabric_name"] - - @fabric_name.setter - def fabric_name(self, value): - method_name = inspect.stack()[0][3] - try: - self.conversion.validate_fabric_name(value) - except (TypeError, ValueError) as error: - msg = f"{self.class_name}.{method_name}: " - msg += f"{error}" - raise ValueError(msg) from error - self.properties["fabric_name"] = value - - @property - def path_fabric_name(self): - """ - - Endpoint path property, including fabric_name. - - Raise ``ValueError`` if fabric_name is not set and - ``self.required_properties`` contains "fabric_name". - """ - method_name = inspect.stack()[0][3] - if self.fabric_name is None and "fabric_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be set prior to accessing path." - raise ValueError(msg) - return f"{self.switches}/{self.fabric_name}" - - -class EpFabricSummary(Switches): - """ - ## V1 API - api.v1.rest.control.switches.EpFabricSummary() - - ### Description - Return endpoint information. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/switches/{fabric_name}/overview`` - - ### Verb - - GET - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricSummary() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.rest.control.switches.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - """ - - Override the path property to mandate fabric_name is set. - - Raise ``ValueError`` if fabric_name is not set. - """ - return f"{self.path_fabric_name}/overview" diff --git a/plugins/module_utils/common/api/v1/rest/control/switches/switches.py b/plugins/module_utils/common/api/v1/rest/control/switches/switches.py new file mode 100644 index 000000000..a6b1317c3 --- /dev/null +++ b/plugins/module_utils/common/api/v1/rest/control/switches/switches.py @@ -0,0 +1,140 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.control import \ + Control + + +class Switches(Control): + """ + ## V1 API Switches() - api.v1.rest.control.switches.Switches() + + ### Description + Common methods and properties for Fabrics() subclasses. + + ### Path + - ``/lan-fabric/rest/control/switches/{fabric_name}`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.switches = f"{self.control}/switches" + msg = f"ENTERED api.v1.rest.control.switches.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + Populate properties specific to this class and its subclasses. + """ + self.properties["fabric_name"] = None + + @property + def fabric_name(self): + """ + - getter: Return the fabric_name. + - setter: Set the fabric_name. + - setter: Raise ``ValueError`` if fabric_name is not valid. + """ + return self.properties["fabric_name"] + + @fabric_name.setter + def fabric_name(self, value): + method_name = inspect.stack()[0][3] + try: + self.conversion.validate_fabric_name(value) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) from error + self.properties["fabric_name"] = value + + @property + def path_fabric_name(self): + """ + - Endpoint path property, including fabric_name. + - Raise ``ValueError`` if fabric_name is not set and + ``self.required_properties`` contains "fabric_name". + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None and "fabric_name" in self.required_properties: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + return f"{self.switches}/{self.fabric_name}" + + +class EpFabricSummary(Switches): + """ + ## V1 API - api.v1.rest.control.switches.EpFabricSummary() + + ### Description + Return endpoint information. + + ### Raises + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + + ### Path + - ``/switches/{fabric_name}/overview`` + + ### Verb + - GET + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpFabricSummary() + instance.fabric_name = "MyFabric" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self._build_properties() + msg = f"ENTERED api.v1.rest.control.switches.{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + """ + - Override the path property to mandate fabric_name is set. + - Raise ``ValueError`` if fabric_name is not set. + """ + return f"{self.path_fabric_name}/overview" diff --git a/plugins/module_utils/common/api/v1/rest/rest.py b/plugins/module_utils/common/api/v1/rest/rest.py new file mode 100644 index 000000000..609976cc7 --- /dev/null +++ b/plugins/module_utils/common/api/v1/rest/rest.py @@ -0,0 +1,49 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.v1 import \ + V1 + + +class Rest(V1): + """ + ## V1 API Rest() - api.v1.rest.Rest() + + ### Description + Common methods and properties for Rest() subclasses. + + ### Path + - ``/api/v1/rest`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.rest = f"{self.v1}/rest" + msg = f"ENTERED api.v1.rest.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + - Populate class-specific properties. + """ diff --git a/plugins/module_utils/common/api/v1/v1.py b/plugins/module_utils/common/api/v1/v1.py new file mode 100644 index 000000000..6dad6fa37 --- /dev/null +++ b/plugins/module_utils/common/api/v1/v1.py @@ -0,0 +1,41 @@ +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.api import Api + + +class V1(Api): + """ + ## v1 API enpoints - Api().V1() + + ### Description + Common methods and properties for API v1 subclasses. + + ### Path + ``/appcenter/cisco/ndfc/api/v1/`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED api.v1.V1()") + self.v1 = f"{self.api}/v1" diff --git a/plugins/module_utils/common/controller_features.py b/plugins/module_utils/common/controller_features.py index 930efe4ee..ab3338fbf 100644 --- a/plugins/module_utils/common/controller_features.py +++ b/plugins/module_utils/common/controller_features.py @@ -27,7 +27,7 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm.fm import \ EpFeatures from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/plugins/module_utils/fabric/config_deploy.py b/plugins/module_utils/fabric/config_deploy.py index 9d5975c37..bf15d34d1 100644 --- a/plugins/module_utils/fabric/config_deploy.py +++ b/plugins/module_utils/fabric/config_deploy.py @@ -22,7 +22,7 @@ import logging from typing import Dict -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabricConfigDeploy from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/plugins/module_utils/fabric/config_save.py b/plugins/module_utils/fabric/config_save.py index 9c0b65fe8..6cb8f99e3 100644 --- a/plugins/module_utils/fabric/config_save.py +++ b/plugins/module_utils/fabric/config_save.py @@ -22,7 +22,7 @@ import logging from typing import Dict -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabricConfigSave from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/plugins/module_utils/fabric/create.py b/plugins/module_utils/fabric/create.py index 33f11d2f3..cdf4cb43f 100644 --- a/plugins/module_utils/fabric/create.py +++ b/plugins/module_utils/fabric/create.py @@ -23,7 +23,7 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabricCreate from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.common import \ FabricCommon diff --git a/plugins/module_utils/fabric/delete.py b/plugins/module_utils/fabric/delete.py index 96c992b54..8958720ee 100644 --- a/plugins/module_utils/fabric/delete.py +++ b/plugins/module_utils/fabric/delete.py @@ -20,7 +20,7 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabricDelete from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError diff --git a/plugins/module_utils/fabric/fabric_details.py b/plugins/module_utils/fabric/fabric_details.py index 23a1416d1..f7cfc6007 100644 --- a/plugins/module_utils/fabric/fabric_details.py +++ b/plugins/module_utils/fabric/fabric_details.py @@ -22,7 +22,7 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/plugins/module_utils/fabric/fabric_summary.py b/plugins/module_utils/fabric/fabric_summary.py index 0cd82f210..7d8ae01c1 100644 --- a/plugins/module_utils/fabric/fabric_summary.py +++ b/plugins/module_utils/fabric/fabric_summary.py @@ -23,7 +23,7 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.switches import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.switches.switches import \ EpFabricSummary from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/plugins/module_utils/fabric/replaced.py b/plugins/module_utils/fabric/replaced.py index 2c72b4e3f..88b6ad8db 100644 --- a/plugins/module_utils/fabric/replaced.py +++ b/plugins/module_utils/fabric/replaced.py @@ -23,7 +23,7 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabricUpdate from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError diff --git a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py index be35238b1..5ed96bd84 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py +++ b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py @@ -18,7 +18,7 @@ import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import ( EpFabricConfigDeploy, EpFabricConfigSave, EpFabricCreate, EpFabricDelete, EpFabricDetails, EpFabricFreezeMode, EpFabricUpdate) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ diff --git a/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py b/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py index 63e0b515d..ab0785d15 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py +++ b/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py @@ -17,7 +17,7 @@ __metaclass__ = type -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.imagemgnt import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.imagemgnt.imagemgnt import \ EpBootFlashInfo from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise diff --git a/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py b/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py index 2556322d2..1e49fd61f 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py +++ b/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py @@ -17,7 +17,7 @@ __metaclass__ = type -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.imageupgrade import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.imageupgrade.imageupgrade import ( EpInstallOptions, EpUpgradeImage) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise diff --git a/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py b/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py index 72608daf8..ff66de1b3 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py +++ b/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py @@ -18,7 +18,7 @@ import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.policymgnt import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.policymgnt.policymgnt import ( EpPolicies, EpPoliciesAllAttached, EpPolicyAttach, EpPolicyCreate, EpPolicyDetach, EpPolicyInfo) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ diff --git a/tests/unit/module_utils/common/api/test_v1_api_staging_management.py b/tests/unit/module_utils/common/api/test_v1_api_staging_management.py index 8635a0d59..8bb951c05 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_staging_management.py +++ b/tests/unit/module_utils/common/api/test_v1_api_staging_management.py @@ -17,7 +17,7 @@ __metaclass__ = type -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.stagingmanagement import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.stagingmanagement.stagingmanagement import ( EpImageStage, EpImageValidate, EpStageInfo) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise diff --git a/tests/unit/module_utils/common/api/test_v1_api_switches.py b/tests/unit/module_utils/common/api/test_v1_api_switches.py index 85d1f3161..a654f846d 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_switches.py +++ b/tests/unit/module_utils/common/api/test_v1_api_switches.py @@ -18,7 +18,7 @@ import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.switches import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.switches.switches import \ EpFabricSummary from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise diff --git a/tests/unit/module_utils/common/api/test_v1_api_templates.py b/tests/unit/module_utils/common/api/test_v1_api_templates.py index 4728677b5..bdedf18f9 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_templates.py +++ b/tests/unit/module_utils/common/api/test_v1_api_templates.py @@ -18,7 +18,7 @@ import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest.config.templates import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest.config.templates.templates import ( EpTemplate, EpTemplates) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise diff --git a/tests/unit/module_utils/common/test_controller_features.py b/tests/unit/module_utils/common/test_controller_features.py index 3f171ea9b..2a7ad7408 100644 --- a/tests/unit/module_utils/common/test_controller_features.py +++ b/tests/unit/module_utils/common/test_controller_features.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm.fm import ( EpFeatures, ) from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import ( diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py index e1b75e8f5..5726aaefc 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabricConfigDeploy from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py index bbbe55d11..7170a9d30 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabricConfigSave from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py index 6327982fa..e4a3a3d5b 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_common.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabricCreate from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py index 564b82f08..079ad6f94 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabricDelete from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py index 0b54e58e1..356b3eb75 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py index fb4b19b37..a54e9c8f0 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py index 305a9bb48..a31f7a19b 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py index fdbf6264b..2eececc72 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ EpFabricUpdate from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py index a929dd62f..dcc6ec8fd 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py @@ -32,7 +32,7 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.switches import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.switches.switches import \ EpFabricSummary from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils From 5401a3623bcfb389ee3f5385e75ac9c5b63d391f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 17 May 2024 09:18:28 -1000 Subject: [PATCH 050/230] TemplateGetAll(): use EpTemplates() 1. TemplateGetAll(): use EpTemplates() for endpoint resolution 2. TemplateGetAll(): Modify rest_send, and results properties not to need RestSend() and Results() classes when verifying their input values. Remove RestSend() and Results() imports. --- .../module_utils/fabric/template_get_all.py | 67 ++++++++----------- .../dcnm/dcnm_fabric/test_template_get_all.py | 58 ++++------------ 2 files changed, 39 insertions(+), 86 deletions(-) diff --git a/plugins/module_utils/fabric/template_get_all.py b/plugins/module_utils/fabric/template_get_all.py index 085bf0184..a147a1650 100644 --- a/plugins/module_utils/fabric/template_get_all.py +++ b/plugins/module_utils/fabric/template_get_all.py @@ -23,16 +23,10 @@ import logging from typing import Any, Dict +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest.config.templates.templates import \ + EpTemplates from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError -# Used only to verify RestSend instance in rest_send property setter -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend -# Used only to verify RestSend instance in rest_send property setter -from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ - Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints class TemplateGetAll: @@ -62,9 +56,7 @@ def __init__(self): msg = "ENTERED TemplateGetAll(): " self.log.debug(msg) - self.endpoints = ApiEndpoints() - self.path = None - self.verb = None + self.ep_templates = EpTemplates() self.response = [] self.response_current = {} @@ -79,22 +71,6 @@ def _init_properties(self) -> None: self._properties["results"] = None self._properties["templates"] = None - def _set_templates_endpoint(self) -> None: - """ - - Set the endpoint for the template to be retrieved from - the controller. - - Raise ``ValueError`` if the endpoint assignment fails. - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - - try: - endpoint = self.endpoints.templates - except ValueError as error: - raise ValueError(error) from error - - self.path = endpoint.get("path") - self.verb = endpoint.get("verb") - def refresh(self): """ - Retrieve the templates from the controller. @@ -104,11 +80,6 @@ def refresh(self): """ method_name = inspect.stack()[0][3] - try: - self._set_templates_endpoint() - except ValueError as error: - raise ValueError(error) from error - if self.rest_send is None: msg = f"{self.class_name}.{method_name}: " msg += "Set instance.rest_send property before " @@ -116,8 +87,8 @@ def refresh(self): self.log.debug(msg) raise ValueError(msg) - self.rest_send.path = self.path - self.rest_send.verb = self.verb + self.rest_send.path = self.ep_templates.path + self.rest_send.verb = self.ep_templates.verb self.rest_send.check_mode = False self.rest_send.commit() @@ -156,9 +127,17 @@ def rest_send(self): @rest_send.setter def rest_send(self, value): method_name = inspect.stack()[0][3] - if not isinstance(value, RestSend): - msg = f"{self.class_name}.{method_name}: " - msg += "rest_send must be an instance of RestSend." + msg = f"{self.class_name}.{method_name}: " + msg += "value must be an instance of RestSend. " + msg += f"Got value {value} of type {type(value).__name__}." + _class_name = None + try: + _class_name = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + self.log.debug(msg) + raise TypeError(msg) from error + if _class_name != "RestSend": self.log.debug(msg) raise TypeError(msg) self._properties["rest_send"] = value @@ -176,9 +155,17 @@ def results(self): @results.setter def results(self, value): method_name = inspect.stack()[0][3] - if not isinstance(value, Results): - msg = f"{self.class_name}.{method_name}: " - msg += "results must be an instance of Results." + msg = f"{self.class_name}.{method_name}: " + msg += "value must be an instance of Results. " + msg += f"Got value {value} of type {type(value).__name__}." + _class_name = None + try: + _class_name = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + self.log.debug(msg) + raise TypeError(msg) from error + if _class_name != "Results": self.log.debug(msg) raise TypeError(msg) self._properties["results"] = value diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py index 2963e56b4..0f8198eb2 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py @@ -32,6 +32,8 @@ import inspect import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest.config.templates.templates import \ + EpTemplates from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ @@ -58,9 +60,7 @@ def test_template_get_all_00010(template_get_all) -> None: with does_not_raise(): instance = template_get_all assert instance.class_name == "TemplateGetAll" - assert isinstance(instance.endpoints, ApiEndpoints) - assert instance.path is None - assert instance.verb is None + assert isinstance(instance.ep_templates, EpTemplates) assert instance.response == [] assert instance.response_current == {} assert instance.result == [] @@ -71,7 +71,8 @@ def test_template_get_all_00010(template_get_all) -> None: MATCH_00020 = r"TemplateGetAll\.rest_send: " -MATCH_00020 += r"rest_send must be an instance of RestSend\." +MATCH_00020 += r"value must be an instance of RestSend.\s+" +MATCH_00020 += r"Got value .* of type .*\." @pytest.mark.parametrize( @@ -109,14 +110,19 @@ def test_template_get_all_00020(template_get_all, value, expected, raised) -> No MATCH_00030 = r"TemplateGetAll\.results: " -MATCH_00030 += r"results must be an instance of Results\." +MATCH_00030 += r"value must be an instance of Results.\s+" +MATCH_00030 += r"Got value .* of type .*\." @pytest.mark.parametrize( "value, expected, raised", [ (Results(), does_not_raise(), False), - (MockAnsibleModule(), pytest.raises(TypeError, match=MATCH_00030), True), + ( + RestSend(MockAnsibleModule()), + pytest.raises(TypeError, match=MATCH_00030), + True, + ), (None, pytest.raises(TypeError, match=MATCH_00030), True), ("foo", pytest.raises(TypeError, match=MATCH_00030), True), (10, pytest.raises(TypeError, match=MATCH_00030), True), @@ -308,43 +314,3 @@ def mock_dcnm_send(*args, **kwargs): assert len(instance.result) == 1 assert instance.result_current.get("success", None) is True assert instance.result_current.get("found", None) is True - - -def test_template_get_all_00070(monkeypatch, template_get_all) -> None: - """ - Classes and Methods - - TemplateGetAll - - __init__() - - _set_template_endpoint() - - Summary - - Verify that TemplateGetAll()._set_templates_endpoint() re-raises - ``ValueError`` when ApiEndpoints() raises ``ValueError``. - """ - - class MockApiEndpoints: # pylint: disable=too-few-public-methods - """ - Mock the ApiEndpoints.templates getter property to raise ``ValueError``. - """ - - @property - def templates(self): - """ - - Mocked property getter. - - Raise ``ValueError``. - """ - print("GETTER EXCEPTION") - raise ValueError("mocked ApiEndpoints().templates getter exception") - - PATCH_API_ENDPOINTS = "ansible_collections.cisco.dcnm.plugins." - PATCH_API_ENDPOINTS += "module_utils.fabric.endpoints.ApiEndpoints.templates" - - match = r"mocked ApiEndpoints\(\)\.templates getter exception" - - with does_not_raise(): - instance = template_get_all - instance.results = Results() - instance.rest_send = RestSend(MockAnsibleModule()) - monkeypatch.setattr(instance, "endpoints", MockApiEndpoints()) - with pytest.raises(ValueError, match=match): - instance.refresh() From 14252cb870e7b1dc32c749b139d48b8623df6c85 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 17 May 2024 09:50:07 -1000 Subject: [PATCH 051/230] TemplateGet(): use EpTemplate() 1. TemplateGet(): use EpTemplate() for endpoint resolution 2. TemplateGet(): Modify rest_send, and results properties not to need RestSend() and Results() classes when verifying their input values. Remove RestSend() and Results() imports. --- plugins/module_utils/fabric/template_get.py | 54 ++++++++++--------- .../dcnm/dcnm_fabric/test_template_get.py | 38 +++++-------- 2 files changed, 43 insertions(+), 49 deletions(-) diff --git a/plugins/module_utils/fabric/template_get.py b/plugins/module_utils/fabric/template_get.py index 4a83ea942..058f02ab5 100644 --- a/plugins/module_utils/fabric/template_get.py +++ b/plugins/module_utils/fabric/template_get.py @@ -23,16 +23,10 @@ import logging from typing import Any, Dict +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest.config.templates.templates import \ + EpTemplate from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError -# Used only to verify RestSend instance in rest_send property setter -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend -# Used only to verify RestSend instance in rest_send property setter -from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ - Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints class TemplateGet: @@ -63,9 +57,7 @@ def __init__(self): msg = "ENTERED TemplateGet(): " self.log.debug(msg) - self.endpoints = ApiEndpoints() - self.path = None - self.verb = None + self.ep_template = EpTemplate() self.response = [] self.response_current = {} @@ -95,15 +87,11 @@ def _set_template_endpoint(self) -> None: self.log.error(msg) raise ValueError(msg) - self.endpoints.template_name = self.template_name try: - endpoint = self.endpoints.template - except ValueError as error: + self.ep_template.template_name = self.template_name + except TypeError as error: raise ValueError(error) from error - self.path = endpoint.get("path") - self.verb = endpoint.get("verb") - def refresh(self): """ - Retrieve the template from the controller. @@ -124,8 +112,8 @@ def refresh(self): self.log.debug(msg) raise ValueError(msg) - self.rest_send.path = self.path - self.rest_send.verb = self.verb + self.rest_send.path = self.ep_template.path + self.rest_send.verb = self.ep_template.verb self.rest_send.check_mode = False self.rest_send.timeout = 2 self.rest_send.commit() @@ -163,9 +151,17 @@ def rest_send(self): @rest_send.setter def rest_send(self, value): method_name = inspect.stack()[0][3] - if not isinstance(value, RestSend): - msg = f"{self.class_name}.{method_name}: " - msg += "rest_send must be an instance of RestSend." + msg = f"{self.class_name}.{method_name}: " + msg += "value must be an instance of RestSend. " + msg += f"Got value {value} of type {type(value).__name__}." + _class_name = None + try: + _class_name = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + self.log.debug(msg) + raise TypeError(msg) from error + if _class_name != "RestSend": self.log.debug(msg) raise TypeError(msg) self._properties["rest_send"] = value @@ -183,9 +179,17 @@ def results(self): @results.setter def results(self, value): method_name = inspect.stack()[0][3] - if not isinstance(value, Results): - msg = f"{self.class_name}.{method_name}: " - msg += "results must be an instance of Results." + msg = f"{self.class_name}.{method_name}: " + msg += "value must be an instance of Results. " + msg += f"Got value {value} of type {type(value).__name__}." + _class_name = None + try: + _class_name = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + self.log.debug(msg) + raise TypeError(msg) from error + if _class_name != "Results": self.log.debug(msg) raise TypeError(msg) self._properties["results"] = value diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py index a43bb2a74..06d2c24d4 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py @@ -32,6 +32,8 @@ import inspect import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.configtemplate.rest.config.templates.templates import \ + EpTemplate from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ @@ -58,9 +60,7 @@ def test_template_get_00010(template_get) -> None: with does_not_raise(): instance = template_get assert instance.class_name == "TemplateGet" - assert isinstance(instance.endpoints, ApiEndpoints) - assert instance.path is None - assert instance.verb is None + assert isinstance(instance.ep_template, EpTemplate) assert instance.response == [] assert instance.response_current == {} assert instance.result == [] @@ -72,7 +72,8 @@ def test_template_get_00010(template_get) -> None: MATCH_00020 = r"TemplateGet\.rest_send: " -MATCH_00020 += r"rest_send must be an instance of RestSend\." +MATCH_00020 += r"value must be an instance of RestSend.\s+" +MATCH_00020 += r"Got value .* of type .*\." @pytest.mark.parametrize( @@ -110,7 +111,8 @@ def test_template_get_00020(template_get, value, expected, raised) -> None: MATCH_00030 = r"TemplateGet\.results: " -MATCH_00030 += r"results must be an instance of Results\." +MATCH_00030 += r"value must be an instance of Results.\s+" +MATCH_00030 += r"Got value .* of type .*\." @pytest.mark.parametrize( @@ -388,44 +390,32 @@ def test_template_get_00070(monkeypatch, template_get) -> None: Summary - Verify that TemplateGet()._set_template_endpoint() re-raises - ``ValueError`` when ApiEndpoints() raises ``ValueError``. + ``ValueError`` when EpTemplate() raises ``ValueError``. """ - class MockApiEndpoints: # pylint: disable=too-few-public-methods + class MockEpTemplate: # pylint: disable=too-few-public-methods """ - Mock the ApiEndpoints.template getter property to raise ``ValueError``. + Mock the EpTemplate.template_name setter property to raise ``ValueError``. """ - @property - def template(self): - """ - - Mocked property getter. - - Raise ``ValueError``. - """ - raise ValueError("mocked ApiEndpoints().template getter exception") - @property def template_name(self): """ - Mocked template_name property getter """ - return self._template_name @template_name.setter def template_name(self, value): """ - Mocked template_name property setter """ - self._template_name = value - - PATCH_API_ENDPOINTS = "ansible_collections.cisco.dcnm.plugins." - PATCH_API_ENDPOINTS += "module_utils.fabric.endpoints.ApiEndpoints.template_name" + raise ValueError("mocked EpTemplate().template_name setter exception.") - match = r"mocked ApiEndpoints\(\)\.template getter exception" + match = r"mocked EpTemplate\(\)\.template_name setter exception\." with does_not_raise(): instance = template_get - monkeypatch.setattr(instance, "endpoints", MockApiEndpoints()) - instance.template_name = "Easy_Fabric" + monkeypatch.setattr(instance, "ep_template", MockEpTemplate()) with pytest.raises(ValueError, match=match): + instance.template_name = "Easy_Fabric" # pylint: disable=pointless-statement instance._set_template_endpoint() From 57c0916c0fb489cc1ac2d5f1aac639b035d4a5fe Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 17 May 2024 10:12:51 -1000 Subject: [PATCH 052/230] ControllerVersion(): Use EpVersion 1. ControllerVersion(): Use EpVersion for endpoint resolution. 2. ControllerVersion(): remove module docstring for consistency with other modules. 3. test_controller_version.py: run through black, isort, pylint. --- .../module_utils/common/controller_version.py | 35 ++++++++----------- .../common/test_controller_version.py | 1 - 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/plugins/module_utils/common/controller_version.py b/plugins/module_utils/common/controller_version.py index 3a79bc985..7ae26652d 100644 --- a/plugins/module_utils/common/controller_version.py +++ b/plugins/module_utils/common/controller_version.py @@ -1,6 +1,3 @@ -""" -Class to retrieve and return information about an NDFC controller -""" # # Copyright (c) 2024 Cisco and/or its affiliates. # @@ -24,8 +21,8 @@ import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.api_endpoints import \ - ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm.fm import \ + EpVersion from ansible_collections.cisco.dcnm.plugins.module_utils.image_upgrade.image_upgrade_common import \ ImageUpgradeCommon from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ @@ -36,24 +33,21 @@ class ControllerVersion(ImageUpgradeCommon): """ Return image version information from the Controller - NOTES: - 1. considered using dcnm_version_supported() but it does not return - minor release info, which is needed due to key changes between - 12.1.2e and 12.1.3b. For example, see ImageStage().commit() - - Endpoint: - /appcenter/cisco/ndfc/api/v1/fm/about/version - - Usage (where module is an instance of AnsibleModule): + ### Endpoint + ``/appcenter/cisco/ndfc/api/v1/fm/about/version`` + ### Usage (where module is an instance of AnsibleModule): + ```python instance = ControllerVersion(module) instance.refresh() if instance.version == "12.1.2e": - do 12.1.2e stuff + # do 12.1.2e stuff else: - do other stuff + # do other stuff + ``` - Response: + ### Response + ```json { "version": "12.1.2e", "mode": "LAN", @@ -64,6 +58,7 @@ class ControllerVersion(ImageUpgradeCommon): "uuid": "f49e6088-ad4f-4406-bef6-2419de914ff1", "is_upgrade_inprogress": false } + ``` """ def __init__(self, ansible_module): @@ -73,7 +68,7 @@ def __init__(self, ansible_module): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED ControllerVersion()") - self.endpoints = ApiEndpoints() + self.ep_version = EpVersion() self._init_properties() def _init_properties(self): @@ -86,8 +81,8 @@ def refresh(self): """ Refresh self.response_data with current version info from the Controller """ - path = self.endpoints.controller_version.get("path") - verb = self.endpoints.controller_version.get("verb") + path = self.ep_version.path + verb = self.ep_version.verb self.properties["response"] = dcnm_send(self.ansible_module, verb, path) self.properties["result"] = self._handle_response(self.response, verb) diff --git a/tests/unit/module_utils/common/test_controller_version.py b/tests/unit/module_utils/common/test_controller_version.py index ff108d6b6..3bae3c9bd 100644 --- a/tests/unit/module_utils/common/test_controller_version.py +++ b/tests/unit/module_utils/common/test_controller_version.py @@ -31,7 +31,6 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson - from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( controller_version_fixture, responses_controller_version) From 52a877f7429bc71a1fcbc3baa1459f61983a2620 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 17 May 2024 10:49:06 -1000 Subject: [PATCH 053/230] FabricUpdateCommon(): Use EpFabricUpdate() 1. FabricUpdateCommon(): use EpFabricUpdate() for endpoint resolution. 2. test_fabric_updatee_bulk.py: Update unit tests to reflect 1 above. --- plugins/module_utils/fabric/update.py | 20 ++++++------- .../dcnm_fabric/test_fabric_update_bulk.py | 29 +++++++++++-------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/plugins/module_utils/fabric/update.py b/plugins/module_utils/fabric/update.py index 27fb4cb81..6689d92be 100644 --- a/plugins/module_utils/fabric/update.py +++ b/plugins/module_utils/fabric/update.py @@ -23,12 +23,12 @@ import json import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ + EpFabricUpdate from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.common import \ FabricCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ FabricTypes @@ -47,7 +47,7 @@ def __init__(self, params): self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.endpoints = ApiEndpoints() + self.ep_fabric_update = EpFabricUpdate() self.fabric_types = FabricTypes() msg = "ENTERED FabricUpdateCommon(): " @@ -253,26 +253,26 @@ def _set_fabric_update_endpoint(self, payload): - Set the endpoint for the fabric create API call. - raise ``ValueError`` if the enpoint assignment fails """ - self.endpoints.fabric_name = payload.get("FABRIC_NAME") - self.fabric_type = copy.copy(payload.get("FABRIC_TYPE")) try: - self.fabric_types.fabric_type = self.fabric_type + self.ep_fabric_update.fabric_name = payload.get("FABRIC_NAME") except ValueError as error: raise ValueError(error) from error + # Used to convert fabric type to template name + self.fabric_type = copy.copy(payload.get("FABRIC_TYPE")) try: - self.endpoints.template_name = self.fabric_types.template_name + self.fabric_types.fabric_type = self.fabric_type except ValueError as error: raise ValueError(error) from error try: - endpoint = self.endpoints.fabric_update + self.ep_fabric_update.template_name = self.fabric_types.template_name except ValueError as error: raise ValueError(error) from error payload.pop("FABRIC_TYPE", None) - self.path = endpoint["path"] - self.verb = endpoint["verb"] + self.path = self.ep_fabric_update.path + self.verb = self.ep_fabric_update.verb def _send_payload(self, payload): """ diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_update_bulk.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_update_bulk.py index d909df836..35a71cb75 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_update_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_update_bulk.py @@ -1846,7 +1846,7 @@ def mock_dcnm_send(*args, **kwargs): def test_fabric_update_bulk_00150(monkeypatch, fabric_update_bulk) -> None: """ Classes and Methods - - ApiEndpoints().fabric_update + - EpFabricUpdate().fabric_name setter - FabricCommon() - __init__() - FabricUpdateCommon() @@ -1857,34 +1857,39 @@ def test_fabric_update_bulk_00150(monkeypatch, fabric_update_bulk) -> None: Summary - Verify FabricUpdateCommon()._send_payload() catches and re-raises ``ValueError`` raised by - ApiEndpoints().fabric_update + EpFabricUpdate().fabric_name setter. Setup - - Mock ApiEndpoints().fabric_update property to raise ``ValueError``. - - Monkeypatch ApiEndpoints().fabric_update to the mocked method. + - Mock EpFabricUpdate().fabric_name property to raise ``ValueError``. + - Monkeypatch EpFabricUpdate().fabric_name to the mocked method. - Populate FabricUpdateCommon._payloads_to_commit with a payload which contains a valid payload. """ - class MockApiEndpoints: # pylint: disable=too-few-public-methods + class MockEpFabricUpdate: # pylint: disable=too-few-public-methods """ - Mock the ApiEndpoints.fabric_update property to raise ``ValueError``. + Mock the MockEpFabricUpdate.fabric_name property to raise ``ValueError``. """ @property - def fabric_update(self): + def fabric_name(self): """ Mocked property getter """ - raise ValueError("mocked ApiEndpoints().fabric_update getter exception.") - PATCH_API_ENDPOINTS = "ansible_collections.cisco.dcnm.plugins." - PATCH_API_ENDPOINTS += "module_utils.fabric.endpoints.ApiEndpoints.fabric_delete" + @fabric_name.setter + def fabric_name(self, value): + """ + Mocked property setter + """ + raise ValueError( + "mocked MockEpFabricUpdate().fabric_name setter exception." + ) with does_not_raise(): instance = fabric_update_bulk - monkeypatch.setattr(instance, "endpoints", MockApiEndpoints()) + monkeypatch.setattr(instance, "ep_fabric_update", MockEpFabricUpdate()) payload = { "BGP_AS": "65001", @@ -1893,6 +1898,6 @@ def fabric_update(self): "FABRIC_TYPE": "VXLAN_EVPN", } - match = r"mocked ApiEndpoints\(\)\.fabric_update getter exception\." + match = r"mocked MockEpFabricUpdate\(\)\.fabric_name setter exception\." with pytest.raises(ValueError, match=match): instance._send_payload(payload) From 384c2e2552dfd42db7f64c76739cdf8de341450f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 17 May 2024 11:14:14 -1000 Subject: [PATCH 054/230] dcnm_fabric: Remove ApiEndpoints() class This commit completely removes legacy endpoint resolution from the dcnm_fabric module. 1. Remove module_utils/fabric/endpoints.py 2. Remove unit tests for the above 3. Remove ApiEndpoints import from remaining dcnm_fabric files. - dcnm_fabric.py - test_template_get.py - test_template_get_all.py --- plugins/module_utils/fabric/endpoints.py | 300 ---------- plugins/modules/dcnm_fabric.py | 7 +- .../dcnm/dcnm_fabric/test_endpoints.py | 544 ------------------ .../dcnm/dcnm_fabric/test_template_get.py | 2 - .../dcnm/dcnm_fabric/test_template_get_all.py | 2 - 5 files changed, 2 insertions(+), 853 deletions(-) delete mode 100644 plugins/module_utils/fabric/endpoints.py delete mode 100644 tests/unit/modules/dcnm/dcnm_fabric/test_endpoints.py diff --git a/plugins/module_utils/fabric/endpoints.py b/plugins/module_utils/fabric/endpoints.py deleted file mode 100644 index f8dd7cead..000000000 --- a/plugins/module_utils/fabric/endpoints.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import copy -import inspect -import logging -import re - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ - ConversionUtils - - -class ApiEndpoints: - """ - Endpoints for fabric API calls - - Usage - - endpoints = ApiEndpoints() - endpoints.fabric_name = "MyFabric" - endpoints.template_name = "MyTemplate" - try: - endpoint = endpoints.fabric_create - except ValueError as error: - self.ansible_module.fail_json(error) - - rest_send = RestSend(self.ansible_module) - rest_send.path = endpoint.get("path") - rest_send.verb = endpoint.get("verb") - rest_send.commit() - """ - - def __init__(self): - self.class_name = self.__class__.__name__ - - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED ApiEndpoints()") - - self.conversion = ConversionUtils() - - self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" - - self.endpoint_fabrics = f"{self.endpoint_api_v1}" - self.endpoint_fabrics += "/rest/control/fabrics" - - self.endpoint_fabric_summary = f"{self.endpoint_api_v1}" - self.endpoint_fabric_summary += "/lan-fabric/rest/control/switches" - self.endpoint_fabric_summary += "/_REPLACE_WITH_FABRIC_NAME_/overview" - - self.endpoint_templates = f"{self.endpoint_api_v1}" - self.endpoint_templates += "/configtemplate/rest/config/templates" - - self._init_properties() - - def _init_properties(self): - """ """ - self.properties = {} - self.properties["fabric_name"] = None - self.properties["template_name"] = None - - @property - def fabric_config_deploy(self): - """ - - return fabric_config_deploy endpoint - - verb: POST - - path: /rest/control/fabrics/{FABRIC_NAME}/config-deploy - - Raise ``ValueError`` if fabric_name is not set. - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if not self.fabric_name: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name is required." - raise ValueError(msg) - path = self.endpoint_fabrics - path += ( - f"/{self.fabric_name}/config-deploy?forceShowRun=false" - ) - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def fabric_config_save(self): - """ - - return fabric_config_save endpoint - - verb: POST - - path: /rest/control/fabrics/{FABRIC_NAME}/config-save - - Raise ``ValueError`` if fabric_name is not set. - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if not self.fabric_name: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name is required." - raise ValueError(msg) - path = self.endpoint_fabrics - path += f"/{self.fabric_name}/config-save" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def fabric_create(self): - """ - return fabric_create endpoint - verb: POST - path: /rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME} - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if not self.fabric_name: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name is required." - raise ValueError(msg) - if not self.template_name: - msg = f"{self.class_name}.{method_name}: " - msg += "template_name is required." - raise ValueError(msg) - path = self.endpoint_fabrics - path += f"/{self.fabric_name}/{self.template_name}" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def fabric_delete(self): - """ - return fabric_delete endpoint - verb: DELETE - path: /rest/control/fabrics/{FABRIC_NAME} - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if not self.fabric_name: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name is required." - raise ValueError(msg) - path = self.endpoint_fabrics - path += f"/{self.fabric_name}" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "DELETE" - return endpoint - - @property - def fabric_summary(self): - """ - return fabric_summary endpoint - verb: GET - path: /rest/control/fabrics/summary/{FABRIC_NAME}/overview - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if not self.fabric_name: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name is required." - raise ValueError(msg) - endpoint = {} - path = copy.copy(self.endpoint_fabric_summary) - endpoint["path"] = re.sub("_REPLACE_WITH_FABRIC_NAME_", self.fabric_name, path) - endpoint["verb"] = "GET" - return endpoint - - @property - def fabric_update(self): - """ - return fabric_update endpoint - verb: PUT - path: /rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME} - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if not self.fabric_name: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name is required." - raise ValueError(msg) - if not self.template_name: - msg = f"{self.class_name}.{method_name}: " - msg += "template_name is required." - raise ValueError(msg) - path = self.endpoint_fabrics - path += f"/{self.fabric_name}/{self.template_name}" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "PUT" - return endpoint - - @property - def fabrics(self): - """ - return fabrics endpoint - verb: GET - path: /rest/control/fabrics - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - endpoint = {} - endpoint["path"] = self.endpoint_fabrics - endpoint["verb"] = "GET" - return endpoint - - @property - def fabric_info(self): - """ - return fabric_info endpoint - verb: GET - path: /rest/control/fabrics/{fabricName} - - Usage: - endpoints = ApiEndpoints() - endpoints.fabric_name = "MyFabric" - try: - endpoint = endpoints.fabric_info - except ValueError as error: - self.ansible_module.fail_json(error) - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if not self.fabric_name: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name is required." - raise ValueError(msg) - path = self.endpoint_fabrics - path += f"/{self.fabric_name}" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def fabric_name(self): - """ - setter: set the fabric_name to include in endpoint paths - getter: get the current value of fabric_name - """ - return self.properties["fabric_name"] - - @fabric_name.setter - def fabric_name(self, value): - self.conversion.validate_fabric_name(value) - self.properties["fabric_name"] = value - - @property - def template_name(self): - """ - setter: set the fabric template_name to include in endpoint paths - getter: get the current value of template_name - """ - return self.properties["template_name"] - - @template_name.setter - def template_name(self, value): - self.properties["template_name"] = value - - @property - def template(self): - """ - return the template content endpoint for template_name - verb: GET - path: /appcenter/cisco/ndfc/api/v1/configtemplate/rest/config/templates/{template_name} - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if not self.template_name: - msg = f"{self.class_name}.{method_name}: " - msg += "template_name is required." - raise ValueError(msg) - path = self.endpoint_templates - path += f"/{self.template_name}" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def templates(self): - """ - return the template contents endpoint - - This endpoint returns the all template names on the controller. - - verb: GET - path: /appcenter/cisco/ndfc/api/v1/configtemplate/rest/config/templates - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - endpoint = {} - endpoint["path"] = self.endpoint_templates - endpoint["verb"] = "GET" - return endpoint diff --git a/plugins/modules/dcnm_fabric.py b/plugins/modules/dcnm_fabric.py index 6d8c04bbe..c2a141815 100644 --- a/plugins/modules/dcnm_fabric.py +++ b/plugins/modules/dcnm_fabric.py @@ -2316,10 +2316,10 @@ from os import environ from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ - ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_features import \ ControllerFeatures +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend @@ -2331,8 +2331,6 @@ FabricCreateBulk from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.delete import \ FabricDelete -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ FabricDetailsByName from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_summary import \ @@ -2378,7 +2376,6 @@ def __init__(self, params): msg += f"check_mode: {self.check_mode}" self.log.debug(msg) - self.endpoints = ApiEndpoints() self.controller_features = ControllerFeatures(params) self.features = {} diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_endpoints.py b/tests/unit/modules/dcnm/dcnm_fabric/test_endpoints.py deleted file mode 100644 index 1093859fe..000000000 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_endpoints.py +++ /dev/null @@ -1,544 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# See the following regarding *_fixture imports -# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html -# Due to the above, we also need to disable unused-import -# Also, fixtures need to use *args to match the signature of the function they are mocking -# pylint: disable=unused-import -# pylint: disable=redefined-outer-name -# pylint: disable=protected-access -# pylint: disable=unused-argument -# pylint: disable=invalid-name -# pylint: disable=pointless-statement - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - -import inspect -import re - -import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints -from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import \ - does_not_raise - - -def test_endpoints_00010() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - ApiEndpoints - - __init__() - - Summary - - Verify the class attributes are initialized to expected values. - - Test - - Class attributes are initialized to expected values - - ``ValueError`` is not called - """ - with does_not_raise(): - instance = ApiEndpoints() - assert instance.class_name == "ApiEndpoints" - assert instance.endpoint_api_v1 == "/appcenter/cisco/ndfc/api/v1" - assert instance.endpoint_fabrics == ( - f"{instance.endpoint_api_v1}" + "/rest/control/fabrics" - ) - assert instance.endpoint_fabric_summary == ( - f"{instance.endpoint_api_v1}" - + "/lan-fabric/rest/control/switches" - + "/_REPLACE_WITH_FABRIC_NAME_/overview" - ) - assert instance.endpoint_templates == ( - f"{instance.endpoint_api_v1}" + "/configtemplate/rest/config/templates" - ) - assert instance.properties["fabric_name"] is None - assert instance.properties["template_name"] is None - - -MATCH_00020a = r"ConversionUtils\.validate_fabric_name: " -MATCH_00020a += r"Invalid fabric name\. " -MATCH_00020a += r"Expected string\. Got.*\." - -MATCH_00020b = r"ConversionUtils\.validate_fabric_name: " -MATCH_00020b += r"Invalid fabric name:.*\. " -MATCH_00020b += "Fabric name must start with a letter A-Z or a-z and " -MATCH_00020b += r"contain only the characters in: \[A-Z,a-z,0-9,-,_\]\." - - -@pytest.mark.parametrize( - "fabric_name, expected, does_raise", - [ - ("MyFabric", does_not_raise(), False), - ("My_Fabric", does_not_raise(), False), - ("My-Fabric", does_not_raise(), False), - ("M", does_not_raise(), False), - (1, pytest.raises(TypeError, match=MATCH_00020a), True), - ({}, pytest.raises(TypeError, match=MATCH_00020a), True), - ([1, 2, 3], pytest.raises(TypeError, match=MATCH_00020a), True), - ("1", pytest.raises(ValueError, match=MATCH_00020b), True), - ("-MyFabric", pytest.raises(ValueError, match=MATCH_00020b), True), - ("_MyFabric", pytest.raises(ValueError, match=MATCH_00020b), True), - ("1MyFabric", pytest.raises(ValueError, match=MATCH_00020b), True), - ("My Fabric", pytest.raises(ValueError, match=MATCH_00020b), True), - ("My*Fabric", pytest.raises(ValueError, match=MATCH_00020b), True), - ], -) -def test_endpoints_00020(fabric_name, expected, does_raise) -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_name.setter - - ConversionUtils - - validate_fabric_name() - - Summary - - Verify ``TypeError`` is raised for non-string fabric_name. - - Verify ``ValueError`` is raised for invalid string fabric_name. - - Verify ``ValueError`` is not raised for valid fabric_name. - """ - with does_not_raise(): - instance = ApiEndpoints() - with expected: - instance.fabric_name = fabric_name - if does_raise is False: - assert instance.fabric_name == fabric_name - - -def test_endpoints_00030() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_config_deploy getter - - Summary - - Verify fabric_config_deploy getter raises ``ValueError`` - if ``fabric_name`` is not set. - """ - with does_not_raise(): - instance = ApiEndpoints() - match = r"ApiEndpoints\.fabric_config_deploy: " - match += r"fabric_name is required\." - with pytest.raises(ValueError, match=match): - instance.fabric_config_deploy - - -def test_endpoints_00031() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_config_deploy getter - - Summary - - Verify fabric_config_deploy getter returns the expected - endpoint when ``fabric_name`` is set. - """ - fabric_name = "MyFabric" - with does_not_raise(): - instance = ApiEndpoints() - instance.fabric_name = fabric_name - endpoint = instance.fabric_config_deploy - assert endpoint.get("verb", None) == "POST" - assert endpoint.get("path", None) == ( - f"{instance.endpoint_fabrics}/{fabric_name}" - + "/config-deploy?forceShowRun=false" - ) - - -def test_endpoints_00040() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_config_save getter - - Summary - - Verify fabric_config_save getter raises ``ValueError`` - if ``fabric_name`` is not set. - """ - with does_not_raise(): - instance = ApiEndpoints() - match = r"ApiEndpoints\.fabric_config_save: " - match += r"fabric_name is required\." - with pytest.raises(ValueError, match=match): - instance.fabric_config_save - - -def test_endpoints_00041() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_config_save getter - - Summary - - Verify fabric_config_save getter returns the expected - endpoint when ``fabric_name`` is set. - """ - fabric_name = "MyFabric" - with does_not_raise(): - instance = ApiEndpoints() - instance.fabric_name = fabric_name - endpoint = instance.fabric_config_save - assert endpoint.get("verb", None) == "POST" - assert endpoint.get("path", None) == ( - f"{instance.endpoint_fabrics}/{fabric_name}" + "/config-save" - ) - - -def test_endpoints_00050() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_create getter - - Summary - - Verify fabric_create getter raises ``ValueError`` - if ``fabric_name`` is not set. - """ - template_name = "MyTemplate" - with does_not_raise(): - instance = ApiEndpoints() - instance.template_name = template_name - match = r"ApiEndpoints\.fabric_create: " - match += r"fabric_name is required\." - with pytest.raises(ValueError, match=match): - instance.fabric_create - - -def test_endpoints_00051() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_create getter - - Summary - - Verify fabric_create getter raises ``ValueError`` - if ``template_name`` is not set. - """ - fabric_name = "MyFabric" - with does_not_raise(): - instance = ApiEndpoints() - instance.fabric_name = fabric_name - match = r"ApiEndpoints\.fabric_create: " - match += r"template_name is required\." - with pytest.raises(ValueError, match=match): - instance.fabric_create - - -def test_endpoints_00052() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_create getter - - Summary - - Verify fabric_create getter returns the expected - endpoint when ``fabric_name`` and ``template_name`` - are set. - """ - fabric_name = "MyFabric" - template_name = "MyTemplate" - with does_not_raise(): - instance = ApiEndpoints() - instance.fabric_name = fabric_name - instance.template_name = template_name - endpoint = instance.fabric_create - assert endpoint.get("verb", None) == "POST" - assert endpoint.get("path", None) == ( - f"{instance.endpoint_fabrics}/" + f"{fabric_name}/" + f"{template_name}" - ) - - -def test_endpoints_00060() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_delete getter - - Summary - - Verify fabric_delete getter raises ``ValueError`` - if ``fabric_name`` is not set. - """ - fabric_name = "MyFabric" - with does_not_raise(): - instance = ApiEndpoints() - instance.template_name = fabric_name - match = r"ApiEndpoints\.fabric_delete: " - match += r"fabric_name is required\." - with pytest.raises(ValueError, match=match): - instance.fabric_delete - - -def test_endpoints_00061() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_delete getter - - Summary - - Verify fabric_delete getter returns the expected - endpoint when ``fabric_name`` is set. - """ - fabric_name = "MyFabric" - with does_not_raise(): - instance = ApiEndpoints() - instance.fabric_name = fabric_name - endpoint = instance.fabric_delete - assert endpoint.get("verb", None) == "DELETE" - assert endpoint.get("path", None) == ( - f"{instance.endpoint_fabrics}/" + f"{fabric_name}" - ) - - -def test_endpoints_00070() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_summary getter - - Summary - - Verify fabric_summary getter raises ``ValueError`` - if ``fabric_name`` is not set. - """ - with does_not_raise(): - instance = ApiEndpoints() - match = r"ApiEndpoints\.fabric_summary: " - match += r"fabric_name is required\." - with pytest.raises(ValueError, match=match): - instance.fabric_summary - - -def test_endpoints_00071() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_summary getter - - Summary - - Verify fabric_summary getter returns the expected - endpoint when ``fabric_name`` is set. - """ - fabric_name = "MyFabric" - with does_not_raise(): - instance = ApiEndpoints() - instance.fabric_name = fabric_name - endpoint = instance.fabric_summary - assert endpoint.get("verb", None) == "GET" - assert endpoint.get("path", None) == ( - f"{instance.endpoint_api_v1}/" - + "lan-fabric/rest/control/switches/" - + f"{fabric_name}/overview" - ) - - -def test_endpoints_00080() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_update getter - - Summary - - Verify fabric_update getter raises ``ValueError`` - if ``fabric_name`` is not set. - """ - template_name = "MyTemplate" - with does_not_raise(): - instance = ApiEndpoints() - instance.template_name = template_name - match = r"ApiEndpoints\.fabric_update: " - match += r"fabric_name is required\." - with pytest.raises(ValueError, match=match): - instance.fabric_update - - -def test_endpoints_00081() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_update getter - - Summary - - Verify fabric_update getter raises ``ValueError`` - if ``template_name`` is not set. - """ - fabric_name = "MyFabric" - with does_not_raise(): - instance = ApiEndpoints() - instance.fabric_name = fabric_name - match = r"ApiEndpoints\.fabric_update: " - match += r"template_name is required\." - with pytest.raises(ValueError, match=match): - instance.fabric_update - - -def test_endpoints_00082() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_update getter - - Summary - - Verify fabric_update getter returns the expected - endpoint when ``fabric_name`` and ``template_name`` - are set. - """ - fabric_name = "MyFabric" - template_name = "MyTemplate" - with does_not_raise(): - instance = ApiEndpoints() - instance.fabric_name = fabric_name - instance.template_name = template_name - endpoint = instance.fabric_update - assert endpoint.get("verb", None) == "PUT" - assert endpoint.get("path", None) == ( - f"{instance.endpoint_fabrics}/" + f"{fabric_name}/" + f"{template_name}" - ) - - -def test_endpoints_00090() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_info getter - - Summary - - Verify fabric_info getter raises ``ValueError`` - if ``fabric_name`` is not set. - """ - with does_not_raise(): - instance = ApiEndpoints() - match = r"ApiEndpoints\.fabric_info: " - match += r"fabric_name is required\." - with pytest.raises(ValueError, match=match): - instance.fabric_info - - -def test_endpoints_00091() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - fabric_info getter - - Summary - - Verify fabric_info getter returns the expected - endpoint when ``fabric_name`` is set. - """ - fabric_name = "MyFabric" - with does_not_raise(): - instance = ApiEndpoints() - instance.fabric_name = fabric_name - endpoint = instance.fabric_info - assert endpoint.get("verb", None) == "GET" - assert endpoint.get("path", None) == ( - f"{instance.endpoint_fabrics}/" + f"{fabric_name}" - ) - - -def test_endpoints_00100() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - template_name getter/setter - - Summary - - Verify template_name getter returns the value set - with template_name setter. - """ - template_name = "MyTemplate" - with does_not_raise(): - instance = ApiEndpoints() - instance.template_name = template_name - assert instance.template_name == template_name - - -def test_endpoints_00110() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - template getter - - Summary - - Verify template getter raises ``ValueError`` - if `template_name`` is not set. - """ - with does_not_raise(): - instance = ApiEndpoints() - match = r"ApiEndpoints\.template: " - match += r"template_name is required\." - with pytest.raises(ValueError, match=match): - instance.template - - -def test_endpoints_00111() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - template getter - - Summary - - Verify template getter returns the expected - endpoint when ``template_name`` is set. - """ - template_name = "MyTemplate" - with does_not_raise(): - instance = ApiEndpoints() - instance.template_name = template_name - endpoint = instance.template - assert endpoint.get("verb", None) == "GET" - assert endpoint.get("path", None) == ( - f"{instance.endpoint_templates}/" + f"{template_name}" - ) - - -def test_endpoints_00120() -> None: - """ - Classes and Methods - - ApiEndpoints - - __init__() - - templates getter - - Summary - - Verify templates getter returns the expected endpoint. - """ - with does_not_raise(): - instance = ApiEndpoints() - endpoint = instance.templates - assert endpoint.get("verb", None) == "GET" - assert endpoint.get("path", None) == (f"{instance.endpoint_templates}") diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py index 06d2c24d4..176cdaae2 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py @@ -40,8 +40,6 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( MockAnsibleModule, ResponseGenerator, does_not_raise, responses_template_get, template_get_fixture) diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py index 0f8198eb2..bc1f28cdc 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py @@ -40,8 +40,6 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( MockAnsibleModule, ResponseGenerator, does_not_raise, responses_template_get_all, template_get_all_fixture) From 2c67232a673033cb588e7c6f4e5c6fd9647e6a36 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 17 May 2024 12:55:25 -1000 Subject: [PATCH 055/230] Remove RestSend and Results import requirement Remove requirement that RestSend and Results be imported merely to verify rest_send and results properties. 1. FabricConfigDeploy(): Modify rest_send, and results properties not to need RestSend() and Results() classes when verifying their input values. Remove RestSend() and Results() imports. 2. FabricConfigSave(): Modify rest_send, and results properties not to need RestSend() and Results() classes when verifying their input values. Remove RestSend() and Results() imports. 3. ControllerFeatures(): modify rest_send setter for consistency with other classes. 4. Modify associated UT to reflect the above changes. --- .../common/controller_features.py | 6 ++-- plugins/module_utils/fabric/config_deploy.py | 32 ++++++++++------- plugins/module_utils/fabric/config_save.py | 32 ++++++++++------- .../common/test_controller_features.py | 34 +++++++------------ .../dcnm_fabric/test_fabric_config_deploy.py | 4 +-- .../dcnm_fabric/test_fabric_config_save.py | 4 +-- 6 files changed, 59 insertions(+), 53 deletions(-) diff --git a/plugins/module_utils/common/controller_features.py b/plugins/module_utils/common/controller_features.py index ab3338fbf..ba87a9c59 100644 --- a/plugins/module_utils/common/controller_features.py +++ b/plugins/module_utils/common/controller_features.py @@ -290,15 +290,15 @@ def rest_send(self): @rest_send.setter def rest_send(self, value): method_name = inspect.stack()[0][3] - test = None + _class_name = None msg = f"{self.class_name}.{method_name}: " msg += "value must be an instance of RestSend. " try: - test = value.class_name + _class_name = value.class_name except AttributeError as error: msg += f"Error detail: {error}." raise TypeError(msg) from error - if test != "RestSend": + if _class_name != "RestSend": self.log.debug(msg) raise TypeError(msg) self.properties["rest_send"] = value diff --git a/plugins/module_utils/fabric/config_deploy.py b/plugins/module_utils/fabric/config_deploy.py index bf15d34d1..7f0bd6e30 100644 --- a/plugins/module_utils/fabric/config_deploy.py +++ b/plugins/module_utils/fabric/config_deploy.py @@ -28,12 +28,6 @@ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError -# Used only to verify RestSend instance in rest_send property setter -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend -# Used only to verify RestSend instance in rest_send property setter -from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ - Results class FabricConfigDeploy: @@ -391,9 +385,15 @@ def rest_send(self): @rest_send.setter def rest_send(self, value): method_name = inspect.stack()[0][3] - if not isinstance(value, RestSend): - msg = f"{self.class_name}.{method_name}: " - msg += "rest_send must be an instance of RestSend." + _class_name = None + msg = f"{self.class_name}.{method_name}: " + msg += "value must be an instance of RestSend. " + try: + _class_name = value.class_name + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _class_name != "RestSend": self.log.debug(msg) raise TypeError(msg) self._properties["rest_send"] = value @@ -411,9 +411,17 @@ def results(self): @results.setter def results(self, value): method_name = inspect.stack()[0][3] - if not isinstance(value, Results): - msg = f"{self.class_name}.{method_name}: " - msg += "results must be an instance of Results." + msg = f"{self.class_name}.{method_name}: " + msg += "value must be an instance of Results. " + msg += f"Got value {value} of type {type(value).__name__}." + _class_name = None + try: + _class_name = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + self.log.debug(msg) + raise TypeError(msg) from error + if _class_name != "Results": self.log.debug(msg) raise TypeError(msg) self._properties["results"] = value diff --git a/plugins/module_utils/fabric/config_save.py b/plugins/module_utils/fabric/config_save.py index 6cb8f99e3..6e4b232ea 100644 --- a/plugins/module_utils/fabric/config_save.py +++ b/plugins/module_utils/fabric/config_save.py @@ -26,12 +26,6 @@ EpFabricConfigSave from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils -# Used only to verify RestSend instance in rest_send property setter -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend -# Used only to verify RestSend instance in rest_send property setter -from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ - Results class FabricConfigSave: @@ -247,9 +241,15 @@ def rest_send(self): @rest_send.setter def rest_send(self, value): method_name = inspect.stack()[0][3] - if not isinstance(value, RestSend): - msg = f"{self.class_name}.{method_name}: " - msg += "rest_send must be an instance of RestSend." + _class_name = None + msg = f"{self.class_name}.{method_name}: " + msg += "value must be an instance of RestSend. " + try: + _class_name = value.class_name + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _class_name != "RestSend": self.log.debug(msg) raise TypeError(msg) self._properties["rest_send"] = value @@ -267,9 +267,17 @@ def results(self): @results.setter def results(self, value): method_name = inspect.stack()[0][3] - if not isinstance(value, Results): - msg = f"{self.class_name}.{method_name}: " - msg += "results must be an instance of Results." + msg = f"{self.class_name}.{method_name}: " + msg += "value must be an instance of Results. " + msg += f"Got value {value} of type {type(value).__name__}." + _class_name = None + try: + _class_name = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + self.log.debug(msg) + raise TypeError(msg) from error + if _class_name != "Results": self.log.debug(msg) raise TypeError(msg) self._properties["results"] = value diff --git a/tests/unit/module_utils/common/test_controller_features.py b/tests/unit/module_utils/common/test_controller_features.py index 2a7ad7408..0a932aba4 100644 --- a/tests/unit/module_utils/common/test_controller_features.py +++ b/tests/unit/module_utils/common/test_controller_features.py @@ -32,29 +32,19 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm.fm import ( - EpFeatures, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import ( - ConversionUtils, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_features import ( - ControllerFeatures, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import ( - ControllerResponseError, -) -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import ( - RestSend, -) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.fm.fm import \ + EpFeatures +from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_features import \ + ControllerFeatures +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ + RestSend from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( - MockAnsibleModule, - ResponseGenerator, - does_not_raise, - controller_features_fixture, - responses_controller_features, - params, -) + MockAnsibleModule, ResponseGenerator, controller_features_fixture, + does_not_raise, params, responses_controller_features) def test_controller_features_00010(controller_features) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py index 5726aaefc..a097a4c92 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py @@ -174,7 +174,7 @@ def test_fabric_config_deploy_00020( MATCH_00030 = r"FabricConfigDeploy\.rest_send: " -MATCH_00030 += r"rest_send must be an instance of RestSend\." +MATCH_00030 += r"value must be an instance of RestSend\." @pytest.mark.parametrize( @@ -214,7 +214,7 @@ def test_fabric_config_deploy_00030( MATCH_00040 = r"FabricConfigDeploy\.results: " -MATCH_00040 += r"results must be an instance of Results\." +MATCH_00040 += r"value must be an instance of Results\." @pytest.mark.parametrize( diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py index 7170a9d30..7766e25bf 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py @@ -173,7 +173,7 @@ def test_fabric_config_save_00020( MATCH_00030 = r"FabricConfigSave\.rest_send: " -MATCH_00030 += r"rest_send must be an instance of RestSend\." +MATCH_00030 += r"value must be an instance of RestSend\." @pytest.mark.parametrize( @@ -213,7 +213,7 @@ def test_fabric_config_save_00030( MATCH_00040 = r"FabricConfigSave\.results: " -MATCH_00040 += r"results must be an instance of Results\." +MATCH_00040 += r"value must be an instance of Results\." @pytest.mark.parametrize( From 632242611ad24c2f767aea1df9f3ff2b27fd26ee Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 18 May 2024 09:14:26 -1000 Subject: [PATCH 056/230] dcnm_fabric: IPFM, update FabricTypes() unit tests for IPFM. --- tests/unit/modules/dcnm/dcnm_fabric/test_fabric_types.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_types.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_types.py index b4d2bdc6c..30f191e47 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_types.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_types.py @@ -47,7 +47,7 @@ def test_fabric_types_00010(fabric_types) -> None: assert instance.class_name == "FabricTypes" assert instance._properties["fabric_type"] is None assert instance._properties["template_name"] is None - for fabric_type in ["LAN_CLASSIC", "VXLAN_EVPN", "VXLAN_EVPN_MSD"]: + for fabric_type in ["IPFM", "LAN_CLASSIC", "VXLAN_EVPN", "VXLAN_EVPN_MSD"]: assert fabric_type in instance.valid_fabric_types for mandatory_parameter in ["FABRIC_NAME", "FABRIC_TYPE"]: assert mandatory_parameter in instance._mandatory_parameters_all_fabrics @@ -64,6 +64,7 @@ def test_fabric_types_00010(fabric_types) -> None: @pytest.mark.parametrize( "fabric_type, template_name, does_raise, expected", [ + ("IPFM", "Easy_Fabric_IPFM", False, does_not_raise()), ("LAN_CLASSIC", "LAN_Classic", False, does_not_raise()), ("VXLAN_EVPN", "Easy_Fabric", False, does_not_raise()), ("VXLAN_EVPN_MSD", "MSD_Fabric", False, does_not_raise()), @@ -119,8 +120,9 @@ def test_fabric_types_00030(fabric_types) -> None: instance.template_name # pylint: disable=pointless-statement -VXLAN_EVPN_PARAMETERS = ["BGP_AS", "FABRIC_NAME", "FABRIC_TYPE"] +IPFM_PARAMETERS = ["FABRIC_NAME", "FABRIC_TYPE"] LAN_CLASSIC_PARAMETERS = ["FABRIC_NAME", "FABRIC_TYPE"] +VXLAN_EVPN_PARAMETERS = ["BGP_AS", "FABRIC_NAME", "FABRIC_TYPE"] VXLAN_EVPN_MSD_PARAMETERS = ["FABRIC_NAME", "FABRIC_TYPE"] MATCH_00040 = r"FabricTypes\.fabric_type.setter:\s+" MATCH_00040 += r"Invalid fabric type: INVALID_FABRIC_TYPE.\s+" @@ -129,6 +131,7 @@ def test_fabric_types_00030(fabric_types) -> None: @pytest.mark.parametrize( "fabric_type, parameters, does_raise, expected", [ + ("IPFM", IPFM_PARAMETERS, False, does_not_raise()), ("LAN_CLASSIC", LAN_CLASSIC_PARAMETERS, False, does_not_raise()), ("VXLAN_EVPN", VXLAN_EVPN_PARAMETERS, False, does_not_raise()), ("VXLAN_EVPN_MSD", VXLAN_EVPN_MSD_PARAMETERS, False, does_not_raise()), From 419b3e2cf7cbbd32e7d40478c5968eb8698f7e1a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 19 May 2024 10:00:58 -1000 Subject: [PATCH 057/230] FabricReplacedCommon().update_replaced_payload(): Simplify logic 1. FabricReplacedCommon().update_replaced_payload(): Simplify logic. I've run this through a test script with data representing all possible combinations, and the results for the original and simplified methods are the same. 2. test_fabric_replaced_bulk.py: Add one more combination to input test parameters. These should now be complete. 3. FabricTypes(): alphabetize _fabric_type_to_feature_map dict by key for easier readability. --- plugins/module_utils/fabric/fabric_types.py | 4 ++-- plugins/module_utils/fabric/replaced.py | 20 +++++++------------ .../dcnm_fabric/test_fabric_replaced_bulk.py | 8 +++++--- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/plugins/module_utils/fabric/fabric_types.py b/plugins/module_utils/fabric/fabric_types.py index 3a4abf43b..4592a1d3a 100644 --- a/plugins/module_utils/fabric/fabric_types.py +++ b/plugins/module_utils/fabric/fabric_types.py @@ -79,10 +79,10 @@ def _init_fabric_types(self) -> None: # Map fabric type to the feature name that must be running # on the controller to enable the fabric type. self._fabric_type_to_feature_name_map = {} + self._fabric_type_to_feature_name_map["IPFM"] = "pmn" + self._fabric_type_to_feature_name_map["LAN_CLASSIC"] = "lan" self._fabric_type_to_feature_name_map["VXLAN_EVPN"] = "vxlan" self._fabric_type_to_feature_name_map["VXLAN_EVPN_MSD"] = "vxlan" - self._fabric_type_to_feature_name_map["LAN_CLASSIC"] = "lan" - self._fabric_type_to_feature_name_map["IPFM"] = "pmn" self._valid_fabric_types = sorted(self._fabric_type_to_template_name_map.keys()) diff --git a/plugins/module_utils/fabric/replaced.py b/plugins/module_utils/fabric/replaced.py index 88b6ad8db..e0ff45101 100644 --- a/plugins/module_utils/fabric/replaced.py +++ b/plugins/module_utils/fabric/replaced.py @@ -149,27 +149,21 @@ def update_replaced_payload(self, parameter, playbook, controller, default): payload_to_send_to_controller.update(result) ``` """ - raise_value_error = False + method_name = inspect.stack()[0][3] if playbook is None: if default is None: return None - if controller != default and controller is not None and controller != "": - return {parameter: default} - if controller != default and (controller is None or controller == ""): - return None if controller == default: return None - raise_value_error = True - msg = "UNHANDLED case when playbook value is None. " + if controller is None or controller == "": + return None + return {parameter: default} if playbook is not None: if playbook == controller: return None - if playbook != controller: - return {parameter: playbook} - raise_value_error = True - msg = "UNHANDLED case when playbook value is not None. " - if raise_value_error is False: - msg = "UNHANDLED case " + return {parameter: playbook} + msg = f"{self.class_name}.{method_name}: " + msg += "UNHANDLED case " msg += f"parameter {parameter}, " msg += f"playbook: {playbook}, " msg += f"controller: {controller}, " diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py index 2eececc72..e25b79013 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py @@ -393,9 +393,11 @@ def test_fabric_replaced_bulk_00031( ("PARAM_8", None, "b", None, None), ("PARAM_9", None, None, None, None), ("PARAM_10", "a", None, None, {"PARAM_10": "a"}), - ("PARAM_11", "a", "b", None, {"PARAM_11": "a"}), - ("PARAM_12", "a", None, "c", {"PARAM_12": "a"}), - ("PARAM_13", None, None, "c", None), + ("PARAM_11", "a", "a", None, None), + ("PARAM_12", "a", "b", None, {"PARAM_12": "a"}), + ("PARAM_13", "a", None, "a", {"PARAM_13": "a"}), + ("PARAM_14", "a", None, "c", {"PARAM_14": "a"}), + ("PARAM_15", None, None, "c", None), ], ) def test_fabric_replaced_bulk_00040( From 4869ff55038b351db21721e05d9f465f86abb8c3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 19 May 2024 10:31:54 -1000 Subject: [PATCH 058/230] FabricReplacedCommon().update_replaced_payload(): Further logic simplification --- plugins/module_utils/fabric/replaced.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/plugins/module_utils/fabric/replaced.py b/plugins/module_utils/fabric/replaced.py index e0ff45101..c093d425f 100644 --- a/plugins/module_utils/fabric/replaced.py +++ b/plugins/module_utils/fabric/replaced.py @@ -149,7 +149,6 @@ def update_replaced_payload(self, parameter, playbook, controller, default): payload_to_send_to_controller.update(result) ``` """ - method_name = inspect.stack()[0][3] if playbook is None: if default is None: return None @@ -158,17 +157,9 @@ def update_replaced_payload(self, parameter, playbook, controller, default): if controller is None or controller == "": return None return {parameter: default} - if playbook is not None: - if playbook == controller: - return None - return {parameter: playbook} - msg = f"{self.class_name}.{method_name}: " - msg += "UNHANDLED case " - msg += f"parameter {parameter}, " - msg += f"playbook: {playbook}, " - msg += f"controller: {controller}, " - msg += f"default: {default}" - raise ValueError(msg) + if playbook == controller: + return None + return {parameter: playbook} def _verify_value_types_for_comparison( self, fabric_name, parameter, user_value, controller_value, default_value From ed595ff78409afd53dad7c77d0d1010a4c53b0ca Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 19 May 2024 10:42:43 -1000 Subject: [PATCH 059/230] FabricReplacedCommon().update_replaced_payload(): docstring update Modify the docstring to remove mention of raising ValueError since this method no longer raises ValueError. --- plugins/module_utils/fabric/replaced.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/module_utils/fabric/replaced.py b/plugins/module_utils/fabric/replaced.py index c093d425f..6bdc2d0f3 100644 --- a/plugins/module_utils/fabric/replaced.py +++ b/plugins/module_utils/fabric/replaced.py @@ -135,7 +135,6 @@ def update_replaced_payload(self, parameter, playbook, controller, default): - None if the parameter does not need to be updated. - A dict with the parameter and playbook value if the parameter needs to be updated. - - raise ``ValueError`` for any unhandled case(s). Usage: ```python From 9045026616944275e4663314ae2e515904a969e8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 20 May 2024 09:58:23 -1000 Subject: [PATCH 060/230] EpFabricConfigDeploy(): add switch_id property This will be useful for the dcnm_maintenance_mode module. - Add switch_id property - Update docstrings --- .../rest/control/fabrics/fabrics.py | 100 +++++++++++++----- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py index 889afd2a1..d87433cb3 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py @@ -134,7 +134,8 @@ class EpFabricConfigDeploy(Fabrics): ## api.v1.lan-fabric.rest.control.fabrics.EpFabricConfigDeploy() ### Description - Return endpoint to initiate config-deploy on fabric_name. + Return endpoint to initiate config-deploy on fabric_name + or fabric_name + switch_id. ### Raises - ``ValueError``: If fabric_name is not set. @@ -146,22 +147,38 @@ class EpFabricConfigDeploy(Fabrics): - ``/fabrics/{fabric_name}/config-deploy`` - ``/fabrics/{fabric_name}/config-deploy?forceShowRun={force_show_run}`` - ``/fabrics/{fabric_name}/config-deploy?inclAllMSDSwitches={include_all_msd_switches}`` + - ``/fabrics/{fabric_name}/config-deploy/{switch_id}`` + - ``/fabrics/{fabric_name}/config-deploy/{switch_id}/?forceShowRun={force_show_run}`` ### Verb - POST ### Parameters - - force_show_run: boolean - - set the ``forceShowRun`` value - - default: False - - include_all_msd_switches: boolean - - set the ``inclAllMSDSwitches`` value - - default: False - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint + - fabric_name: + - set the ``fabric_name`` to be used in the path + - string + - required + - force_show_run: boolean + - set the ``forceShowRun`` value + - boolean + - default: False + - optional + - include_all_msd_switches: boolean + - set the ``inclAllMSDSwitches`` value + - boolean + - default: False + - optional + - path: + - retrieve the path for the endpoint + - string + - switch_id: string + - set the ``switch_id`` to be used in the path + - string + - optional + - if set, ``include_all_msd_switches`` is not added to the path + - verb: + - retrieve the verb for the endpoint + - string (e.g. GET, POST, PUT, DELETE) ### Usage ```python @@ -186,17 +203,20 @@ def __init__(self): def _build_properties(self): super()._build_properties() - self.properties["verb"] = "POST" self.properties["force_show_run"] = False self.properties["include_all_msd_switches"] = False + self.properties["switch_id"] = None + self.properties["verb"] = "POST" @property def force_show_run(self): """ - - getter: Return the force_show_run value. - - setter: Set the force_show_run value. - - setter: Raise ``ValueError`` if force_show_run is not a boolean. - - Default: False + - getter: Return the force_show_run value. + - setter: Set the force_show_run value. + - setter: Raise ``ValueError`` if force_show_run is + not a boolean. + - Default: False + - Optional """ return self.properties["force_show_run"] @@ -213,10 +233,15 @@ def force_show_run(self, value): @property def include_all_msd_switches(self): """ - - getter: Return the include_all_msd_switches. - - setter: Set the include_all_msd_switches. - - setter: Raise ``ValueError`` if include_all_msd_switches is a boolean. - - Default: False + - getter: Return the include_all_msd_switches. + - setter: Set the include_all_msd_switches. + - setter: Raise ``ValueError`` if include_all_msd_switches + is not a boolean. + - Default: False + - Optional + - Notes: + - ``include_all_msd_switches`` is removed from the path if + ``switch_id`` is set. """ return self.properties["include_all_msd_switches"] @@ -237,11 +262,38 @@ def path(self): - Raise ``ValueError`` if fabric_name is not set. """ _path = self.path_fabric_name - _path += "/config-deploy?" - _path += f"forceShowRun={self.force_show_run}" - _path += f"&inclAllMSDSwitches={self.include_all_msd_switches}" + _path += "/config-deploy" + if self.switch_id: + _path += f"/{self.switch_id}" + _path += f"?forceShowRun={self.force_show_run}" + if not self.switch_id: + _path += f"&inclAllMSDSwitches={self.include_all_msd_switches}" return _path + @property + def switch_id(self): + """ + - getter: Return the switch_id value. + - setter: Set the switch_id value. + - setter: Raise ``ValueError`` if switch_id is not a string. + - Default: None + - Optional + - Notes: + - ``include_all_msd_switches`` is removed from the path if + ``switch_id`` is set. + """ + return self.properties["switch_id"] + + @switch_id.setter + def switch_id(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected string for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["switch_id"] = value + class EpFabricConfigSave(Fabrics): """ From 50bb8af6e3cda20c66facf90d92abe3fd25c3d54 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 20 May 2024 15:47:04 -1000 Subject: [PATCH 061/230] Remove files associated with unpublished NDFC REST API path These files were related to an unpublished NDFC REST API path that we won't be using. Unpublished path: /api/v1/rest/* Published path: /api/v1/lan_fabric/rest/* --- .../common/api/v1/rest/__init__.py | 0 .../common/api/v1/rest/control/__init__.py | 0 .../common/api/v1/rest/control/control.py | 49 -- .../api/v1/rest/control/fabrics/__init__.py | 0 .../api/v1/rest/control/fabrics/fabrics.py | 658 ------------------ .../api/v1/rest/control/switches/__init__.py | 0 .../api/v1/rest/control/switches/switches.py | 140 ---- .../module_utils/common/api/v1/rest/rest.py | 49 -- 8 files changed, 896 deletions(-) delete mode 100644 plugins/module_utils/common/api/v1/rest/__init__.py delete mode 100644 plugins/module_utils/common/api/v1/rest/control/__init__.py delete mode 100644 plugins/module_utils/common/api/v1/rest/control/control.py delete mode 100644 plugins/module_utils/common/api/v1/rest/control/fabrics/__init__.py delete mode 100644 plugins/module_utils/common/api/v1/rest/control/fabrics/fabrics.py delete mode 100644 plugins/module_utils/common/api/v1/rest/control/switches/__init__.py delete mode 100644 plugins/module_utils/common/api/v1/rest/control/switches/switches.py delete mode 100644 plugins/module_utils/common/api/v1/rest/rest.py diff --git a/plugins/module_utils/common/api/v1/rest/__init__.py b/plugins/module_utils/common/api/v1/rest/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/module_utils/common/api/v1/rest/control/__init__.py b/plugins/module_utils/common/api/v1/rest/control/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/module_utils/common/api/v1/rest/control/control.py b/plugins/module_utils/common/api/v1/rest/control/control.py deleted file mode 100644 index 84091d2c1..000000000 --- a/plugins/module_utils/common/api/v1/rest/control/control.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.rest import \ - Rest - - -class Control(Rest): - """ - ## V1 API Control() - api.v1.rest.control.Control() - - ### Description - Common methods and properties for Control() subclasses. - - ### Path - - ``/api/v1/rest/control`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.control = f"{self.rest}/control" - msg = f"ENTERED api.v1.LanFabric.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Set the fabric_name property. - """ diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics/__init__.py b/plugins/module_utils/common/api/v1/rest/control/fabrics/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/module_utils/common/api/v1/rest/control/fabrics/fabrics.py b/plugins/module_utils/common/api/v1/rest/control/fabrics/fabrics.py deleted file mode 100644 index 332c5758b..000000000 --- a/plugins/module_utils/common/api/v1/rest/control/fabrics/fabrics.py +++ /dev/null @@ -1,658 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import inspect -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.control import \ - Control -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ - FabricTypes - - -class Fabrics(Control): - """ - ## V1 API Fabrics - api.v1.rest.control.fabrics.Fabrics() - - ### Description - Common methods and properties for Fabrics() subclasses. - - ### Path - - ``/rest/control/fabrics`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.fabric_types = FabricTypes() - self.fabrics = f"{self.control}/fabrics" - msg = f"ENTERED api.v1.rest.control.fabrics.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Set the fabric_name property. - """ - self.properties["fabric_name"] = None - self.properties["template_name"] = None - - @property - def fabric_name(self): - """ - - getter: Return the fabric_name. - - setter: Set the fabric_name. - - setter: Raise ``ValueError`` if fabric_name is not valid. - """ - return self.properties["fabric_name"] - - @fabric_name.setter - def fabric_name(self, value): - method_name = inspect.stack()[0][3] - try: - self.conversion.validate_fabric_name(value) - except (TypeError, ValueError) as error: - msg = f"{self.class_name}.{method_name}: " - msg += f"{error}" - raise ValueError(msg) from error - self.properties["fabric_name"] = value - - @property - def path_fabric_name(self): - """ - - Endpoint path property, including fabric_name. - - Raise ``ValueError`` if fabric_name is not set and - ``self.required_properties`` contains "fabric_name". - """ - method_name = inspect.stack()[0][3] - if self.fabric_name is None and "fabric_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be set prior to accessing path." - raise ValueError(msg) - return f"{self.fabrics}/{self.fabric_name}" - - @property - def path_fabric_name_template_name(self): - """ - - Endpoint path property, including fabric_name and template_name. - - Raise ``ValueError`` if fabric_name is not set and - ``self.required_properties`` contains "fabric_name". - - Raise ``ValueError`` if template_name is not set and - ``self.required_properties`` contains "template_name". - """ - method_name = inspect.stack()[0][3] - if self.fabric_name is None and "fabric_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be set prior to accessing path." - raise ValueError(msg) - if self.template_name is None and "template_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "template_name must be set prior to accessing path." - raise ValueError(msg) - return f"{self.fabrics}/{self.fabric_name}/{self.template_name}" - - @property - def template_name(self): - """ - - getter: Return the template_name. - - setter: Set the template_name. - - setter: Raise ``ValueError`` if template_name is not a string. - """ - return self.properties["template_name"] - - @template_name.setter - def template_name(self, value): - method_name = inspect.stack()[0][3] - if value not in self.fabric_types.valid_fabric_template_names: - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid template_name: {value}. " - msg += "Expected one of: " - msg += f"{', '.join(self.fabric_types.valid_fabric_template_names)}." - raise ValueError(msg) - self.properties["template_name"] = value - - -class EpFabricConfigDeploy(Fabrics): - """ - ## V1 API - Fabrics().EpFabricConfigDeploy() - - ### Description - Return endpoint to initiate config-deploy on fabric_name. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If force_show_run is not boolean. - - ``ValueError``: If include_all_msd_switches is not boolean. - - ### Path - - ``/fabrics/{fabric_name}/config-deploy`` - - ``/fabrics/{fabric_name}/config-deploy?forceShowRun={force_show_run}`` - - ``/fabrics/{fabric_name}/config-deploy?inclAllMSDSwitches={include_all_msd_switches}`` - - ### Verb - - POST - - ### Parameters - - force_show_run: boolean - - set the ``forceShowRun`` value - - default: False - - include_all_msd_switches: boolean - - set the ``inclAllMSDSwitches`` value - - default: False - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricConfigDeploy() - instance.fabric_name = "MyFabric" - instance.force_show_run = True - instance.include_all_msd_switches = True - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "POST" - self.properties["force_show_run"] = False - self.properties["include_all_msd_switches"] = False - - @property - def force_show_run(self): - """ - - getter: Return the force_show_run value. - - setter: Set the force_show_run value. - - setter: Raise ``ValueError`` if force_show_run is not a boolean. - - Default: False - """ - return self.properties["force_show_run"] - - @force_show_run.setter - def force_show_run(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += f"Expected boolean for {method_name}. " - msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) - self.properties["force_show_run"] = value - - @property - def include_all_msd_switches(self): - """ - - getter: Return the include_all_msd_switches. - - setter: Set the include_all_msd_switches. - - setter: Raise ``ValueError`` if include_all_msd_switches is a boolean. - - Default: False - """ - return self.properties["include_all_msd_switches"] - - @include_all_msd_switches.setter - def include_all_msd_switches(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, bool): - msg = f"{self.class_name}.{method_name}: " - msg += f"Expected boolean for {method_name}. " - msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) - self.properties["include_all_msd_switches"] = value - - @property - def path(self): - """ - - Override the path property to mandate fabric_name is set. - - Raise ``ValueError`` if fabric_name is not set. - """ - _path = self.path_fabric_name - _path += "/config-deploy?" - _path += f"forceShowRun={self.force_show_run}" - _path += f"&inclAllMSDSwitches={self.include_all_msd_switches}" - return _path - - -class EpFabricConfigSave(Fabrics): - """ - ## V1 API - Fabrics().EpFabricConfigSave() - - ### Description - Return endpoint to initiate config-save on fabric_name. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If ticket_id is not a string. - - ### Path - - ``/fabrics/{fabric_name}/config-save`` - - ``/fabrics/{fabric_name}/config-save?ticketId={ticket_id}`` - - ### Verb - - POST - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - ticket_id: string - - optional unless Change Control is enabled - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricConfigSave() - instance.fabric_name = "MyFabric" - instance.ticket_id = "MyTicket1234" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "POST" - self.properties["ticket_id"] = None - - @property - def ticket_id(self): - """ - - getter: Return the ticket_id. - - setter: Set the ticket_id. - - setter: Raise ``ValueError`` if ticket_id is not a string. - - Default: None - - Note: ticket_id is optional unless Change Control is enabled. - """ - return self.properties["ticket_id"] - - @ticket_id.setter - def ticket_id(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, str): - msg = f"{self.class_name}.{method_name}: " - msg += f"Expected string for {method_name}. " - msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) - self.properties["ticket_id"] = value - - @property - def path(self): - """ - - Endpoint for config-save. - - Set self.ticket_id if Change Control is enabled. - - Raise ``ValueError`` if fabric_name is not set. - """ - _path = self.path_fabric_name - _path += "/config-save" - if self.ticket_id: - _path += f"?ticketId={self.ticket_id}" - return _path - - -class EpFabricCreate(Fabrics): - """ - ## V1 API - Fabrics().EpFabricCreate() - - ### Description - Return endpoint information. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If template_name is not set. - - ``ValueError``: If template_name is not a valid fabric template name. - - ### Path - - ``/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` - - ### Verb - - POST - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - template_name: string - - set the ``template_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricCreate() - instance.fabric_name = "MyFabric" - instance.template_name = "Easy_Fabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self.required_properties.add("template_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "POST" - - @property - def path(self): - """ - - Endpoint for fabric create. - - Raise ``ValueError`` if fabric_name is not set. - """ - return self.path_fabric_name_template_name - - -class EpFabricDelete(Fabrics): - """ - ## V1 API - Fabrics().EpFabricDelete() - - ### Description - Return endpoint to delete ``fabric_name``. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/fabrics/{fabric_name}`` - - ### Verb - - DELETE - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricDelete() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "DELETE" - - @property - def path(self): - """ - - Endpoint for fabric delete. - - Raise ``ValueError`` if fabric_name is not set. - """ - return self.path_fabric_name - - -class EpFabricDetails(Fabrics): - """ - ## V1 API - Fabrics().EpFabricDetails() - - ### Description - Return the endpoint to query ``fabric_name`` details. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/fabrics/{fabric_name}`` - - ### Verb - - GET - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricDelete() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - return self.path_fabric_name - - -class EpFabricFreezeMode(Fabrics): - """ - ## V1 API - Fabrics().EpFabricFreezeMode() - - ### Description - Return the endpoint to query ``fabric_name`` freezemode status. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/fabrics/{fabric_name}/freezemode`` - - ### Verb - - GET - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricDelete() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - return f"{self.path_fabric_name}/freezemode" - - -# class EpFabricSummary() See module_utils/common/api/v1/rest/control/switches.py - - -class EpFabricUpdate(Fabrics): - """ - ## V1 API - Fabrics().EpFabricUpdate() - - ### Description - Return endpoint information. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If template_name is not set. - - ``ValueError``: If template_name is not a valid fabric template name. - - ### Path - - ``/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` - - ### Verb - - PUT - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - template_name: string - - set the ``template_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricUpdate() - instance.fabric_name = "MyFabric" - instance.template_name = "Easy_Fabric_IPFM" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self.required_properties.add("template_name") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "PUT" - - @property - def path(self): - """ - - Endpoint for fabric create. - - Raise ``ValueError`` if fabric_name is not set. - """ - return self.path_fabric_name_template_name - - -class EpFabrics(Fabrics): - """ - ## V1 API - Fabrics().EpFabrics() - - ### Description - Return the endpoint to query fabrics. - - ### Raises - - None - - ### Path - - ``/rest/control/fabrics`` - - ### Verb - - GET - - ### Parameters - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabrics() - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._build_properties() - msg = f"ENTERED api.v1.LanFabric.Fabrics.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - return self.fabrics diff --git a/plugins/module_utils/common/api/v1/rest/control/switches/__init__.py b/plugins/module_utils/common/api/v1/rest/control/switches/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/module_utils/common/api/v1/rest/control/switches/switches.py b/plugins/module_utils/common/api/v1/rest/control/switches/switches.py deleted file mode 100644 index a6b1317c3..000000000 --- a/plugins/module_utils/common/api/v1/rest/control/switches/switches.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import inspect -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.rest.control.control import \ - Control - - -class Switches(Control): - """ - ## V1 API Switches() - api.v1.rest.control.switches.Switches() - - ### Description - Common methods and properties for Fabrics() subclasses. - - ### Path - - ``/lan-fabric/rest/control/switches/{fabric_name}`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.switches = f"{self.control}/switches" - msg = f"ENTERED api.v1.rest.control.switches.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - Populate properties specific to this class and its subclasses. - """ - self.properties["fabric_name"] = None - - @property - def fabric_name(self): - """ - - getter: Return the fabric_name. - - setter: Set the fabric_name. - - setter: Raise ``ValueError`` if fabric_name is not valid. - """ - return self.properties["fabric_name"] - - @fabric_name.setter - def fabric_name(self, value): - method_name = inspect.stack()[0][3] - try: - self.conversion.validate_fabric_name(value) - except (TypeError, ValueError) as error: - msg = f"{self.class_name}.{method_name}: " - msg += f"{error}" - raise ValueError(msg) from error - self.properties["fabric_name"] = value - - @property - def path_fabric_name(self): - """ - - Endpoint path property, including fabric_name. - - Raise ``ValueError`` if fabric_name is not set and - ``self.required_properties`` contains "fabric_name". - """ - method_name = inspect.stack()[0][3] - if self.fabric_name is None and "fabric_name" in self.required_properties: - msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be set prior to accessing path." - raise ValueError(msg) - return f"{self.switches}/{self.fabric_name}" - - -class EpFabricSummary(Switches): - """ - ## V1 API - api.v1.rest.control.switches.EpFabricSummary() - - ### Description - Return endpoint information. - - ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ### Path - - ``/switches/{fabric_name}/overview`` - - ### Verb - - GET - - ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - path: retrieve the path for the endpoint - - verb: retrieve the verb for the endpoint - - ### Usage - ```python - instance = EpFabricSummary() - instance.fabric_name = "MyFabric" - path = instance.path - verb = instance.verb - ``` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self._build_properties() - msg = f"ENTERED api.v1.rest.control.switches.{self.class_name}" - self.log.debug(msg) - - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - - @property - def path(self): - """ - - Override the path property to mandate fabric_name is set. - - Raise ``ValueError`` if fabric_name is not set. - """ - return f"{self.path_fabric_name}/overview" diff --git a/plugins/module_utils/common/api/v1/rest/rest.py b/plugins/module_utils/common/api/v1/rest/rest.py deleted file mode 100644 index 609976cc7..000000000 --- a/plugins/module_utils/common/api/v1/rest/rest.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# pylint: disable=line-too-long -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.v1 import \ - V1 - - -class Rest(V1): - """ - ## V1 API Rest() - api.v1.rest.Rest() - - ### Description - Common methods and properties for Rest() subclasses. - - ### Path - - ``/api/v1/rest`` - """ - - def __init__(self): - super().__init__() - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.rest = f"{self.v1}/rest" - msg = f"ENTERED api.v1.rest.{self.class_name}" - self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - - Populate class-specific properties. - """ From b199354ced34b9deda59b8b09deb6f9c1bac37aa Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 21 May 2024 19:33:44 -1000 Subject: [PATCH 062/230] dcnm_maintenance_mode: Initial commit 1. Add dcnm_maintenance_mode.py - main module 2. Add module_utils/common/switch_details.py - This is a fork of existing SwitchDetails() with AnsibleModule dependency removed. 3. Results(): Update docstrings with Markdown formatting. 4. ParamsValidate(): Update docstrings with Markdown formatting. 5. MaintenanceMode(): New class to enable/disable switch maintenance mode. 6. ControllerResponseError(): Add a class docstring. 7. Add /api/v1/lan_fabric/rest.inventory/inventory.py 8. EpMaintenanceModeEnable(): Added to /api/v1/lan_fabric/rest/control/fabrics/fabrics.py 9. EpMaintenanceModeDisable(): Added to /api/v1/lan_fabric/rest/control/fabrics/fabrics.py --- .../rest/control/fabrics/fabrics.py | 228 +++++- .../v1/lan_fabric/rest/inventory/__init__.py | 0 .../v1/lan_fabric/rest/inventory/inventory.py | 98 +++ plugins/module_utils/common/exceptions.py | 4 + .../module_utils/common/maintenance_mode.py | 426 ++++++++++ .../module_utils/common/params_validate.py | 33 +- plugins/module_utils/common/results.py | 63 +- plugins/module_utils/common/switch_details.py | 407 ++++++++++ plugins/modules/dcnm_maintenance_mode.py | 756 ++++++++++++++++++ 9 files changed, 1972 insertions(+), 43 deletions(-) create mode 100644 plugins/module_utils/common/api/v1/lan_fabric/rest/inventory/__init__.py create mode 100644 plugins/module_utils/common/api/v1/lan_fabric/rest/inventory/inventory.py create mode 100644 plugins/module_utils/common/maintenance_mode.py create mode 100644 plugins/module_utils/common/switch_details.py create mode 100644 plugins/modules/dcnm_maintenance_mode.py diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py index d87433cb3..136710e87 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py @@ -52,7 +52,9 @@ def _build_properties(self): - Set the fabric_name property. """ self.properties["fabric_name"] = None + self.properties["serial_number"] = None self.properties["template_name"] = None + self.properties["ticket_id"] = None @property def fabric_name(self): @@ -88,6 +90,26 @@ def path_fabric_name(self): raise ValueError(msg) return f"{self.fabrics}/{self.fabric_name}" + @property + def path_fabric_name_serial_number(self): + """ + - Endpoint path property, including fabric_name and + switch serial_number. + - Raise ``ValueError`` if fabric_name is not set. + - Raise ``ValueError`` if serial_number is not set. + - /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{fabricName}/switches/{serialNumber} + """ + method_name = inspect.stack()[0][3] + if self.fabric_name is None: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be set prior to accessing path." + raise ValueError(msg) + if self.serial_number is None: + msg = f"{self.class_name}.{method_name}: " + msg += "serial_number must be set prior to accessing path." + raise ValueError(msg) + return f"{self.fabrics}/{self.fabric_name}/switches/{self.serial_number}" + @property def path_fabric_name_template_name(self): """ @@ -108,6 +130,26 @@ def path_fabric_name_template_name(self): raise ValueError(msg) return f"{self.fabrics}/{self.fabric_name}/{self.template_name}" + @property + def serial_number(self): + """ + - getter: Return the switch serial_number. + - setter: Set the switch serial_number. + - setter: Raise ``ValueError`` if serial_number is not a string. + - Default: None + """ + return self.properties["serial_number"] + + @serial_number.setter + def serial_number(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected string for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["serial_number"] = value + @property def template_name(self): """ @@ -128,6 +170,27 @@ def template_name(self, value): raise ValueError(msg) self.properties["template_name"] = value + @property + def ticket_id(self): + """ + - getter: Return the ticket_id. + - setter: Set the ticket_id. + - setter: Raise ``ValueError`` if ticket_id is not a string. + - Default: None + - Note: ticket_id is optional unless Change Control is enabled. + """ + return self.properties["ticket_id"] + + @ticket_id.setter + def ticket_id(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected string for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self.properties["ticket_id"] = value + class EpFabricConfigDeploy(Fabrics): """ @@ -346,28 +409,6 @@ def __init__(self): def _build_properties(self): super()._build_properties() self.properties["verb"] = "POST" - self.properties["ticket_id"] = None - - @property - def ticket_id(self): - """ - - getter: Return the ticket_id. - - setter: Set the ticket_id. - - setter: Raise ``ValueError`` if ticket_id is not a string. - - Default: None - - Note: ticket_id is optional unless Change Control is enabled. - """ - return self.properties["ticket_id"] - - @ticket_id.setter - def ticket_id(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, str): - msg = f"{self.class_name}.{method_name}: " - msg += f"Expected string for {method_name}. " - msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) - self.properties["ticket_id"] = value @property def path(self): @@ -606,6 +647,149 @@ def path(self): return f"{self.path_fabric_name}/freezemode" +class EpMaintenanceModeEnable(Fabrics): + """ + ## V1 API - Fabrics().EpMaintenanceModeEnable() + + ### Description + Return endpoint to enable maintenance mode on a switch. + + ### Raises + - ``ValueError``: If ``fabric_name`` is not set. + - ``ValueError``: If ``fabric_name`` is invalid. + - ``ValueError``: If ``serial_number`` is not set. + - ``ValueError``: If ``ticket_id`` is not a string. + + ### Path + - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode`` + - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode?ticketId={ticket_id}`` + + ### Verb + - POST + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - serial_number: string + - set the switch ``serial_number`` to be used in the path + - required + - ticket_id: string + - optional unless Change Control is enabled + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpMaintenanceModeEnable() + instance.fabric_name = "MyFabric" + instance.serial_number = "CHM1234567" + instance.ticket_id = "MyTicket1234" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self.required_properties.add("serial_number") + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + """ + - Endpoint for config-save. + - Set self.ticket_id if Change Control is enabled. + - Raise ``ValueError`` if fabric_name is not set. + """ + _path = self.path_fabric_name_serial_number + _path += "/maintenance-mode" + if self.ticket_id: + _path += f"?ticketId={self.ticket_id}" + return _path + + @property + def verb(self): + return "POST" + + +class EpMaintenanceModeDisable(Fabrics): + """ + ## V1 API - Fabrics().EpMaintenanceModeDisable() + + ### Description + Return endpoint to remove switch from maintenance mode + (i.e. enable normal mode). + + ### Raises + - ``ValueError``: If ``fabric_name`` is not set. + - ``ValueError``: If ``fabric_name`` is invalid. + - ``ValueError``: If ``serial_number`` is not set. + - ``ValueError``: If ``ticket_id`` is not a string. + + ### Path + - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode`` + - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode?ticketId={ticket_id}`` + + ### Verb + - DELETE + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - serial_number: string + - set the switch ``serial_number`` to be used in the path + - required + - ticket_id: string + - optional unless Change Control is enabled + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpMaintenanceModeDisable() + instance.fabric_name = "MyFabric" + instance.serial_number = "CHM1234567" + instance.ticket_id = "MyTicket1234" + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self.required_properties.add("serial_number") + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + """ + - Endpoint for config-save. + - Set self.ticket_id if Change Control is enabled. + - Raise ``ValueError`` if fabric_name is not set. + """ + _path = self.path_fabric_name_serial_number + _path += "/maintenance-mode" + if self.ticket_id: + _path += f"?ticketId={self.ticket_id}" + return _path + + @property + def verb(self): + return "DELETE" + + # class EpFabricSummary() See module_utils/common/api/v1/rest/control/switches.py diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/inventory/__init__.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/inventory/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/inventory/inventory.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/inventory/inventory.py new file mode 100644 index 000000000..56070a67d --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/inventory/inventory.py @@ -0,0 +1,98 @@ +# 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. +# pylint: disable=line-too-long +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.rest import \ + Rest + + +class Inventory(Rest): + """ + ## api.v1.lan_fabric.rest.inventory.Inventory() + + ### Description + Common methods and properties for Inventory() subclasses. + + ### Path + - ``/api/v1/lan-fabric/rest/inventory`` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.inventory = f"{self.rest}/inventory" + msg = f"ENTERED api.v1.lan_fabric.rest.inventory.{self.class_name}" + self.log.debug(msg) + self._build_properties() + + def _build_properties(self): + """ + Populate properties specific to this class and its subclasses. + """ + + +class EpAllSwitches(Inventory): + """ + ##api.v1.lan_fabric.rest.inventory.EpAllSwitches() + + ### Description + Return endpoint information. + + ### Raises + - None + + ### Path + - ``/api/v1/lan-fabric/rest/inventory/allswitches`` + + ### Verb + - GET + + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpAllSwitches() + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self._build_properties() + msg = "ENTERED api.v1.lan_fabric.rest.inventory." + msg += f"{self.class_name}" + self.log.debug(msg) + + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" + + @property + def path(self): + """ + Return endpoint path. + """ + return f"{self.inventory}/allswitches" diff --git a/plugins/module_utils/common/exceptions.py b/plugins/module_utils/common/exceptions.py index d1947d8a9..a918779d8 100644 --- a/plugins/module_utils/common/exceptions.py +++ b/plugins/module_utils/common/exceptions.py @@ -19,4 +19,8 @@ class ControllerResponseError(Exception): + """ + Used to raise an exception when the controller returns a non-200 response. + """ + pass diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py new file mode 100644 index 000000000..46cc93a17 --- /dev/null +++ b/plugins/module_utils/common/maintenance_mode.py @@ -0,0 +1,426 @@ +# 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 logging +from typing import Dict + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import ( + EpMaintenanceModeDisable, EpMaintenanceModeEnable) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils + + +class MaintenanceMode: + """ + # Modify the maintenance mode state of a switch. + + - Raise ``ValueError`` for any caller errors, e.g. required properties + not being set before calling FabricConfigDeploy().commit(). + - Update MaintenanceMode().results to reflect success/failure of + the operation on the controller. + + ## Usage (where params is AnsibleModule.params) + + ```python + instance = MaintenanceMode(params) + instance.fabric_name = "MyFabric" + instance.mode = "maintenance" # or "normal" + instance.ip_address = "192.168.1.2" + instance.rest_send = RestSend(ansible_module) + instance.results = Results() + instance.serial_number = "FDO1234567" + try: + instance.commit() + except ValueError as error: + raise ValueError(error) from error + ``` + """ + + def __init__(self, params): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + self.params = params + self.action = "maintenance_mode" + self.cannot_perform_action_reason = "" + self.action_failed = False + self.fabric_can_be_deployed = False + + self.check_mode = self.params.get("check_mode", None) + if self.check_mode is None: + msg = f"{self.class_name}.__init__(): " + msg += "params is missing mandatory check_mode parameter." + raise ValueError(msg) + + self.state = self.params.get("state", None) + if self.state is None: + msg = f"{self.class_name}.__init__(): " + msg += "params is missing mandatory state parameter." + raise ValueError(msg) + + self.action_result: Dict[str, bool] = {} + + self.valid_modes = ["maintenance", "normal"] + self.path = None + self.verb = None + self._init_properties() + + self.conversion = ConversionUtils() + self.ep_maintenance_mode_enable = EpMaintenanceModeEnable() + self.ep_maintenance_mode_disable = EpMaintenanceModeDisable() + + msg = "ENTERED MaintenanceMode(): " + msg += f"check_mode: {self.check_mode}, " + msg += f"state: {self.state}" + self.log.debug(msg) + + def _init_properties(self): + self._properties = {} + self._properties["fabric_name"] = None + self._properties["ip_address"] = None + self._properties["mode"] = None + self._properties["rest_send"] = None + self._properties["results"] = None + self._properties["serial_number"] = None + + # def _can_fabric_be_deployed(self) -> None: + # """ + # - Set self.fabric_can_be_deployed to True if the fabric configuration + # can be deployed. + # - Set self.fabric_can_be_deployed to False otherwise. + # """ + # method_name = inspect.stack()[0][3] + + # self.fabric_can_be_deployed = False + + # deploy = self.payload.get("DEPLOY", None) + # if deploy is False or deploy is None: + # msg = f"Fabric {self.fabric_name} DEPLOY is False or None. " + # msg += "Skipping config-deploy." + # self.log.debug(msg) + # self.cannot_perform_action_reason = msg + # self.fabric_can_be_deployed = False + # self.action_failed = False + # return + + # try: + # self.fabric_summary.fabric_name = self.fabric_name + # except ValueError as error: + # msg = f"Fabric {self.fabric_name} is invalid. " + # msg += "Cannot deploy fabric. " + # msg += f"Error detail: {error}" + # self.log.debug(msg) + # self.cannot_perform_action_reason = msg + # self.fabric_can_be_deployed = False + # self.action_failed = True + # return + + # try: + # self.fabric_summary.refresh() + # except (ControllerResponseError, ValueError) as error: + # msg = f"{self.class_name}.{method_name}: " + # msg += "Error during FabricSummary().refresh(). " + # msg += f"Error detail: {error}" + # self.cannot_perform_action_reason = msg + # self.fabric_can_be_deployed = False + # self.action_failed = True + # return + + # if self.fabric_summary.fabric_is_empty is True: + # msg = f"Fabric {self.fabric_name} is empty. " + # msg += "Cannot deploy an empty fabric." + # self.log.debug(msg) + # self.cannot_perform_action_reason = msg + # self.fabric_can_be_deployed = False + # self.action_failed = False + # return + + # try: + # self.fabric_details.refresh() + # except ValueError as error: + # msg = f"{self.class_name}.{method_name}: " + # msg += "Error during FabricDetailsByName().refresh(). " + # msg += f"Error detail: {error}" + # self.cannot_perform_action_reason = msg + # self.fabric_can_be_deployed = False + # self.action_failed = True + # return + + # self.fabric_details.filter = self.fabric_name + + # if self.fabric_details.deployment_freeze is True: + # msg = f"Fabric {self.fabric_name} DEPLOYMENT_FREEZE == True. " + # msg += "Cannot deploy a fabric with deployment freeze enabled." + # self.log.debug(msg) + # self.cannot_perform_action_reason = msg + # self.fabric_can_be_deployed = False + # self.action_failed = False + # return + + # if self.fabric_details.is_read_only is True: + # msg = f"Fabric {self.fabric_name} IS_READ_ONLY == True. " + # msg += "Cannot deploy a read only fabric." + # self.log.debug(msg) + # self.cannot_perform_action_reason = msg + # self.fabric_can_be_deployed = False + # self.action_failed = False + # return + + # self.fabric_can_be_deployed = True + + def commit(self): + """ + - Initiate a config-deploy operation on the controller. + - Raise ``ValueError`` if FabricConfigDeploy().fabric_name is not set. + - Raise ``ValueError`` if FabricConfigDeploy().ip_address is not set. + - Raise ``ValueError`` if FabricConfigDeploy().mode is not set. + - Raise ``ValueError`` if FabricConfigDeploy().rest_send is not set. + - Raise ``ValueError`` if FabricConfigDeploy().results is not set. + - Raise ``ValueError`` if FabricConfigDeploy().serial_number is not set. + - Raise ``ValueError`` if the endpoint assignment fails. + """ + method_name = inspect.stack()[0][3] + + if self.fabric_name is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.fabric_name must be set " + msg += "before calling commit." + raise ValueError(msg) + if self.ip_address is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.ip_address must be set " + msg += "before calling commit." + raise ValueError(msg) + if self.mode is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.mode must be set " + msg += "before calling commit." + raise ValueError(msg) + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.rest_send must be set " + msg += "before calling commit." + raise ValueError(msg) + if self.results is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.results must be set " + msg += "before calling commit." + raise ValueError(msg) + if self.serial_number is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.serial_number must be set " + msg += "before calling commit." + raise ValueError(msg) + + # self._can_fabric_be_deployed() + + msg = f"{self.class_name}.{method_name}: " + msg += f"action_failed: {self.action_failed}" + msg += f"fabric_name: {self.fabric_name}, " + msg += f"mode: {self.mode}, " + msg += f"ip_address: {self.ip_address}, " + # msg += f"fabric_can_be_deployed: {self.fabric_can_be_deployed}, " + # msg += f"cannot_perform_action_reason: {self.cannot_perform_action_reason}" + msg += f"serial_number: {self.serial_number}, " + self.log.debug(msg) + + # if self.fabric_can_be_deployed is False: + # self.results.diff_current = {} + # self.results.action = self.action + # self.results.check_mode = self.check_mode + # self.results.state = self.state + # self.results.response_current = { + # "RETURN_CODE": 200, + # "MESSAGE": self.cannot_perform_action_reason, + # } + # if self.action_failed is True: + # self.results.result_current = {"changed": False, "success": False} + # else: + # self.results.result_current = {"changed": True, "success": True} + # self.results.register_task_result() + # return + + if self.mode == "maintenance": + endpoint = self.ep_maintenance_mode_enable + else: + endpoint = self.ep_maintenance_mode_disable + + try: + endpoint.fabric_name = self.fabric_name + endpoint.serial_number = self.serial_number + self.path = endpoint.path + self.verb = endpoint.verb + except ValueError as error: + self.results.diff_current = {} + self.results.result_current = self.results.failed_result + self.results.register_task_result() + raise ValueError(error) from error + + self.rest_send.path = self.path + self.rest_send.verb = self.verb + self.rest_send.payload = None + self.rest_send.commit() + + result = self.rest_send.result_current["success"] + self.action_result[self.ip_address] = result + if self.action_result[self.ip_address] is False: + self.results.diff_current = {} + else: + self.results.diff_current = { + "ip_address": self.ip_address, + f"{self.action}": "OK", + } + + self.results.action = self.action + self.results.check_mode = self.check_mode + self.results.state = self.state + self.results.response_current = copy.deepcopy(self.rest_send.response_current) + self.results.result_current = copy.deepcopy(self.rest_send.result_current) + self.results.register_task_result() + + @property + def fabric_name(self): + """ + The name of the fabric to config-save. + """ + return self._properties["fabric_name"] + + @fabric_name.setter + def fabric_name(self, value): + try: + self.conversion.validate_fabric_name(value) + except (TypeError, ValueError) as error: + raise ValueError(error) from error + self._properties["fabric_name"] = value + + @property + def ip_address(self): + """ + - The ip_address of the switch. Used only for more informative + error messages. + - Raise ``ValueError`` if the value is not a string. + """ + return self._properties["ip_address"] + + @ip_address.setter + def ip_address(self, value): + method_name = inspect.stack()[0][3] + + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name} must be a string. " + msg += f"Got type: {type(value).__name__}." + self.log.debug(msg) + raise ValueError(msg) + self._properties["ip_address"] = value + + @property + def mode(self): + """ + The indended mode. + - getter: Return the mode. + - setter: Set the mode. + - setter: Raise ``ValueError`` if the value is not one of + "maintenance" or "normal". + """ + return self._properties["mode"] + + @mode.setter + def mode(self, value): + if value not in self.valid_modes: + msg = f"{self.class_name}.mode is invalid. " + msg += f"Got value {value}. " + msg += f"Expected one of {','.join(self.valid_modes)}." + raise ValueError(msg) + self._properties["mode"] = value + + @property + def rest_send(self): + """ + - getter: Return an instance of the RestSend class. + - setter: Set an instance of the RestSend class. + - setter: Raise ``TypeError`` if the value is not an + instance of RestSend. + """ + return self._properties["rest_send"] + + @rest_send.setter + def rest_send(self, value): + method_name = inspect.stack()[0][3] + _class_name = None + msg = f"{self.class_name}.{method_name}: " + msg += "value must be an instance of RestSend. " + try: + _class_name = value.class_name + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _class_name != "RestSend": + self.log.debug(msg) + raise TypeError(msg) + self._properties["rest_send"] = value + + @property + def results(self): + """ + - getter: Return an instance of the Results class. + - setter: Set an instance of the Results class. + - setter: Raise ``TypeError`` if the value is not an + instance of Results. + """ + return self._properties["results"] + + @results.setter + def results(self, value): + method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += "value must be an instance of Results. " + msg += f"Got value {value} of type {type(value).__name__}." + _class_name = None + try: + _class_name = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + self.log.debug(msg) + raise TypeError(msg) from error + if _class_name != "Results": + self.log.debug(msg) + raise TypeError(msg) + self._properties["results"] = value + + @property + def serial_number(self): + """ + - The serial_number of the switch. + - Raise ``ValueError`` if the value is not a string. + """ + return self._properties["serial_number"] + + @serial_number.setter + def serial_number(self, value): + method_name = inspect.stack()[0][3] + + if not isinstance(value, str): + msg = f"{self.class_name}.{method_name} must be a string. " + msg += f"Got type: {type(value).__name__}." + self.log.debug(msg) + raise ValueError(msg) + self._properties["serial_number"] = value diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index a38a07836..2c064d09d 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -29,23 +29,27 @@ class ParamsValidate: """ + ### Summary Validate playbook parameters. - This expects the following: - 1. parameters: fully-merged dictionary of parameters - 2. params_spec: Dictionary that describes each parameter + ### Mandatory Properties + - ``parameters``: fully-merged dictionary of parameters + - ``params_spec``: Dictionary that describes each parameter in parameters - Usage (where ansible_module is an instance of AnsibleModule): + ### Usage - Assume the following params_spec describing parameters - ip_address and foo. - ip_address is a required parameter of type ipv4. - foo is an optional parameter of type dict. - foo contains a parameter named bar that is an optional - parameter of type str with a default value of bingo. - bar can be assigned one of three values: bingo, bango, or bongo. + - Ansible_module is an instance of AnsibleModule): + Assume the following params_spec describing parameters + ``ip_address`` and ``foo`` . + - ``ip_address`` is a required parameter of type ipv4. + - ``foo`` is an optional parameter of type dict. + - ``foo`` contains a parameter named ``bar`` that is an optional + parameter of type str with a default value of bingo. + - ``bar`` can be assigned one of three values: bingo, bango, or bongo. + + ```python params_spec: Dict[str, Any] = {} params_spec["ip_address"] = {} params_spec["ip_address"]["required"] = False @@ -62,18 +66,25 @@ class ParamsValidate: params_spec["foo"]["baz"]["type"] = int params_spec["foo"]["baz"]["range_min"] = 0 params_spec["foo"]["baz"]["range_max"] = 10 + ``` Which describes the following YAML: + ```yaml ip_address: 1.2.3.4 foo: bar: bingo baz: 10 + ``` + + ### Invocation + ```python validator = ParamsValidate(ansible_module) validator.parameters = ansible_module.params validator.params_spec = params_spec validator.commit() + ``` """ def __init__(self, ansible_module): diff --git a/plugins/module_utils/common/results.py b/plugins/module_utils/common/results.py index 9ef9e8115..ed4c23e1b 100644 --- a/plugins/module_utils/common/results.py +++ b/plugins/module_utils/common/results.py @@ -149,7 +149,7 @@ def commit(self): "success": true } - An examplke of a metadata dict would be (sequence_number is added by Results): + An example of a metadata dict would be (sequence_number is added by Results): { "action": "merge", @@ -301,7 +301,24 @@ def register_task_result(self): def build_final_result(self): """ - Build the final result + Build the final result. This consists of the following: + ```json + { + "changed": True, # or False + "failed": True, + "diff": { + [], + }, + "response": { + [], + }, + "result": { + [], + }, + "metadata": { + [], + } + ``` """ msg = f"self.changed: {self.changed}, " msg = f"self.failed: {self.failed}, " @@ -424,9 +441,9 @@ def diff(self, value): @property def diff_current(self): """ - Return the current diff - - This is a dict of the current diff set by the handler. + - getter: Return the current diff + - setter: Set the current diff + - setter: raise ``ValueError`` if value is not a dict """ value = self.properties.get("diff_current") value["sequence_number"] = self.task_sequence_number @@ -472,7 +489,9 @@ def metadata(self): List of dicts representing the metadata (if any) for each diff. - raise ValueError if value is not a dict + - getter: Return the metadata + - setter: Append value to the metadata list + - setter: raise ``ValueError`` if value is not a dict """ return self.properties["metadata"] @@ -489,8 +508,8 @@ def metadata(self, value): @property def metadata_current(self): """ - Return the current metadata which is comprised of the - properties action, check_mode, and state. + - getter: Return the current metadata which is comprised of the + properties action, check_mode, and state. """ value = {} value["action"] = self.action @@ -506,6 +525,10 @@ def response_current(self): instance.commit() must be called first. This is a dict of the current response from the controller. + + - getter: Return the current response + - setter: Set the current response + - setter: raise ``ValueError`` if value is not a dict """ value = self.properties.get("response_current") value["sequence_number"] = self.task_sequence_number @@ -528,6 +551,10 @@ def response(self): instance.commit() must be called first. This is a list of responses from the controller. + + - getter: Return the response list + - setter: Append value to the response list + - setter: raise ``ValueError`` if value is not a dict """ return self.properties.get("response") @@ -545,7 +572,11 @@ def response(self, value): @property def response_data(self): """ - Return the contents of the DATA key within current_response. + - getter: Return the contents of the DATA key within + ``current_response``. + - setter: set ``response_data`` to the value passed in + which should be the contents of the DATA key within + ``current_response``. """ return self.properties.get("response_data") @@ -560,6 +591,10 @@ def result(self): instance.commit() must be called first. This is a list of results from the controller. + + - getter: Return the result list + - setter: Append value to the result list + - setter: raise ``ValueError`` if value is not a dict """ return self.properties.get("result") @@ -581,6 +616,10 @@ def result_current(self): instance.commit() must be called first. This is a dict containing the current result. + + - getter: Return the current result + - setter: Set the current result + - setter: raise ``ValueError`` if value is not a dict """ value = self.properties.get("result_current") value["sequence_number"] = self.task_sequence_number @@ -599,7 +638,11 @@ def result_current(self, value): @property def state(self): """ - Ansible state + The Ansible state + + - getter: Return the state + - setter: Set the state + - setter: raise ``ValueError`` if value is not a string """ return self.properties["state"] diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py new file mode 100644 index 000000000..448f73f02 --- /dev/null +++ b/plugins/module_utils/common/switch_details.py @@ -0,0 +1,407 @@ +# +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import json +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.inventory.inventory import \ + EpAllSwitches +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError + + +class SwitchDetails: + """ + Retrieve switch details from the controller and provide property accessors + for the switch attributes. + + ### Usage + ```python + instance = SwitchDetails() + instance.results = Results() + instance.rest_send = RestSend(ansible_module) + instance.refresh() + instance.filter = "10.1.1.1" + fabric_name = instance.fabric_name + serial_number = instance.serial_number + etc... + ``` + + ### Endpoint + ``/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches`` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED common.SwitchDetails()") + + self.conversions = ConversionUtils() + self.ep_all_switches = EpAllSwitches() + self.path = self.ep_all_switches.path + self.verb = self.ep_all_switches.verb + + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["filter"] = None + self.properties["info"] = {} + self.properties["params"] = None + + def validate_commit_parameters(self): + """ + Validate that mandatory parameters are set before calling refresh(). + + ### Raises + - ``ValueError`` if instance.rest_send is not set. + - ``ValueError`` if instance.results is not set. + """ + method_name = inspect.stack()[0][3] + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.rest_send must be set before calling " + msg += f"{self.class_name}.refresh()." + raise ValueError(msg) + if self.results is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.results must be set before calling " + msg += f"{self.class_name}.refresh()." + raise ValueError(msg) + + def refresh(self): + """ + Refresh switch_details with current switch details from + the controller. + + ### Raises + - ``ControllerResponseError`` if the controller response is not 200. + """ + method_name = inspect.stack()[0][3] + + self.validate_commit_parameters() + + # Regardless of ansible_module.check_mode, we need to get the switch details + # So, set check_mode to False + self.rest_send.check_mode = False + self.rest_send.verb = self.verb + self.rest_send.path = self.path + self.rest_send.commit() + + msg = "self.rest_send.response_current: " + msg += ( + f"{json.dumps(self.rest_send.response_current, indent=4, sort_keys=True)}" + ) + self.log.debug(msg) + + msg = "self.rest_send.result_current: " + msg += f"{json.dumps(self.rest_send.result_current, indent=4, sort_keys=True)}" + self.log.debug(msg) + + self.results.response_current = self.rest_send.response_current + self.results.response = self.rest_send.response_current + self.results.result_current = self.rest_send.result_current + self.results.result = self.rest_send.result_current + + if self.results.response_current.get("RETURN_CODE") == 200: + self.results.failed = False + else: + self.results.failed = True + # SwitchDetails never changes the controller state + self.results.changed = False + + if self.results.response_current["RETURN_CODE"] != 200: + msg = f"{self.class_name}.{method_name}: " + msg += "Unable to retrieve switch information from the controller. " + msg += f"Got response {self.results.response_current}" + raise ControllerResponseError(msg) + + data = self.results.response_current.get("DATA") + self.properties["info"] = {} + for switch in data: + self.properties["info"][switch["ipAddress"]] = switch + + msg = "self.properties[info]: " + msg += f"{json.dumps(self.properties['info'], indent=4, sort_keys=True)}" + self.log.debug(msg) + + def _get(self, item): + """ + Return the value of the item from the filtered switch. + + ### Raises + - ``ValueError`` if ``filter`` is not set. + - ``ValueError`` if ``filter`` is not in the controller response. + - ``ValueError`` if item is not in the filtered switch dict. + """ + method_name = inspect.stack()[0][3] + + if self.filter is None: + msg = f"{self.class_name}.{method_name}: " + msg += "set instance.filter before accessing " + msg += f"property {item}." + raise ValueError(msg) + + if self.filter not in self.properties["info"]: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.filter} does not exist on the controller." + raise ValueError(msg) + + if item not in self.properties["info"][self.filter]: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.filter} does not have a key named {item}." + raise ValueError(msg) + + return self.conversions.make_boolean( + self.conversions.make_none(self.properties["info"][self.filter].get(item)) + ) + + @property + def filter(self): + """ + Set the query filter. + + The filter should be the ip_address of the switch from which to + retrieve details. + + ``filter`` must be set before accessing this class's properties. + """ + return self.properties.get("filter") + + @filter.setter + def filter(self, value): + self.properties["filter"] = value + + @property + def fabric_name(self): + """ + - Return the ``fabricName`` of the filtered switch, if it exists. + - Return ``None`` otherwise. + """ + return self._get("fabricName") + + @property + def hostname(self): + """ + - Return the ``hostName`` of the filtered switch, if it exists. + - Return ``None`` otherwise. + + ### NOTES + - ``hostname`` is None for NDFC version 12.1.2e + - Better to use ``logical_name`` which is populated + in both NDFC versions 12.1.2e and 12.1.3b + """ + return self._get("hostName") + + @property + def info(self): + """ + - Return parsed data from the GET request. + - Return ``None`` otherwise + + NOTE: Keyed on ip_address + """ + return self.properties["info"] + + @property + def is_non_nexus(self): + """ + - Return the ``isNonNexus`` status of the filtered switch, if it exists. + - Return ``None`` otherwise + - Example: false, true + """ + return self._get("isNonNexus") + + @property + def logical_name(self): + """ + - Return the ``logicalName`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("logicalName") + + @property + def managable(self): + """ + - Return the ``managable`` status of the filtered switch, if it exists. + - Return ``None`` otherwise + - Example: false, true + """ + return self._get("managable") + + @property + def mode(self): + """ + - Return the ``mode`` of the filtered switch, if it exists. + - Return ``None`` otherwise + - ``mode`` is converted from Titlecase to lowercase. + - Example: maintenance, migration, normal, inconsistent + """ + mode = self._get("mode") + if mode is None: + return None + return mode.lower() + + @property + def model(self): + """ + - Return the ``model`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("model") + + @property + def oper_status(self): + """ + - Return the ``operStatus`` of the filtered switch, if it exists. + - Return ``None`` otherwise + - Example: Minor + """ + return self._get("operStatus") + + @property + def platform(self): + """ + - Return the ``platform`` of the filtered switch, if it exists. + - Return ``None`` otherwise + + ### NOTES + - ``platform`` is derived from ``model``. + It is not in the controller response. + """ + model = self._get("model") + if model is None: + return None + return model.split("-")[0] + + @property + def release(self): + """ + - Return the ``release`` of the filtered switch, if it exists. + - Return ``None`` otherwise + - Example: 10.2(5) + """ + return self._get("release") + + @property + def rest_send(self): + """ + An instance of the ``RestSend`` class. + """ + return self.properties["rest_send"] + + @rest_send.setter + def rest_send(self, value): + self.properties["rest_send"] = value + + @property + def results(self): + """ + An instance of the ``Results`` class. + """ + return self.properties["results"] + + @results.setter + def results(self, value): + self.properties["results"] = value + + @property + def role(self): + """ + - Return the ``switchRole`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("switchRole") + + @property + def serial_number(self): + """ + - Return the ``serialNumber`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("serialNumber") + + @property + def source_interface(self): + """ + - Return the ``sourceInterface`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("sourceInterface") + + @property + def source_vrf(self): + """ + - Return the ``sourceVrf`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("sourceVrf") + + @property + def status(self): + """ + - Return the ``status`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("status") + + @property + def switch_db_id(self): + """ + - Return the ``switchDbID`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("switchDbID") + + @property + def switch_role(self): + """ + - Return the ``switchRole`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("switchRole") + + @property + def switch_uuid(self): + """ + - Return the ``swUUID`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("swUUID") + + @property + def switch_uuid_id(self): + """ + - Return the ``swUUIDId`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("swUUIDId") + + @property + def system_mode(self): + """ + - Return the ``systemMode`` of the filtered switch, if it exists. + - Return ``None`` otherwise + """ + return self._get("systemMode") diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py new file mode 100644 index 000000000..5f1283ace --- /dev/null +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -0,0 +1,756 @@ +#!/usr/bin/python +# +# Copyright (c) 2020-2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +DOCUMENTATION = """ +--- +module: dcnm_maintenance_mode +short_description: Manage Maintenance Mode Configuration of NX-OS Switches. +version_added: "3.5.0" +author: Allen Robel (@quantumonion) +description: +- Enable Maintenance or Normal Mode. +options: + state: + choices: + - merged + - query + default: merged + description: + - The state of the feature or object after module completion + type: str + config: + description: + - A dictionary containing the maintenance mode configuration. + type: dict + required: true + suboptions: + deploy: + description: + - Whether to deploy the switch configurations. + default: False + required: false + type: bool + mode: + default: maintenance + description: + - Enable maintenance or normal mode on all switches. + required: false + type: bool + switches: + description: + - A list of target switches. + - Per-switch options override the global options. + required: false + type: list + elements: dict + suboptions: + ip_address: + description: + - The IP address of the switch. + required: true + type: str + mode: + description: + - Enable maintenance or normal mode for the switch. + required: true + type: str + deploy: + default: False + description: + - Whether to deploy the switch configuration. + required: false + type: bool +""" + +EXAMPLES = """ + +# Enable maintenance mode on all switches. +# Do not deploy the configuration on any switch. + +- name: Configure switch mode + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: false + mode: maintenance + switches: + - ip_address: 192.168.1.2 + - ip_address: 192.160.1.3 + - ip_address: 192.160.1.4 + register: result +- debug: + var: result + +# Enable maintenance mode on two switches. +# Enable normal mode on one switch. +# Deploy the configuration on one switch. + +- name: Configure switch mode + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: false + mode: maintenance + switches: + - ip_address: 192.168.1.2 + mode: normal + - ip_address: 192.160.1.3 + deploy: true + - ip_address: 192.160.1.4 + register: result +- debug: + var: result + + +""" +# pylint: disable=wrong-import-position +import copy +import inspect +import json +import logging +from os import environ +from typing import Any, Dict, List + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ + EpMaintenanceModeDisable, EpMaintenanceModeEnable +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log +from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import \ + MaintenanceMode +from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ + MergeDicts +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults import \ + ParamsMergeDefaults +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ + ParamsValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ + RestSend +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results +from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ + SwitchDetails + +# TODO: Write a VerifyPlaybookParams class for maintenance mode +class VerifyPlaybookParams: + """ + Verify the playbook parameters for the maintenance mode module + """ + + def __init__(self): + self.class_name = self + +def json_pretty(msg): + """ + Return a pretty-printed JSON string for logging messages + """ + return json.dumps(msg, indent=4, sort_keys=True) + +class ParamsSpec: + """ + Build parameter specifications for the dcnm_maintenance_mode module. + + ### Usage + ```python + from ansible.module_utils.basic import AnsibleModule + + argument_spec = {} + argument_spec["config"] = { + "required": True, + "type": "dict", + } + argument_spec["state"] = { + "choices": ["merged", "query"], + "default": "merged", + "required": False, + "type": "str" + } + + ansible_module = AnsibleModule( + argument_spec=argument_spec, supports_check_mode=True + ) + + params_spec = ParamsSpec() + params_spec.params = ansible_module.params + params_spec.commit() + spec = params_spec.params_spec + ``` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED ParamsSpec()") + + self._properties = {} + self._properties["params"] = None + self._params_spec: Dict[str, Any] = {} + + self.valid_states = ["merged", "query"] + + def commit(self): + """ + Build the parameter specification based on the state + + ## Raises + - ValueError if params.state is not a valid state for + the dcnm_maintenance_mode module + """ + method_name = inspect.stack()[0][3] + + if self.params["state"] not in self.valid_states: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid state {self.params['state']}. " + msg += f"Expected one of {', '.join(self.valid_states)}." + raise ValueError(msg) + + if self.params["state"] == "merged": + self._build_params_spec_for_merged_state() + if self.params["state"] == "query": + self._build_params_spec_for_query_state() + + def _build_params_spec_for_merged_state(self) -> Dict[str, Any]: + """ + Build the parameter specifications for ``merged`` state. + """ + self._params_spec: Dict[str, Any] = {} + self._params_spec["ip_address"] = {} + self._params_spec["ip_address"]["required"] = True + self._params_spec["ip_address"]["type"] = "ipv4" + + self._params_spec["mode"] = {} + self._params_spec["mode"]["required"] = False + self._params_spec["mode"]["type"] = "str" + + self._params_spec["deploy"] = {} + self._params_spec["deploy"]["required"] = False + self._params_spec["deploy"]["type"] = "bool" + self._params_spec["deploy"]["default"] = False + + def _build_params_spec_for_query_state(self) -> Dict[str, Any]: + """ + Build the parameter specifications for ``query`` state. + """ + self._params_spec: Dict[str, Any] = {} + self._params_spec["ip_address"] = {} + self._params_spec["ip_address"]["required"] = True + self._params_spec["ip_address"]["type"] = "ipv4" + + @property + def params_spec(self) -> Dict[str, Any]: + """ + return the parameter specification + """ + return self._params_spec + + @property + def params(self) -> Dict[str, Any]: + """ + Expects value to be the return value of + ``AnsibleModule.params`` property. + + - getter: return the params + - setter: set the params + - setter: raise ``ValueError`` if value is not a dict + """ + return self._properties["params"] + @params.setter + def params(self, value: Dict[str, Any]) -> None: + """ + - setter: set the params + """ + self._properties["params"] = value + +class Common: + """ + Common methods, properties, and resources for all states. + """ + + def __init__(self, params): + self.class_name = self.__class__.__name__ + self.params = params + self.log = logging.getLogger(f"dcnm.{self.class_name}") + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + self.check_mode = self.params.get("check_mode", None) + if self.check_mode is None: + msg = f"{self.class_name}.__init__(): " + msg += "check_mode is required" + raise ValueError(msg) + + self.state = self.params.get("state", None) + if self.state is None: + msg = f"{self.class_name}.__init__(): " + msg += "state is required" + raise ValueError(msg) + + self.results = Results() + self.results.state = self.state + self.results.check_mode = self.check_mode + + self.params_spec = ParamsSpec() + self.params_spec.params = self.params + try: + self.params_spec.commit() + except ValueError as error: + self.ansible_module.fail_json(error, **self.results.failed_result) + + msg = f"ENTERED Common().{method_name}: " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + + self._verify_playbook_params = VerifyPlaybookParams() + + self.switch_details = SwitchDetails() + self.switch_details.results = self.results + + # populated in self.validate_input() + self.payloads = {} + + self.config = self.params.get("config") + if not isinstance(self.config, dict): + msg = "expected dict type for self.config. " + msg += f"got {type(self.config).__name__}" + raise ValueError(msg) + + self.validated = [] + self.have = {} + self.want = [] + self.query = [] + self._implemented_states = set() + # populated in self._merge_global_and_switch_configs() + self.switch_configs = [] + + self._init_properties() + + def _init_properties(self): + self._properties = {} + self._properties["ansible_module"] = None + + def get_have(self): + """ + Caller: main() + + Build self.have, a dict containing the current mode of all switches. + + Have is a dict, keyed on switch_ip, where each element is a dict + with the following structure: + + ```json + { + "192.169.1.2": { + fabric_name: "MyFabric", + mode: "Maintenance", + serial_number: "FCI1234567" + }, + "192.169.1.3": { + fabric_name: "YourFabric", + mode: "Normal", + serial_number: "FCH2345678" + } + } + ``` + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self.switch_details.rest_send = RestSend(self.ansible_module) + self.switch_details.refresh() + self.have = {} + # self.config has already been validated + for switch in self.config.get("switches"): + ip_address = switch.get("ip_address") + self.switch_details.filter = ip_address + serial_number = self.switch_details.serial_number + if serial_number is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"Switch with ip_address {ip_address} " + msg += "does not exist on the controller." + self.ansible_module.fail_json(msg, **self.results.failed_result) + mode = self.switch_details.mode + fabric_name = self.switch_details.fabric_name + self.have[ip_address] = {} + self.have[ip_address].update({"mode": mode}) + self.have[ip_address].update({"serial_number": serial_number}) + self.have[ip_address].update({"fabric_name": fabric_name}) + + def get_want(self) -> None: + """ + Caller: main() + + 1. Merge the global config into each switch config + 2. Validate the merged configs + 3. Populate self.want with the validated configs + + ### self.want structure + + ```json + { + "192.168.1.2" { + "mode": "maintenance", + "deploy": false + }, + "192.168.1.3" { + "mode": "normal", + "deploy": true + } + } + ``` + """ + msg = "ENTERED" + self.log.debug(msg) + # Generate the params_spec used to validate the configs + params_spec = ParamsSpec() + params_spec.params = self.params + params_spec.commit() + + # Builds self.switch_configs + self._merge_global_and_switch_configs(self.config) + + # If a parameter is missing from the config, and the parameter + # has a default value, merge the default value for the parameter + # into the config. + merged_configs = [] + merge_defaults = ParamsMergeDefaults(self.ansible_module) + merge_defaults.params_spec = params_spec.params_spec + for config in self.switch_configs: + merge_defaults.parameters = config + merge_defaults.commit() + merged_configs.append(merge_defaults.merged_parameters) + + # validate the merged configs + self.validated_configs = [] + self.validator = ParamsValidate(self.ansible_module) + self.validator.params_spec = params_spec.params_spec + for config in merged_configs: + self.validator.parameters = config + self.validator.commit() + self.want.append(copy.deepcopy(config)) + + # Exit if there's nothing to do + if len(self.want) == 0: + self.ansible_module.exit_json(**self.results.ok_result) + + def _merge_global_and_switch_configs(self, config) -> None: + """ + Merge the global config with each switch config and + populate list of merged configs self.switch_configs. + + Merge rules: + 1. switch_config takes precedence over global_config. + 2. If switch_config is missing a parameter, use parameter + from global_config. + 3. If switch_config has a parameter, use it. + """ + method_name = inspect.stack()[0][3] + + if not config.get("switches"): + msg = f"{self.class_name}.{method_name}: " + msg += "playbook is missing list of switches" + self.ansible_module.fail_json(msg) + + self.switch_configs = [] + merged_configs = [] + for switch in config["switches"]: + # we need to rebuild global_config in this loop + # because merge_dicts modifies it in place + global_config = copy.deepcopy(config) + global_config.pop("switches", None) + msg = ( + f"global_config: {json.dumps(global_config, indent=4, sort_keys=True)}" + ) + self.log.debug(msg) + + msg = f"switch PRE_MERGE : {json.dumps(switch, indent=4, sort_keys=True)}" + self.log.debug(msg) + + merge_dicts = MergeDicts(self.ansible_module) + merge_dicts.dict1 = global_config + merge_dicts.dict2 = switch + merge_dicts.commit() + switch_config = merge_dicts.dict_merged + + msg = f"switch POST_MERGE: {json.dumps(switch_config, indent=4, sort_keys=True)}" + self.log.debug(msg) + + merged_configs.append(switch_config) + self.switch_configs = copy.copy(merged_configs) + + @property + def ansible_module(self): + """ + getter: return an instance of AnsibleModule + setter: set an instance of AnsibleModule + """ + return self._properties["ansible_module"] + + @ansible_module.setter + def ansible_module(self, value): + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + if not isinstance(value, AnsibleModule): + msg = f"{self.class_name}.{method_name}: " + msg += "expected AnsibleModule instance. " + msg += f"got {type(value).__name__}." + raise ValueError(msg) + self._properties["ansible_module"] = value + +class Merged(Common): + """ + Handle merged state + """ + + def __init__(self, params): + self.class_name = self.__class__.__name__ + super().__init__(params) + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + msg = f"ENTERED Merged.{method_name}: " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + + self.need = [] + + self._implemented_states.add("merged") + + def get_need(self): + """ + Build self.need for merged state. + + ### Caller + commit() + + ### self.need structure + ```json + { + "172.22.150.2": { + "deploy": false + "fabric_name": "MyFabric", + "mode": "maintenance", + "serial_number": "FCI1234567" + }, + "172.22.150.3": { + "deploy": true + "fabric_name": "YourFabric", + "mode": "normal", + "serial_number": "HMD2345678" + } + } + """ + self.need = {} + for want in self.want: + want_ip = want.get("ip_address", None) + if want_ip not in self.have: + continue + serial_number = self.have[want_ip]["serial_number"] + fabric_name = self.have[want_ip]["fabric_name"] + if want.get("mode") != self.have[want_ip]["mode"]: + self.need[want_ip] = want + self.need[want_ip].update({"fabric_name": fabric_name}) + self.need[want_ip].update({"serial_number": serial_number}) + self.need[want_ip].update({"mode": want.get("mode")}) + + def commit(self): + """ + Commit the merged state request + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + msg = f"{self.class_name}.{method_name}: entered" + self.log.debug(msg) + + self.rest_send = RestSend(self.ansible_module) + + self.get_want() + self.get_have() + self.get_need() + self.send_need() + + def send_need(self) -> None: + """ + Caller: commit() + + Build and send the payload to modify maintenance mode. + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + msg = f"{self.class_name}.{method_name}: entered. " + msg += f"self.need: {json_pretty(self.need)}" + self.log.debug(msg) + + if len(self.need) == 0: + msg = f"{self.class_name}.{method_name}: " + msg += "No switches to modify." + self.log.debug(msg) + return + + instance = MaintenanceMode(self.params) + instance.rest_send = RestSend(self.ansible_module) + instance.results = self.results + for ip_address, switch in self.need.items(): + mode = switch.get("mode", None) + serial_number = switch.get("serial_number", None) + fabric_name = switch.get("fabric_name", None) + deploy = switch.get("deploy", False) + try: + instance.fabric_name = fabric_name + instance.ip_address = ip_address + instance.mode = mode + instance.serial_number = serial_number + instance.commit() + except ValueError as error: + self.results.build_final_result() + self.ansible_module.fail_json(f"{error}", **self.results.final_result) + + +class Query(Common): + """ + Handle query state + """ + + def __init__(self, ansible_module): + self.class_name = self.__class__.__name__ + super().__init__(ansible_module) + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + msg = "ENTERED Query(): " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + + self._implemented_states.add("query") + + def commit(self) -> None: + """ + 1. query the switches in self.want that exist on the controller + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + self.get_want() + + self.switch_details.rest_send = RestSend(self.ansible_module) + self.switch_details.refresh() + # self.config has already been validated + for ip_address in self.want.keys(): + self.switch_details.filter = ip_address + serial_number = self.switch_details.serial_number + if serial_number is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"Switch with ip_address {ip_address} " + msg += "does not exist on the controller." + self.ansible_module.fail_json(msg, **self.results.failed_result) + mode = self.switch_details.mode + fabric_name = self.switch_details.fabric_name + self.results.diff_current = { + "fabric_name": fabric_name, + "ip_address": ip_address, + "mode": mode, + "serial_number": serial_number, + } + self.results.changed = False + self.results.action = "query" + self.results.failed = False + self.results.result_current = {"changed": False, "success": True} + self.results.register_task_result() + + +def main(): + """main entry point for module execution""" + + argument_spec = {} + argument_spec["config"] = { + "required": True, + "type": "dict", + } + # argument_spec["deploy"] = { + # "default": True, + # "required": False, + # "type": "bool", + # } + # argument_spec["mode"] = { + # "choices": ["Maintenance", "Normal"], + # "default": "Maintenance", + # "required": False, + # "type": "str" + # } + argument_spec["state"] = { + "choices": ["merged", "query"], + "default": "merged", + "required": False, + "type": "str" + } + + ansible_module = AnsibleModule( + argument_spec=argument_spec, supports_check_mode=True + ) + log = Log(ansible_module) + + # Create the base/parent logger for the dcnm collection. + # Set the following environment variable to enable logging: + # - NDFC_LOGGING_CONFIG= + # logging_config.json must be must be conformant with logging.config.dictConfig + # and must not log to the console. + # For an example logging_config.json configuration, see: + # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json + config_file = environ.get("NDFC_LOGGING_CONFIG", None) + if config_file is not None: + log.config = config_file + try: + log.commit() + except json.decoder.JSONDecodeError as error: + msg = f"Invalid logging configuration file: {log.config}. " + msg += f"Error detail: {error}" + ansible_module.fail_json(msg) + except ValueError as error: + msg = f"Invalid logging configuration file: {log.config}. " + msg += f"Error detail: {error}" + ansible_module.fail_json(msg) + + ansible_module.params["check_mode"] = ansible_module.check_mode + if ansible_module.params["state"] == "merged": + task = Merged(ansible_module.params) + task.ansible_module = ansible_module + task.commit() + elif ansible_module.params["state"] == "query": + task = Query(ansible_module.params) + task.ansible_module = ansible_module + task.commit() + else: + # We should never get here since the state parameter has + # already been validated. + msg = f"Unknown state {task.ansible_module.params['state']}" + ansible_module.fail_json(msg) + + task.results.build_final_result() + + # Results().failed is a property that returns a set() + # of boolean values. pylint doesn't seem to understand this so we've + # disabled the unsupported-membership-test warning. + if True in task.results.failed: # pylint: disable=unsupported-membership-test + msg = "Module failed." + ansible_module.fail_json(msg, **task.results.final_result) + ansible_module.exit_json(**task.results.final_result) + + +if __name__ == "__main__": + main() From 903200063e41095e3530450d7d7e7997ccada4a7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 21 May 2024 19:52:29 -1000 Subject: [PATCH 063/230] dcnm_maintenance_mode: Add to test/sanity/ignore-* --- tests/sanity/ignore-2.10.txt | 1 + tests/sanity/ignore-2.11.txt | 1 + tests/sanity/ignore-2.12.txt | 1 + tests/sanity/ignore-2.13.txt | 1 + tests/sanity/ignore-2.14.txt | 1 + tests/sanity/ignore-2.9.txt | 1 + 6 files changed, 6 insertions(+) diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 16a03434b..60d9043d3 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -15,5 +15,6 @@ plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 lic plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.6!skip plugins/modules/dcnm_rest.py import-2.7!skip diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index 27ab1ec0a..4723c583b 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -15,6 +15,7 @@ plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 lic plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.6!skip plugins/modules/dcnm_rest.py import-2.7!skip plugins/httpapi/dcnm.py import-2.7!skip diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 82cf53b09..334160f16 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -15,6 +15,7 @@ plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 lic plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.6!skip plugins/modules/dcnm_rest.py import-2.7!skip plugins/httpapi/dcnm.py import-3.8!skip diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index a95eca621..b535a3144 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -16,6 +16,7 @@ plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GP plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.7!skip plugins/httpapi/dcnm.py import-3.8!skip plugins/httpapi/dcnm.py import-3.9!skip diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index 1e315bd7d..15705d33b 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -16,6 +16,7 @@ plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GP plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.7!skip plugins/httpapi/dcnm.py import-3.9!skip plugins/httpapi/dcnm.py import-3.10!skip diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 16a03434b..60d9043d3 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -15,5 +15,6 @@ plugins/modules/dcnm_links.py validate-modules:missing-gplv3-license # GPLv3 lic plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.6!skip plugins/modules/dcnm_rest.py import-2.7!skip From 417ca97380f79343588474056f27a6404ca3302f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 21 May 2024 20:15:33 -1000 Subject: [PATCH 064/230] Fix PEP8 errors, more... 1. Query() was treating self.want as a dict instead of as a list. --- plugins/modules/dcnm_maintenance_mode.py | 59 +++++++++++------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 5f1283ace..685b02672 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -126,13 +126,9 @@ import json import logging from os import environ -from typing import Any, Dict, List +from typing import Any, Dict from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ - EpMaintenanceModeDisable, EpMaintenanceModeEnable -from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ - ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import \ MaintenanceMode @@ -149,14 +145,6 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ SwitchDetails -# TODO: Write a VerifyPlaybookParams class for maintenance mode -class VerifyPlaybookParams: - """ - Verify the playbook parameters for the maintenance mode module - """ - - def __init__(self): - self.class_name = self def json_pretty(msg): """ @@ -164,6 +152,7 @@ def json_pretty(msg): """ return json.dumps(msg, indent=4, sort_keys=True) + class ParamsSpec: """ Build parameter specifications for the dcnm_maintenance_mode module. @@ -273,6 +262,7 @@ def params(self) -> Dict[str, Any]: - setter: raise ``ValueError`` if value is not a dict """ return self._properties["params"] + @params.setter def params(self, value: Dict[str, Any]) -> None: """ @@ -280,6 +270,7 @@ def params(self, value: Dict[str, Any]) -> None: """ self._properties["params"] = value + class Common: """ Common methods, properties, and resources for all states. @@ -303,6 +294,8 @@ def __init__(self, params): msg += "state is required" raise ValueError(msg) + self._init_properties() + self.results = Results() self.results.state = self.state self.results.check_mode = self.check_mode @@ -319,20 +312,23 @@ def __init__(self, params): msg += f"check_mode: {self.check_mode}" self.log.debug(msg) - self._verify_playbook_params = VerifyPlaybookParams() - self.switch_details = SwitchDetails() self.switch_details.results = self.results # populated in self.validate_input() self.payloads = {} + # populated in self.get_want() + self.validated_configs = [] + self.config = self.params.get("config") if not isinstance(self.config, dict): msg = "expected dict type for self.config. " msg += f"got {type(self.config).__name__}" raise ValueError(msg) + self.validator = ParamsValidate(self.ansible_module) + self.validated = [] self.have = {} self.want = [] @@ -341,8 +337,6 @@ def __init__(self, params): # populated in self._merge_global_and_switch_configs() self.switch_configs = [] - self._init_properties() - def _init_properties(self): self._properties = {} self._properties["ansible_module"] = None @@ -403,16 +397,18 @@ def get_want(self) -> None: ### self.want structure ```json - { - "192.168.1.2" { + [ + { + "ip_address": "192.168.1.2", "mode": "maintenance", "deploy": false }, - "192.168.1.3" { - "mode": "normal", - "deploy": true + { + "ip_address": "192.168.1.3", + "mode": "maintenance", + "deploy": false } - } + ] ``` """ msg = "ENTERED" @@ -438,7 +434,6 @@ def get_want(self) -> None: # validate the merged configs self.validated_configs = [] - self.validator = ParamsValidate(self.ansible_module) self.validator.params_spec = params_spec.params_spec for config in merged_configs: self.validator.parameters = config @@ -512,6 +507,7 @@ def ansible_module(self, value): raise ValueError(msg) self._properties["ansible_module"] = value + class Merged(Common): """ Handle merged state @@ -609,7 +605,7 @@ def send_need(self) -> None: mode = switch.get("mode", None) serial_number = switch.get("serial_number", None) fabric_name = switch.get("fabric_name", None) - deploy = switch.get("deploy", False) + # deploy = switch.get("deploy", False) try: instance.fabric_name = fabric_name instance.ip_address = ip_address @@ -651,7 +647,8 @@ def commit(self) -> None: self.switch_details.rest_send = RestSend(self.ansible_module) self.switch_details.refresh() # self.config has already been validated - for ip_address in self.want.keys(): + for item in self.want: + ip_address = item.get("ip_address") self.switch_details.filter = ip_address serial_number = self.switch_details.serial_number if serial_number is None: @@ -662,10 +659,10 @@ def commit(self) -> None: mode = self.switch_details.mode fabric_name = self.switch_details.fabric_name self.results.diff_current = { - "fabric_name": fabric_name, - "ip_address": ip_address, - "mode": mode, - "serial_number": serial_number, + "fabric_name": fabric_name, + "ip_address": ip_address, + "mode": mode, + "serial_number": serial_number, } self.results.changed = False self.results.action = "query" @@ -697,7 +694,7 @@ def main(): "choices": ["merged", "query"], "default": "merged", "required": False, - "type": "str" + "type": "str", } ansible_module = AnsibleModule( From 43278a4083b4750a14ada1b48919030327aa8224 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 22 May 2024 07:53:22 -1000 Subject: [PATCH 065/230] Modify diff to indicate what mode was entered. Previously the diff looked like: ```json { "ip_address": "172.22.150.107", "maintenance_mode": "OK", "sequence_number": 1 } ``` Changed it to indicate the mode the switch was changed to. ```json { "ip_address": "172.22.150.107", "maintenance_mode": "normal", "sequence_number": 1 } ``` --- plugins/module_utils/common/maintenance_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 46cc93a17..80c2b26d3 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -287,7 +287,7 @@ def commit(self): else: self.results.diff_current = { "ip_address": self.ip_address, - f"{self.action}": "OK", + f"{self.action}": self.mode, } self.results.action = self.action From 552a8b5892f50d8d04bce1f9e9173885e20f2df0 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 22 May 2024 11:14:08 -1000 Subject: [PATCH 066/230] SwitchDetails(): add maintenance_mode property maintenance_mode is synthesized from mode and system_mode. mode: NDFC's current configuration for the switch's systemMode system_mode: The switch's current running state for systemMode. When mode and system_mode differ, NDFC reports this (in a private API) as "inconsistent". maintenance_mode is intended to mimick the behavior of NDFC's private API. maintenance_mode will return "inconsistent if mode != system_mode. maintenance_mode will otherwise return mode (i.e. NDFC's current state for the switch's system mode) --- plugins/module_utils/common/switch_details.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 448f73f02..4f4fe1ae1 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -241,6 +241,24 @@ def logical_name(self): """ return self._get("logicalName") + @property + def maintenance_mode(self): + """ + - Return a synthesized value for ``maintenanceMode`` status of the + filtered switch, if it exists. + - Return ``mode`` otherwise. + - Example: ``inconsistent``, ``maintenance``, ``migration``, ``normal`` + + ### NOTES + - ``mode`` is the current NDFC configured value of the switch's + ``systemMode`` (``system_mode``), whereas ``system_mode`` is the + current value on the switch. When these differ, NDFC displays + ``inconsistent`` for the switch's Mode. + """ + if self.mode != self.system_mode: + return "inconsistent" + return self.mode + @property def managable(self): """ From c5705d13e29c885e5b8507578351c146b76da683 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 22 May 2024 13:45:54 -1000 Subject: [PATCH 067/230] SwitchDetails(): compare values using lower() --- plugins/module_utils/common/switch_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 4f4fe1ae1..6f2c818e1 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -255,7 +255,7 @@ def maintenance_mode(self): current value on the switch. When these differ, NDFC displays ``inconsistent`` for the switch's Mode. """ - if self.mode != self.system_mode: + if self.mode.lower() != self.system_mode.lower(): return "inconsistent" return self.mode From 44f04edd7ae15933da14ce5b3d033c66591ced2f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 22 May 2024 13:59:50 -1000 Subject: [PATCH 068/230] MaintenanceMode(): implement config-deploy 1. import EpFabricConfigDeploy. 2. Add a deploy property. 3. Add method deploy_switch() and call from commit() if deploy is True. 4. Refactor commit() to move parameter validation to verify_commit_parameters() 5. Improve some docstrings. 6. EpFabricConfigDeploy(): Update docstring Usage section to include switch_id. --- .../rest/control/fabrics/fabrics.py | 3 +- .../module_utils/common/maintenance_mode.py | 120 +++++++++++++++--- 2 files changed, 104 insertions(+), 19 deletions(-) diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py index 136710e87..29d4af547 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py @@ -247,8 +247,7 @@ class EpFabricConfigDeploy(Fabrics): ```python instance = EpFabricConfigDeploy() instance.fabric_name = "MyFabric" - instance.force_show_run = True - instance.include_all_msd_switches = True + instance.switch_id = "CHM1234567" path = instance.path verb = instance.verb ``` diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 80c2b26d3..e49d4d86c 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -23,7 +23,7 @@ from typing import Dict from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import ( - EpMaintenanceModeDisable, EpMaintenanceModeEnable) + EpFabricConfigDeploy, EpMaintenanceModeDisable, EpMaintenanceModeEnable) from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils @@ -95,6 +95,7 @@ def __init__(self, params): def _init_properties(self): self._properties = {} + self._properties["deploy"] = None self._properties["fabric_name"] = None self._properties["ip_address"] = None self._properties["mode"] = None @@ -187,19 +188,18 @@ def _init_properties(self): # self.fabric_can_be_deployed = True - def commit(self): + def verify_commit_parameters(self): """ - - Initiate a config-deploy operation on the controller. - - Raise ``ValueError`` if FabricConfigDeploy().fabric_name is not set. - - Raise ``ValueError`` if FabricConfigDeploy().ip_address is not set. - - Raise ``ValueError`` if FabricConfigDeploy().mode is not set. - - Raise ``ValueError`` if FabricConfigDeploy().rest_send is not set. - - Raise ``ValueError`` if FabricConfigDeploy().results is not set. - - Raise ``ValueError`` if FabricConfigDeploy().serial_number is not set. - - Raise ``ValueError`` if the endpoint assignment fails. + Verify that required parameters are set before calling commit. + + - Raise ``ValueError`` if ``fabric_name`` is not set. + - Raise ``ValueError`` if ``ip_address`` is not set. + - Raise ``ValueError`` if ``mode`` is not set. + - Raise ``ValueError`` if ``rest_send`` is not set. + - Raise ``ValueError`` if ``results`` is not set. + - Raise ``ValueError`` if ``serial_number`` is not set. """ method_name = inspect.stack()[0][3] - if self.fabric_name is None: msg = f"{self.class_name}.{method_name}: " msg += f"{self.class_name}.fabric_name must be set " @@ -231,10 +231,28 @@ def commit(self): msg += "before calling commit." raise ValueError(msg) - # self._can_fabric_be_deployed() + def commit(self): + """ + - Initiate a config-deploy operation on the controller. + - Re-raise ``ValueError`` if ``fabric_name`` is not set. + - Re-raise ``ValueError`` if ``ip_address`` is not set. + - Re-raise ``ValueError`` if ``mode`` is not set. + - Re-raise ``ValueError`` if ``rest_send`` is not set. + - Re-raise ``ValueError`` if ``results`` is not set. + - Re-raise ``ValueError`` if ``serial_number`` is not set. + """ + method_name = inspect.stack()[0][3] + try: + self.verify_commit_parameters() + except ValueError as error: + raise ValueError(error) from error + + # self._can_fabric_be_deployed() + # /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{fabricName}/switches/{serialNumber}/deploy-maintenance-mode msg = f"{self.class_name}.{method_name}: " msg += f"action_failed: {self.action_failed}" + msg += f"deploy: {self.deploy}, " msg += f"fabric_name: {self.fabric_name}, " msg += f"mode: {self.mode}, " msg += f"ip_address: {self.ip_address}, " @@ -259,6 +277,18 @@ def commit(self): # self.results.register_task_result() # return + self.change_system_mode() + + if self.deploy is True: + self.deploy_switch() + + def change_system_mode(self): + """ + Change the ``systemMode`` configuration for the switch. + + ### Raises + - ``ValueError`` if endpoint resolution fails. + """ if self.mode == "maintenance": endpoint = self.ep_maintenance_mode_enable else: @@ -280,14 +310,14 @@ def commit(self): self.rest_send.payload = None self.rest_send.commit() + action = "maintenance_mode" result = self.rest_send.result_current["success"] - self.action_result[self.ip_address] = result - if self.action_result[self.ip_address] is False: + if result is False: self.results.diff_current = {} else: self.results.diff_current = { "ip_address": self.ip_address, - f"{self.action}": self.mode, + f"{action}": self.mode, } self.results.action = self.action @@ -297,10 +327,66 @@ def commit(self): self.results.result_current = copy.deepcopy(self.rest_send.result_current) self.results.register_task_result() + def deploy_switch(self): + """ + Initiate a switch config-deploy. + """ + # Start the config-deploy + ep_deploy = EpFabricConfigDeploy() + ep_deploy.fabric_name = self.fabric_name + ep_deploy.switch_id = self.serial_number + self.rest_send.path = ep_deploy.path + self.rest_send.verb = ep_deploy.verb + self.rest_send.payload = None + self.rest_send.commit() + + # Register the result + action = "config_deploy" + result = self.rest_send.result_current["success"] + if result is False: + self.results.diff_current = {} + else: + self.results.diff_current = { + "ip_address": self.ip_address, + f"{action}": result, + } + + self.results.action = action + self.results.check_mode = self.check_mode + self.results.state = self.state + self.results.response_current = copy.deepcopy(self.rest_send.response_current) + self.results.result_current = copy.deepcopy(self.rest_send.result_current) + self.results.register_task_result() + + @property + def deploy(self): + """ + Whether to issue a recalculate and deploy on the switch + after changing the mode. + + - getter: Return the deploy value. + - setter: Set the deploy value. + - setter: Raise ``ValueError`` if the value is not a boolean. + """ + return self._properties["deploy"] + + @deploy.setter + def deploy(self, value): + if not isinstance(value, bool): + msg = f"{self.class_name}.deploy must be a boolean. " + msg += f"Got type: {type(value).__name__}." + raise ValueError(msg) + self._properties["deploy"] = value + @property def fabric_name(self): """ - The name of the fabric to config-save. + The name of the fabric to which the switch belongs. + + - getter: Return the fabric_name. + - setter: Set the fabric_name. + - setter: Raise ``ValueError`` if the value is not a valid + fabric name. """ return self._properties["fabric_name"] @@ -335,7 +421,7 @@ def ip_address(self, value): @property def mode(self): """ - The indended mode. + The indended maintenance mode. - getter: Return the mode. - setter: Set the mode. - setter: Raise ``ValueError`` if the value is not one of From 993b1bfa6f570941ebc15d256dccc4d2efd4e4e8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 22 May 2024 15:52:49 -1000 Subject: [PATCH 069/230] Use SwitchDetails().maintenance_mode property 1. dcnm_maintenance_mode.py 1. get_have() change mode key to maintenance_mode in self.have to reflect that we are accessing SwitchDetails().maintenance_mode property 2. Merged().get_need() change mode key to maintenance_mode in self.need so that it's more clear that we are working with maintenance mode rather than some other mode. --- plugins/modules/dcnm_maintenance_mode.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 685b02672..1a448c11d 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -354,12 +354,12 @@ def get_have(self): { "192.169.1.2": { fabric_name: "MyFabric", - mode: "Maintenance", + maintenance_mode: "Maintenance", serial_number: "FCI1234567" }, "192.169.1.3": { fabric_name: "YourFabric", - mode: "Normal", + maintenance_mode: "Normal", serial_number: "FCH2345678" } } @@ -379,10 +379,10 @@ def get_have(self): msg += f"Switch with ip_address {ip_address} " msg += "does not exist on the controller." self.ansible_module.fail_json(msg, **self.results.failed_result) - mode = self.switch_details.mode + mode = self.switch_details.maintenance_mode fabric_name = self.switch_details.fabric_name self.have[ip_address] = {} - self.have[ip_address].update({"mode": mode}) + self.have[ip_address].update({"maintenance_mode": mode}) self.have[ip_address].update({"serial_number": serial_number}) self.have[ip_address].update({"fabric_name": fabric_name}) @@ -542,13 +542,13 @@ def get_need(self): "172.22.150.2": { "deploy": false "fabric_name": "MyFabric", - "mode": "maintenance", + "maintenance_mode": "maintenance", "serial_number": "FCI1234567" }, "172.22.150.3": { "deploy": true "fabric_name": "YourFabric", - "mode": "normal", + "maintenance_mode": "normal", "serial_number": "HMD2345678" } } @@ -560,11 +560,12 @@ def get_need(self): continue serial_number = self.have[want_ip]["serial_number"] fabric_name = self.have[want_ip]["fabric_name"] - if want.get("mode") != self.have[want_ip]["mode"]: + if want.get("mode") != self.have[want_ip]["maintenance_mode"]: self.need[want_ip] = want + self.need[want_ip].update({"deploy": want.get("deploy")}) self.need[want_ip].update({"fabric_name": fabric_name}) self.need[want_ip].update({"serial_number": serial_number}) - self.need[want_ip].update({"mode": want.get("mode")}) + self.need[want_ip].update({"maintenance_mode": want.get("mode")}) def commit(self): """ @@ -602,11 +603,12 @@ def send_need(self) -> None: instance.rest_send = RestSend(self.ansible_module) instance.results = self.results for ip_address, switch in self.need.items(): - mode = switch.get("mode", None) + mode = switch.get("maintenance_mode", None) serial_number = switch.get("serial_number", None) fabric_name = switch.get("fabric_name", None) - # deploy = switch.get("deploy", False) + deploy = switch.get("deploy", False) try: + instance.deploy = deploy instance.fabric_name = fabric_name instance.ip_address = ip_address instance.mode = mode From 2f281a913e99990e193d05ce136eaeb7b815230e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 22 May 2024 16:22:44 -1000 Subject: [PATCH 070/230] RestSend(): improve docstrings --- plugins/module_utils/common/rest_send.py | 183 ++++++++++++++++------- 1 file changed, 127 insertions(+), 56 deletions(-) diff --git a/plugins/module_utils/common/rest_send.py b/plugins/module_utils/common/rest_send.py index 9fd433e9e..6a1c65e7a 100644 --- a/plugins/module_utils/common/rest_send.py +++ b/plugins/module_utils/common/rest_send.py @@ -34,14 +34,19 @@ class RestSend: """ + ### Summary Send REST requests to the controller with retries, and handle responses. - Usage (where ansible_module is an instance of AnsibleModule): + ### Usage + ``ansible_module`` is an instance of ``AnsibleModule``. + ```python rest_send = RestSend(ansible_module) rest_send.path = "/rest/top-down/fabrics" rest_send.verb = "GET" - rest_send.payload = my_payload # Optional + rest_send.payload = my_payload # optional + rest_send.timeout = 300 # optional + rest_send.unit_test = True # optional rest_send.commit() # list of responses from the controller for this session @@ -52,6 +57,7 @@ class RestSend: result = rest_send.result # dict with current controller result result_current = rest_send.result_current + ``` """ def __init__(self, ansible_module): @@ -85,6 +91,9 @@ def __init__(self, ansible_module): self.log.debug(msg) def _verify_commit_parameters(self): + """ + Verify that required parameters are set prior to calling ``commit()`` + """ if self.verb is None: msg = f"{self.class_name}._verify_commit_parameters: " msg += "verb must be set before calling commit()." @@ -110,14 +119,14 @@ def commit_check_mode(self): """ Simulate a dcnm_send() call for check_mode - Properties read: - self.verb: HTTP verb e.g. GET, POST, PUT, DELETE - self.path: HTTP path e.g. http://controller_ip/path/to/endpoint - self.payload: Optional HTTP payload + ### Properties read: + - ``verb``: HTTP verb e.g. DELETE, GET, POST, PUT + - ``path``: HTTP path e.g. http://controller_ip/path/to/endpoint + - ``payload``: Optional HTTP payload - Properties written: - self.properties["response_current"]: raw simulated response - self.properties["result_current"]: result from self._handle_response() method + ### Properties written: + - ``response_current``: raw simulated response + - ``result_current``: result from self._handle_response() method """ method_name = inspect.stack()[0][3] caller = inspect.stack()[1][3] @@ -147,16 +156,18 @@ def commit_normal_mode(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 + ### Raises + - AnsibleModule.fail_json() if the response is not a dict + ### Properties read + - ``send_interval``: interval between retries (set in ImageUpgradeCommon) + - ``timeout``: timeout in seconds (set in ImageUpgradeCommon) + - ``verb``: HTTP verb e.g. GET, POST, PUT, DELETE + - ``path``: HTTP path e.g. http://controller_ip/path/to/endpoint + - ``payload`` Optional HTTP payload - Properties written: - self.properties["response"]: raw response from the controller - self.properties["result"]: result from self._handle_response() method + ## Properties written + - ``response``: raw response from the controller + - ``result``: result from self._handle_response() method """ method_name = inspect.stack()[0][3] caller = inspect.stack()[1][3] @@ -247,16 +258,20 @@ def _handle_unknown_request_verbs(self, response): 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 + ### Summary + Handle GET responses from the controller. + + ### Caller + ``self._handle_response()`` + + ### 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} @@ -280,18 +295,21 @@ def _handle_get_response(self, response): def _handle_post_put_delete_response(self, response): """ - Caller: - - self.self._handle_response() - + ### Summary 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 + ### Caller + ``self.self._handle_response()`` + + + ### 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: @@ -309,17 +327,18 @@ def _handle_post_put_delete_response(self, response): @property def check_mode(self): """ + ### Summary Determines if dcnm_send should be called. - Default: False + ### Default + ``False`` - If False, dcnm_send is called. Real controller responses - are returned by RestSend() + - If ``False``, dcnm_send is called. Real controller responses + are returned by RestSend() + - If ``True``, dcnm_send is not called. Simulated controller + responses are returned by RestSend() - If True, dcnm_send is not called. Simulated controller responses - are returned by RestSend() - - Discussion: + ### Discussion We don't set check_mode from the value of self.ansible_module.check_mode because we want to be able to read data from the controller even when self.ansible_module.check_mode is True. For example, SwitchIssuDetails @@ -349,7 +368,12 @@ def failed_result(self): def path(self): """ Endpoint path for the REST request. - e.g. "/appcenter/cisco/ndfc/api/v1/...etc..." + + ### Raises + None + + ### Example + ``/appcenter/cisco/ndfc/api/v1/...etc...`` """ return self.properties.get("path") @@ -361,6 +385,9 @@ def path(self, value): def payload(self): """ Return the payload to send to the controller + + ### Raises + None """ return self.properties["payload"] @@ -372,9 +399,11 @@ def payload(self, value): def response_current(self): """ Return the current POST response from the controller - instance.commit() must be called first. + as a ``dict``. ``commit()`` must be called first. - This is a dict of the current response from the controller. + - getter: Return a copy of ``response_current`` + - setter: Set ``response_current`` + - setter: call ``Ansible.fail_json`` if value is not a dict """ return copy.deepcopy(self.properties.get("response_current")) @@ -392,9 +421,12 @@ def response_current(self, value): def response(self): """ Return the aggregated POST response from the controller - instance.commit() must be called first. + ``commit()`` must be called first. This is a list of responses from the controller. + - getter: Return a copy of ``response`` + - setter: Append to ``response`` + - setter: call ``Ansible.fail_json`` if value is not a dict """ return copy.deepcopy(self.properties.get("response")) @@ -415,6 +447,10 @@ def result(self): instance.commit() must be called first. This is a list of results from the controller. + + - getter: Return a copy of result + - setter: Append to result + - setter: call ``Ansible.fail_json`` if value is not a dict """ return copy.deepcopy(self.properties.get("result")) @@ -435,6 +471,10 @@ def result_current(self): instance.commit() must be called first. This is a dict containing the current result. + + - getter: Return a copy of ``result_current`` + - setter: Set ``result_current`` + - setter: call ``Ansible.fail_json`` if value is not a dict """ return copy.deepcopy(self.properties.get("result_current")) @@ -451,9 +491,21 @@ def result_current(self, value): @property def send_interval(self): """ + ### Summary Send interval, in seconds, for retrying responses from the controller. - Valid values: int() - Default: 5 + + ### Valid values + ``int`` + ### Default + ``5`` + + ### Raises + Calls ``AnsibleModule.fail_json`` if value is not an ``int`` + + - getter: Returns ``send_interval`` + - setter: Sets ``send_interval`` + - setter: Calls ``AnsibleModule.fail_json`` if value is not + an ``int`` """ return self.properties.get("send_interval") @@ -469,9 +521,17 @@ def send_interval(self, value): @property def timeout(self): """ + ### Summary Timeout, in seconds, for retrieving responses from the controller. - Valid values: int() - Default: 300 + + ### Raises + Calls ``AnsibleModule.fail_json`` if value is not an ``int`` + + ### Valid values + ``int`` + + ### Default + ``300`` """ return self.properties.get("timeout") @@ -487,9 +547,15 @@ def timeout(self, value): @property def unit_test(self): """ - Is the class running under a unit test. + ### Summary + Is RestSend being called from a unit test. Set this to True in unit tests to speed the test up. - Default: False + + ### Raises + Calls ``AnsibleModule.fail_json`` if value is not an ``bool`` + + ### Default + ``False`` """ return self.properties.get("unit_test") @@ -506,7 +572,12 @@ def unit_test(self, value): def verb(self): """ Verb for the REST request. - One of "GET", "POST", "PUT", "DELETE" + + ### Raises + Calls ``AnsibleModule.fail_json`` if value is not a valid verb. + + ### Valid values + ``GET``, ``POST``, ``PUT``, ``DELETE`` """ return self.properties.get("verb") From b8a51d069d788785ef263fae4ae6d5f4500fe183 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 23 May 2024 08:53:34 -1000 Subject: [PATCH 071/230] Handle "inconsistent" and "migration" states. --- plugins/module_utils/common/switch_details.py | 53 ++++++++++++------- plugins/modules/dcnm_maintenance_mode.py | 20 +++++++ 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 6f2c818e1..3b8920f9d 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -102,23 +102,13 @@ def refresh(self): self.validate_commit_parameters() - # Regardless of ansible_module.check_mode, we need to get the switch details - # So, set check_mode to False + # Regardless of ansible_module.check_mode, we need to get the + # switch details. So, set check_mode to False. self.rest_send.check_mode = False self.rest_send.verb = self.verb self.rest_send.path = self.path self.rest_send.commit() - msg = "self.rest_send.response_current: " - msg += ( - f"{json.dumps(self.rest_send.response_current, indent=4, sort_keys=True)}" - ) - self.log.debug(msg) - - msg = "self.rest_send.result_current: " - msg += f"{json.dumps(self.rest_send.result_current, indent=4, sort_keys=True)}" - self.log.debug(msg) - self.results.response_current = self.rest_send.response_current self.results.response = self.rest_send.response_current self.results.result_current = self.rest_send.result_current @@ -142,10 +132,6 @@ def refresh(self): for switch in data: self.properties["info"][switch["ipAddress"]] = switch - msg = "self.properties[info]: " - msg += f"{json.dumps(self.properties['info'], indent=4, sort_keys=True)}" - self.log.debug(msg) - def _get(self, item): """ Return the value of the item from the filtered switch. @@ -244,17 +230,48 @@ def logical_name(self): @property def maintenance_mode(self): """ + ### Summary - Return a synthesized value for ``maintenanceMode`` status of the filtered switch, if it exists. - Return ``mode`` otherwise. - - Example: ``inconsistent``, ``maintenance``, ``migration``, ``normal`` + - Values: + - ``inconsistent``: ``mode`` and ``system_mode`` differ. + See NOTES. + - ``maintenance``: The switch is in maintenance mode. It has + withdrawn its routes, etc, from the fabric so that traffic + does not traverse the switch. Maintenance operations will + not impact traffic in the hosting fabric. + - ``migration``: The switch config is not compatible with the + switch role in the hosting fabric. Manual remediation is + required. + - ``normal``: The switch is participating as a traffic + forwarding agent in the hosting fabric. + + ### Raises + - ``ValueError`` if ``mode`` cannot be ascertained. + - ``ValueError`` if ``system_mode`` cannot be ascertained. ### NOTES - ``mode`` is the current NDFC configured value of the switch's ``systemMode`` (``system_mode``), whereas ``system_mode`` is the current value on the switch. When these differ, NDFC displays - ``inconsistent`` for the switch's Mode. + ``inconsistent`` for the switch's ``maintenanceMode`` state. + To resolve ``inconsistent`` state, a switch ``config-deploy`` + must be initiated on the controller. """ + method_name = inspect.stack()[0][3] + if self.mode is None: + msg = f"{self.class_name}.{method_name}: " + msg += "mode is not set. Either ``filter`` has not been " + msg += "set, or the controller response is invalid." + raise ValueError(msg) + if self.system_mode is None: + msg = f"{self.class_name}.{method_name}: " + msg += "system_mode is not set. Either ``filter`` has not been " + msg += "set, or the controller response is invalid." + raise ValueError(msg) + if self.mode.lower() == "migration": + return "migration" if self.mode.lower() != self.system_mode.lower(): return "inconsistent" return self.mode diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 1a448c11d..63546699b 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -380,6 +380,26 @@ def get_have(self): msg += "does not exist on the controller." self.ansible_module.fail_json(msg, **self.results.failed_result) mode = self.switch_details.maintenance_mode + if mode == "inconsistent": + msg = f"{self.class_name}.{method_name}: " + msg += "Switch maintenance mode state differs from the " + msg += "controller's maintenance mode state for switch " + msg += f"with ip_address {ip_address}. This is typically " + msg += "resolved by initiating a switch Deploy Config on " + msg += "the controller." + self.ansible_module.fail_json(msg, **self.results.failed_result) + if mode == "migration": + msg = f"{self.class_name}.{method_name}: " + msg += "Switch maintenance mode is in migration state for the " + msg += f"switch with ip_address {ip_address}. " + msg += "This indicates that the switch configuration is not " + msg += "compatible with the switch role in the hosting " + msg += "fabric. The issue might be resolved by initiating a " + msg += "fabric Recalculate & Deploy on the controller. " + msg += "Failing that, the switch configuration might need to be " + msg += "manually modified to match the switch role in the " + msg += "hosting fabric." + self.ansible_module.fail_json(msg, **self.results.failed_result) fabric_name = self.switch_details.fabric_name self.have[ip_address] = {} self.have[ip_address].update({"maintenance_mode": mode}) From c92ecd90f70a8980f253f379beeaee03013639b1 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 23 May 2024 09:10:29 -1000 Subject: [PATCH 072/230] Handle non-existent switch case. --- plugins/module_utils/common/switch_details.py | 3 ++- plugins/modules/dcnm_maintenance_mode.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 3b8920f9d..3b2eff178 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -151,7 +151,8 @@ def _get(self, item): if self.filter not in self.properties["info"]: msg = f"{self.class_name}.{method_name}: " - msg += f"{self.filter} does not exist on the controller." + msg += f"Switch with ip_address {self.filter} does not exist on " + msg += "the controller." raise ValueError(msg) if item not in self.properties["info"][self.filter]: diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 63546699b..4913b7c96 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -373,7 +373,10 @@ def get_have(self): for switch in self.config.get("switches"): ip_address = switch.get("ip_address") self.switch_details.filter = ip_address - serial_number = self.switch_details.serial_number + try: + serial_number = self.switch_details.serial_number + except ValueError as error: + self.ansible_module.fail_json(f"{error}", **self.results.failed_result) if serial_number is None: msg = f"{self.class_name}.{method_name}: " msg += f"Switch with ip_address {ip_address} " From 9147c09b0f0f3034e94c33de4d1eb50d819a4312 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 23 May 2024 09:55:40 -1000 Subject: [PATCH 073/230] General error handling improvements, more... 1. SwitchDetails().refresh(): Catch and re-raise ValueError if mandatory parameters are not set. 2. ParamsSpec().params setter: Raise ValueError if value is not a dict. 3. Common().__init__(): Catch ParamsSpec().params ValueError and call fail_json() if self.params is invalid. 4. SwitchDetails(): Remove unused json import. --- plugins/module_utils/common/switch_details.py | 7 +++++-- plugins/modules/dcnm_maintenance_mode.py | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 3b2eff178..45dbbd0a6 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -19,7 +19,6 @@ __author__ = "Allen Robel" import inspect -import json import logging from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.inventory.inventory import \ @@ -97,10 +96,14 @@ def refresh(self): ### Raises - ``ControllerResponseError`` if the controller response is not 200. + - ``ValueError`` if mandatory parameters are not set. """ method_name = inspect.stack()[0][3] - self.validate_commit_parameters() + try: + self.validate_commit_parameters() + except ValueError as error: + raise ValueError(error) from error # Regardless of ansible_module.check_mode, we need to get the # switch details. So, set check_mode to False. diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 4913b7c96..b42703171 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -178,7 +178,10 @@ class ParamsSpec: ) params_spec = ParamsSpec() - params_spec.params = ansible_module.params + try: + params_spec.params = ansible_module.params + except ValueError as error: + ansible_module.fail_json(error) params_spec.commit() spec = params_spec.params_spec ``` @@ -268,6 +271,11 @@ def params(self, value: Dict[str, Any]) -> None: """ - setter: set the params """ + if not isinstance(value, dict): + msg = f"{self.class_name}.params.setter: " + msg += "expected dict type for value. " + msg += f"got {type(value).__name__}." + raise ValueError(msg) self._properties["params"] = value @@ -301,7 +309,10 @@ def __init__(self, params): self.results.check_mode = self.check_mode self.params_spec = ParamsSpec() - self.params_spec.params = self.params + try: + self.params_spec.params = self.params + except ValueError as error: + self.ansible_module.fail_json(error, **self.results.failed_result) try: self.params_spec.commit() except ValueError as error: From 8f788c06c6e9de2cc27352052c30767b8c1e5e05 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 23 May 2024 11:10:44 -1000 Subject: [PATCH 074/230] Implement query state. 1. Common().get_have(): Move method to Merged() 2. Merged().get_have(): Added from Common() and slightly modified. 3. Query().get_have(): New method - differs from Merged().get_have() in that it doesn't call fail_json() if mode is "inconsistent" or "migration". 4. main(): remove commented code. --- plugins/modules/dcnm_maintenance_mode.py | 303 +++++++++++++---------- 1 file changed, 172 insertions(+), 131 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index b42703171..ac90bc70d 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -344,7 +344,6 @@ def __init__(self, params): self.have = {} self.want = [] self.query = [] - self._implemented_states = set() # populated in self._merge_global_and_switch_configs() self.switch_configs = [] @@ -352,81 +351,14 @@ def _init_properties(self): self._properties = {} self._properties["ansible_module"] = None - def get_have(self): - """ - Caller: main() - - Build self.have, a dict containing the current mode of all switches. - - Have is a dict, keyed on switch_ip, where each element is a dict - with the following structure: - - ```json - { - "192.169.1.2": { - fabric_name: "MyFabric", - maintenance_mode: "Maintenance", - serial_number: "FCI1234567" - }, - "192.169.1.3": { - fabric_name: "YourFabric", - maintenance_mode: "Normal", - serial_number: "FCH2345678" - } - } - ``` - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.switch_details.rest_send = RestSend(self.ansible_module) - self.switch_details.refresh() - self.have = {} - # self.config has already been validated - for switch in self.config.get("switches"): - ip_address = switch.get("ip_address") - self.switch_details.filter = ip_address - try: - serial_number = self.switch_details.serial_number - except ValueError as error: - self.ansible_module.fail_json(f"{error}", **self.results.failed_result) - if serial_number is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"Switch with ip_address {ip_address} " - msg += "does not exist on the controller." - self.ansible_module.fail_json(msg, **self.results.failed_result) - mode = self.switch_details.maintenance_mode - if mode == "inconsistent": - msg = f"{self.class_name}.{method_name}: " - msg += "Switch maintenance mode state differs from the " - msg += "controller's maintenance mode state for switch " - msg += f"with ip_address {ip_address}. This is typically " - msg += "resolved by initiating a switch Deploy Config on " - msg += "the controller." - self.ansible_module.fail_json(msg, **self.results.failed_result) - if mode == "migration": - msg = f"{self.class_name}.{method_name}: " - msg += "Switch maintenance mode is in migration state for the " - msg += f"switch with ip_address {ip_address}. " - msg += "This indicates that the switch configuration is not " - msg += "compatible with the switch role in the hosting " - msg += "fabric. The issue might be resolved by initiating a " - msg += "fabric Recalculate & Deploy on the controller. " - msg += "Failing that, the switch configuration might need to be " - msg += "manually modified to match the switch role in the " - msg += "hosting fabric." - self.ansible_module.fail_json(msg, **self.results.failed_result) - fabric_name = self.switch_details.fabric_name - self.have[ip_address] = {} - self.have[ip_address].update({"maintenance_mode": mode}) - self.have[ip_address].update({"serial_number": serial_number}) - self.have[ip_address].update({"fabric_name": fabric_name}) - def get_want(self) -> None: """ - Caller: main() + ### Summary + Build self.want, a list of validated playbook configurations. - 1. Merge the global config into each switch config - 2. Validate the merged configs - 3. Populate self.want with the validated configs + 1. Merge the playbook global config into each switch config. + 2. Validate the merged configs from step 1 against the param spec. + 3. Populate self.want with the validated configs. ### self.want structure @@ -439,8 +371,8 @@ def get_want(self) -> None: }, { "ip_address": "192.168.1.3", - "mode": "maintenance", - "deploy": false + "mode": "normal", + "deploy": true } ] ``` @@ -480,14 +412,15 @@ def get_want(self) -> None: def _merge_global_and_switch_configs(self, config) -> None: """ - Merge the global config with each switch config and - populate list of merged configs self.switch_configs. - - Merge rules: - 1. switch_config takes precedence over global_config. - 2. If switch_config is missing a parameter, use parameter - from global_config. - 3. If switch_config has a parameter, use it. + ### Summary + Merge the global playbook config with each switch config and + populate a list of merged configs (``self.switch_configs``). + + ### Merge rules + - switch_config takes precedence over global_config. + - If switch_config is missing a parameter, use parameter + from global_config. + - If switch_config has a parameter, use it. """ method_name = inspect.stack()[0][3] @@ -561,7 +494,87 @@ def __init__(self, params): self.need = [] - self._implemented_states.add("merged") + def get_have(self): + """ + ### Summary + Build self.have, a dict containing the current mode of all switches. + + Have is a dict, keyed on switch_ip, where each element is a dict + with the following structure: + - ``fabric_name``: The name of the switch's hosting fabric. + - ``mode``: The current maintenance mode of the switch. + - ``role``: The role of the switch in the hosting fabric. + - ``serial_number``: The serial number of the switch. + + ```json + { + "192.169.1.2": { + fabric_name: "MyFabric", + mode: "maintenance", + role: "spine", + serial_number: "FCI1234567" + }, + "192.169.1.3": { + fabric_name: "YourFabric", + mode: "normal", + role: "leaf", + serial_number: "FCH2345678" + } + } + ``` + ### NOTES + - We are not currently using ``role``. We added it to improve + error messages, but will need to pass this to MaintenanceMode() + in order to do so. This will require adding a ``role`` property + to MaintenanceMode(). But ``role`` is not strictly needed for the + MaintenanceMode() class. Hence, we're not adding this now. Maybe + in a future release. + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + self.switch_details.rest_send = RestSend(self.ansible_module) + self.switch_details.refresh() + self.have = {} + # self.config has already been validated + for switch in self.config.get("switches"): + ip_address = switch.get("ip_address") + self.switch_details.filter = ip_address + try: + serial_number = self.switch_details.serial_number + except ValueError as error: + self.ansible_module.fail_json(f"{error}", **self.results.failed_result) + if serial_number is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"Switch with ip_address {ip_address} " + msg += "does not exist on the controller." + self.ansible_module.fail_json(msg, **self.results.failed_result) + mode = self.switch_details.maintenance_mode + if mode == "inconsistent": + msg = f"{self.class_name}.{method_name}: " + msg += "Switch maintenance mode state differs from the " + msg += "controller's maintenance mode state for switch " + msg += f"with ip_address {ip_address}. This is typically " + msg += "resolved by initiating a switch Deploy Config on " + msg += "the controller." + self.ansible_module.fail_json(msg, **self.results.failed_result) + if mode == "migration": + msg = f"{self.class_name}.{method_name}: " + msg += "Switch maintenance mode is in migration state for the " + msg += f"switch with ip_address {ip_address}. " + msg += "This indicates that the switch configuration is not " + msg += "compatible with the switch role in the hosting " + msg += "fabric. The issue might be resolved by initiating a " + msg += "fabric Recalculate & Deploy on the controller. " + msg += "Failing that, the switch configuration might need to be " + msg += "manually modified to match the switch role in the " + msg += "hosting fabric." + self.ansible_module.fail_json(msg, **self.results.failed_result) + fabric_name = self.switch_details.fabric_name + role = self.switch_details.role + self.have[ip_address] = {} + self.have[ip_address].update({"fabric_name": fabric_name}) + self.have[ip_address].update({"mode": mode}) + self.have[ip_address].update({"role": role}) + self.have[ip_address].update({"serial_number": serial_number}) def get_need(self): """ @@ -576,30 +589,30 @@ def get_need(self): "172.22.150.2": { "deploy": false "fabric_name": "MyFabric", - "maintenance_mode": "maintenance", + "mode": "maintenance", "serial_number": "FCI1234567" }, "172.22.150.3": { "deploy": true "fabric_name": "YourFabric", - "maintenance_mode": "normal", + "mode": "normal", "serial_number": "HMD2345678" } } """ self.need = {} for want in self.want: - want_ip = want.get("ip_address", None) - if want_ip not in self.have: + key = want.get("ip_address", None) + if key not in self.have: continue - serial_number = self.have[want_ip]["serial_number"] - fabric_name = self.have[want_ip]["fabric_name"] - if want.get("mode") != self.have[want_ip]["maintenance_mode"]: - self.need[want_ip] = want - self.need[want_ip].update({"deploy": want.get("deploy")}) - self.need[want_ip].update({"fabric_name": fabric_name}) - self.need[want_ip].update({"serial_number": serial_number}) - self.need[want_ip].update({"maintenance_mode": want.get("mode")}) + serial_number = self.have[key]["serial_number"] + fabric_name = self.have[key]["fabric_name"] + if want.get("mode") != self.have[key]["mode"]: + self.need[key] = want + self.need[key].update({"deploy": want.get("deploy")}) + self.need[key].update({"fabric_name": fabric_name}) + self.need[key].update({"serial_number": serial_number}) + self.need[key].update({"mode": want.get("mode")}) def commit(self): """ @@ -637,7 +650,7 @@ def send_need(self) -> None: instance.rest_send = RestSend(self.ansible_module) instance.results = self.results for ip_address, switch in self.need.items(): - mode = switch.get("maintenance_mode", None) + mode = switch.get("mode", None) serial_number = switch.get("serial_number", None) fabric_name = switch.get("fabric_name", None) deploy = switch.get("deploy", False) @@ -661,7 +674,6 @@ class Query(Common): def __init__(self, ansible_module): self.class_name = self.__class__.__name__ super().__init__(ansible_module) - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -670,41 +682,81 @@ def __init__(self, ansible_module): msg += f"check_mode: {self.check_mode}" self.log.debug(msg) - self._implemented_states.add("query") - - def commit(self) -> None: - """ - 1. query the switches in self.want that exist on the controller + def get_have(self): """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + ### Summary + Build self.have, a dict containing the current mode of all switches. - self.get_want() + Have is a dict, keyed on switch_ip, where each element is a dict + with the following structure: + - ``fabric_name``: The name of the switch's hosting fabric. + - ``mode``: The current maintenance mode of the switch. + - ``role``: The role of the switch in the hosting fabric. + - ``serial_number``: The serial number of the switch. + ```json + { + "192.169.1.2": { + fabric_name: "MyFabric", + mode: "maintenance", + role: "spine", + serial_number: "FCI1234567" + }, + "192.169.1.3": { + fabric_name: "YourFabric", + mode: "normal", + role: "leaf", + serial_number: "FCH2345678" + } + } + ``` + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.switch_details.rest_send = RestSend(self.ansible_module) self.switch_details.refresh() + self.have = {} # self.config has already been validated - for item in self.want: - ip_address = item.get("ip_address") + for switch in self.config.get("switches"): + ip_address = switch.get("ip_address") self.switch_details.filter = ip_address - serial_number = self.switch_details.serial_number + try: + serial_number = self.switch_details.serial_number + except ValueError as error: + self.ansible_module.fail_json(f"{error}", **self.results.failed_result) if serial_number is None: msg = f"{self.class_name}.{method_name}: " msg += f"Switch with ip_address {ip_address} " msg += "does not exist on the controller." self.ansible_module.fail_json(msg, **self.results.failed_result) - mode = self.switch_details.mode + + mode = self.switch_details.maintenance_mode + role = self.switch_details.switch_role fabric_name = self.switch_details.fabric_name - self.results.diff_current = { - "fabric_name": fabric_name, - "ip_address": ip_address, - "mode": mode, - "serial_number": serial_number, - } - self.results.changed = False - self.results.action = "query" - self.results.failed = False - self.results.result_current = {"changed": False, "success": True} - self.results.register_task_result() + self.have[ip_address] = {} + self.have[ip_address].update({"mode": mode}) + if role is not None: + self.have[ip_address].update({"role": role}) + else: + self.have[ip_address].update({"role": "na"}) + self.have[ip_address].update({"serial_number": serial_number}) + self.have[ip_address].update({"fabric_name": fabric_name}) + + def commit(self) -> None: + """ + ### Summary + Query the switches in self.want that exist on the controller + and update ``self.results`` with the query results. + """ + self.get_want() + self.get_have() + + # If we got this far, the request was successful. + self.results.diff_current = self.have + self.results.changed = False + self.results.action = "query" + self.results.failed = False + self.results.result_current = {"changed": False, "success": True} + self.results.register_task_result() def main(): @@ -715,17 +767,6 @@ def main(): "required": True, "type": "dict", } - # argument_spec["deploy"] = { - # "default": True, - # "required": False, - # "type": "bool", - # } - # argument_spec["mode"] = { - # "choices": ["Maintenance", "Normal"], - # "default": "Maintenance", - # "required": False, - # "type": "str" - # } argument_spec["state"] = { "choices": ["merged", "query"], "default": "merged", From b34e006a6ec5b176efef199ffdf8f3613bf8be3e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 23 May 2024 20:00:23 -1000 Subject: [PATCH 075/230] Bulk per-fabric config-deploy 1. MaintenanceMode(): modify to accept a list of config dicts and process all switches simultaneously, as opposed to one at a time. This was needed because it takes way too long to config-deploy each switch individually. 2. Fabrics().EpFabricsConfigDeploy(): Modify to access a list for switch_id. 3. Merged().send_need(): Modify to align with the rewritten MaintenanceMode() class. --- .../rest/control/fabrics/fabrics.py | 24 +- .../module_utils/common/maintenance_mode.py | 575 +++++++++++------- plugins/modules/dcnm_maintenance_mode.py | 68 ++- 3 files changed, 399 insertions(+), 268 deletions(-) diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py index 29d4af547..23a6b714a 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py @@ -221,12 +221,12 @@ class EpFabricConfigDeploy(Fabrics): - set the ``fabric_name`` to be used in the path - string - required - - force_show_run: boolean + - force_show_run: - set the ``forceShowRun`` value - boolean - default: False - optional - - include_all_msd_switches: boolean + - include_all_msd_switches: - set the ``inclAllMSDSwitches`` value - boolean - default: False @@ -234,9 +234,9 @@ class EpFabricConfigDeploy(Fabrics): - path: - retrieve the path for the endpoint - string - - switch_id: string + - switch_id: - set the ``switch_id`` to be used in the path - - string + - string or list - optional - if set, ``include_all_msd_switches`` is not added to the path - verb: @@ -247,7 +247,9 @@ class EpFabricConfigDeploy(Fabrics): ```python instance = EpFabricConfigDeploy() instance.fabric_name = "MyFabric" - instance.switch_id = "CHM1234567" + instance.switch_id = ["CHM1234567", "CHM7654321"] + # or instance.switch_id = "CHM1234567" + # or instance.switch_id = "CHM7654321,CHM1234567" path = instance.path verb = instance.verb ``` @@ -337,21 +339,27 @@ def switch_id(self): """ - getter: Return the switch_id value. - setter: Set the switch_id value. - - setter: Raise ``ValueError`` if switch_id is not a string. + - setter: Raise ``ValueError`` if switch_id is not a string or list. - Default: None - Optional - Notes: - ``include_all_msd_switches`` is removed from the path if ``switch_id`` is set. + - If value is a list, it is converted to a comma-separated + string. """ return self.properties["switch_id"] @switch_id.setter def switch_id(self, value): method_name = inspect.stack()[0][3] - if not isinstance(value, str): + if isinstance(value, str): + pass + elif isinstance(value, list): + value = ",".join(value) + else: msg = f"{self.class_name}.{method_name}: " - msg += f"Expected string for {method_name}. " + msg += f"Expected string or list for {method_name}. " msg += f"Got {value} with type {type(value).__name__}." raise ValueError(msg) self.properties["switch_id"] = value diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index e49d4d86c..20365e062 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -30,23 +30,56 @@ class MaintenanceMode: """ - # Modify the maintenance mode state of a switch. + ### Modify the maintenance mode state of switches. - Raise ``ValueError`` for any caller errors, e.g. required properties - not being set before calling FabricConfigDeploy().commit(). + not being set before calling MaintenanceMode().commit(). - Update MaintenanceMode().results to reflect success/failure of the operation on the controller. + - For switches that are to be deployed, initiate a per-fabric + bulk config-deploy. + + ### Example value for ``config`` in the Usage section below: + ```json + [ + { + "deploy": false, + "fabric_name": "MyFabric", + "ip_address": "192.168.1.2", + "mode": "maintenance", + "serial_number": "FCI1234567" + }, + { + "deploy": true, + "fabric_name": "YourFabric", + "ip_address": "192.168.1.3", + "mode": "normal", + "serial_number": "HMD2345678" + } + ] + ``` + + ### Usage + - Where ``params`` is ``AnsibleModule.params`` + - Where ``config`` is a list of dicts, each containing the following: + - ``deploy``: ``bool``. If True, the switch maintenance mode + will be deployed. + - ``fabric_name``: ``str``. The name of the switch's hosting fabric. + - ``ip_address``: ``str``. The ip address of the switch. + - ``mode``: ``str``. The intended maintenance mode. Must be one of + "maintenance" or "normal". + - ``serial_number``: ``str``. The serial number of the switch. + - ## Usage (where params is AnsibleModule.params) ```python instance = MaintenanceMode(params) - instance.fabric_name = "MyFabric" - instance.mode = "maintenance" # or "normal" - instance.ip_address = "192.168.1.2" + try: + instance.config = config + except ValueError as error: + raise ValueError(error) from error instance.rest_send = RestSend(ansible_module) instance.results = Results() - instance.serial_number = "FDO1234567" try: instance.commit() except ValueError as error: @@ -77,7 +110,12 @@ def __init__(self, params): msg += "params is missing mandatory state parameter." raise ValueError(msg) + # Populated in build_deploy_dict() + self.deploy_dict = {} + # Populated in build_endpoints_list() + self.endpoints = [] self.action_result: Dict[str, bool] = {} + self.serial_number_to_ip_address = {} self.valid_modes = ["maintenance", "normal"] self.path = None @@ -95,13 +133,9 @@ def __init__(self, params): def _init_properties(self): self._properties = {} - self._properties["deploy"] = None - self._properties["fabric_name"] = None - self._properties["ip_address"] = None - self._properties["mode"] = None + self._properties["config"] = None self._properties["rest_send"] = None self._properties["results"] = None - self._properties["serial_number"] = None # def _can_fabric_be_deployed(self) -> None: # """ @@ -188,31 +222,193 @@ def _init_properties(self): # self.fabric_can_be_deployed = True - def verify_commit_parameters(self): + def verify_config_parameters(self, value): + """ + Verify that required parameters are present in config. + + ### Raises + - ``ValueError`` if ``config`` is not a list. + - ``ValueError`` if ``config`` contains invalid content. + + ### NOTES + 1. See the following validation methods for details: + - verify_deploy() + - verify_fabric_name() + - verify_ip_address() + - verify_mode() + - verify_serial_number() + """ + method_name = inspect.stack()[0][3] + if not isinstance(value, list): + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.config must be a list. " + msg += f"Got type: {type(value).__name__}." + raise ValueError(msg) + + for item in value: + try: + self.verify_deploy(item) + self.verify_fabric_name(item) + self.verify_ip_address(item) + self.verify_mode(item) + self.verify_serial_number(item) + except ValueError as error: + raise ValueError(error) from error + + def verify_deploy(self, item): """ - Verify that required parameters are set before calling commit. - - - Raise ``ValueError`` if ``fabric_name`` is not set. - - Raise ``ValueError`` if ``ip_address`` is not set. - - Raise ``ValueError`` if ``mode`` is not set. - - Raise ``ValueError`` if ``rest_send`` is not set. - - Raise ``ValueError`` if ``results`` is not set. - - Raise ``ValueError`` if ``serial_number`` is not set. + - Raise ``ValueError`` if ``deploy`` is not present. + - Raise ``ValueError`` if ``deploy`` is not a boolean. """ method_name = inspect.stack()[0][3] - if self.fabric_name is None: + if item.get("deploy", None) is None: msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.fabric_name must be set " - msg += "before calling commit." + msg += "deploy must be present in config." + raise ValueError(msg) + if not isinstance(item.get("deploy", None), bool): + msg = f"{self.class_name}.{method_name}: " + msg += "deploy must be a boolean." + raise ValueError(msg) + + def verify_fabric_name(self, item): + """ + - Raise ``ValueError`` if ``fabric_name`` is not present. + - Raise ``ValueError`` if ``fabric_name`` is not a valid fabric name. + """ + method_name = inspect.stack()[0][3] + if item.get("fabric_name", None) is None: + msg = f"{self.class_name}.{method_name}: " + msg += "fabric_name must be present in config." + raise ValueError(msg) + try: + self.conversion.validate_fabric_name(item.get("fabric_name", None)) + except (TypeError, ValueError) as error: + raise ValueError(error) from error + + def verify_ip_address(self, item): + """ + - Raise ``ValueError`` if ``ip_address`` is not present. + """ + method_name = inspect.stack()[0][3] + if item.get("ip_address", None) is None: + msg = f"{self.class_name}.{method_name}: " + msg += "ip_address must be present in config." + raise ValueError(msg) + + def verify_mode(self, item): + """ + ### Summary + Validate the ``mode`` parameter. + + ### Raises + - ``ValueError`` if ``mode`` is not present. + - ``ValueError`` if ``mode`` is not one of "maintenance" or "normal". + """ + method_name = inspect.stack()[0][3] + if item.get("mode", None) is None: + msg = f"{self.class_name}.{method_name}: " + msg += "mode must be present in config." + raise ValueError(msg) + if item.get("mode", None) not in self.valid_modes: + msg = f"{self.class_name}.{method_name}: " + msg += "mode must be one of 'maintenance' or 'normal'." + raise ValueError(msg) + + def verify_serial_number(self, item): + """ + ### Summary + Validate the ``serial_number`` parameter. + + ### Raises + - ``ValueError`` if ``serial_number`` is not present. + """ + method_name = inspect.stack()[0][3] + if item.get("serial_number", None) is None: + msg = f"{self.class_name}.{method_name}: " + msg += "serial_number must be present in config." raise ValueError(msg) - if self.ip_address is None: + + def build_deploy_dict(self): + """ + ### Summary + - Build the deploy_dict, keyed on fabric_name, with a list of + serial_numbers to deploy for each fabric. + """ + self.deploy_dict = {} + for item in self.config: + fabric_name = item.get("fabric_name") + serial_number = item.get("serial_number") + deploy = item.get("deploy") + if fabric_name not in self.deploy_dict: + self.deploy_dict[fabric_name] = [] + if deploy is True: + self.deploy_dict[fabric_name].append(serial_number) + + def build_endpoints_list(self): + """ + ### Summary + - Build the maintenance_mode endpoints to send to the controller. + This is a list of tuples, each containing the path, verb, and + comma-separated list of ip addresses. + i.e. [(path, verb, ip_addresses), (path, verb, ip_addresses), ...] + - Also populate self.serial_number_to_ip_address dict, keyed on + serial_number, and value of ip_address associated with + serial_number. This is used later in the commit() method. + + ### Raises + - ``ValueError`` if ``config`` is not set. + """ + method_name = inspect.stack()[0][3] + if self.config is None: msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.ip_address must be set " + msg += f"{self.class_name}.config must be set " msg += "before calling commit." raise ValueError(msg) - if self.mode is None: + + self.serial_number_to_ip_address = {} + # Populate dict to sort serial_numbers by fabric and mode + # This drives endpoint creation further below. + mode_dict = {} + for item in self.config: + fabric_name = item.get("fabric_name") + serial_number = item.get("serial_number") + mode = item.get("mode") + ip_address = item.get("ip_address") + self.serial_number_to_ip_address[serial_number] = ip_address + if fabric_name not in mode_dict: + mode_dict[fabric_name] = {} + if mode not in mode_dict[fabric_name]: + mode_dict[fabric_name][mode] = [] + mode_dict[fabric_name][mode].append(serial_number) + + # populate endpoints using mode_dict + self.endpoints = [] + for fabric, data in mode_dict.items(): + for mode, serial_numbers in data.items(): + for serial_number in serial_numbers: + ip_address = self.serial_number_to_ip_address[serial_number] + if mode == "normal": + instance = self.ep_maintenance_mode_disable + else: + instance = self.ep_maintenance_mode_enable + instance.fabric_name = fabric + instance.serial_number = serial_number + endpoint = (instance.path, instance.verb, ip_address, mode) + self.endpoints.append(copy.copy(endpoint)) + + def verify_commit_parameters(self): + """ + ### Summary + Verify that required parameters are present before calling commit. + + ### Raises + - ``ValueError`` if ``rest_send`` is not set. + - ``ValueError`` if ``results`` is not set. + """ + method_name = inspect.stack()[0][3] + if self.config is None: msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.mode must be set " + msg += f"{self.class_name}.config must be set " msg += "before calling commit." raise ValueError(msg) if self.rest_send is None: @@ -225,42 +421,96 @@ def verify_commit_parameters(self): msg += f"{self.class_name}.results must be set " msg += "before calling commit." raise ValueError(msg) - if self.serial_number is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.serial_number must be set " - msg += "before calling commit." - raise ValueError(msg) def commit(self): """ - - Initiate a config-deploy operation on the controller. - - Re-raise ``ValueError`` if ``fabric_name`` is not set. - - Re-raise ``ValueError`` if ``ip_address`` is not set. - - Re-raise ``ValueError`` if ``mode`` is not set. - - Re-raise ``ValueError`` if ``rest_send`` is not set. - - Re-raise ``ValueError`` if ``results`` is not set. - - Re-raise ``ValueError`` if ``serial_number`` is not set. - """ - method_name = inspect.stack()[0][3] + ### Summary + Initiates the maintenance mode change on the controller. + ### Raises + - ``ValueError`` if ``rest_send`` is not set. + - ``ValueError`` if ``results`` is not set. + """ try: self.verify_commit_parameters() except ValueError as error: raise ValueError(error) from error - # self._can_fabric_be_deployed() - # /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{fabricName}/switches/{serialNumber}/deploy-maintenance-mode - msg = f"{self.class_name}.{method_name}: " - msg += f"action_failed: {self.action_failed}" - msg += f"deploy: {self.deploy}, " - msg += f"fabric_name: {self.fabric_name}, " - msg += f"mode: {self.mode}, " - msg += f"ip_address: {self.ip_address}, " - # msg += f"fabric_can_be_deployed: {self.fabric_can_be_deployed}, " - # msg += f"cannot_perform_action_reason: {self.cannot_perform_action_reason}" - msg += f"serial_number: {self.serial_number}, " - self.log.debug(msg) + self.change_system_mode() + self.deploy_switches() + def change_system_mode(self): + """ + Change the ``systemMode`` configuration for the switch. + + ### Raises + - ``ValueError`` if endpoint resolution fails. + """ + self.build_endpoints_list() + for endpoint in self.endpoints: + self.rest_send.path = endpoint[0] + self.rest_send.verb = endpoint[1] + self.rest_send.payload = None + self.rest_send.commit() + + action = "maintenance_mode" + result = self.rest_send.result_current["success"] + if result is False: + self.results.diff_current = {} + else: + self.results.diff_current = { + "ip_address": endpoint[2], + f"{action}": endpoint[3], + } + + self.results.action = self.action + self.results.check_mode = self.check_mode + self.results.state = self.state + self.results.response_current = copy.deepcopy( + self.rest_send.response_current + ) + self.results.result_current = copy.deepcopy(self.rest_send.result_current) + self.results.register_task_result() + + def deploy_switches(self): + """ + Initiate config-deploy for the switches in ``self.deploy_dict``. + """ + self.build_deploy_dict() + ep_deploy = EpFabricConfigDeploy() + for fabric, serial_numbers in self.deploy_dict.items(): + # Start the config-deploy + ep_deploy.fabric_name = fabric + ep_deploy.switch_id = serial_numbers + self.rest_send.path = ep_deploy.path + self.rest_send.verb = ep_deploy.verb + self.rest_send.payload = None + self.rest_send.commit() + + # Register the result + action = "config_deploy" + result = self.rest_send.result_current["success"] + if result is False: + self.results.diff_current = {} + else: + diff = {} + diff.update({f"{action}": result}) + for serial_number in serial_numbers: + ip_address = self.serial_number_to_ip_address[serial_number] + diff.update({ip_address: serial_number}) + self.results.diff_current = diff + + self.results.action = action + self.results.check_mode = self.check_mode + self.results.state = self.state + self.results.response_current = copy.deepcopy( + self.rest_send.response_current + ) + self.results.result_current = copy.deepcopy(self.rest_send.result_current) + self.results.register_task_result() + + # Use this if we cannot update maintenance mode in frozen fabrics + # self._can_fabric_be_deployed() # if self.fabric_can_be_deployed is False: # self.results.diff_current = {} # self.results.action = self.action @@ -277,166 +527,56 @@ def commit(self): # self.results.register_task_result() # return - self.change_system_mode() - - if self.deploy is True: - self.deploy_switch() - - def change_system_mode(self): - """ - Change the ``systemMode`` configuration for the switch. - - ### Raises - - ``ValueError`` if endpoint resolution fails. - """ - if self.mode == "maintenance": - endpoint = self.ep_maintenance_mode_enable - else: - endpoint = self.ep_maintenance_mode_disable - - try: - endpoint.fabric_name = self.fabric_name - endpoint.serial_number = self.serial_number - self.path = endpoint.path - self.verb = endpoint.verb - except ValueError as error: - self.results.diff_current = {} - self.results.result_current = self.results.failed_result - self.results.register_task_result() - raise ValueError(error) from error - - self.rest_send.path = self.path - self.rest_send.verb = self.verb - self.rest_send.payload = None - self.rest_send.commit() - - action = "maintenance_mode" - result = self.rest_send.result_current["success"] - if result is False: - self.results.diff_current = {} - else: - self.results.diff_current = { - "ip_address": self.ip_address, - f"{action}": self.mode, - } - - self.results.action = self.action - self.results.check_mode = self.check_mode - self.results.state = self.state - self.results.response_current = copy.deepcopy(self.rest_send.response_current) - self.results.result_current = copy.deepcopy(self.rest_send.result_current) - self.results.register_task_result() - - def deploy_switch(self): - """ - Initiate a switch config-deploy. - """ - # Start the config-deploy - ep_deploy = EpFabricConfigDeploy() - ep_deploy.fabric_name = self.fabric_name - ep_deploy.switch_id = self.serial_number - self.rest_send.path = ep_deploy.path - self.rest_send.verb = ep_deploy.verb - self.rest_send.payload = None - self.rest_send.commit() - - # Register the result - action = "config_deploy" - result = self.rest_send.result_current["success"] - if result is False: - self.results.diff_current = {} - else: - self.results.diff_current = { - "ip_address": self.ip_address, - f"{action}": result, - } - - self.results.action = action - self.results.check_mode = self.check_mode - self.results.state = self.state - self.results.response_current = copy.deepcopy(self.rest_send.response_current) - self.results.result_current = copy.deepcopy(self.rest_send.result_current) - self.results.register_task_result() - @property - def deploy(self): + def config(self): """ - Whether to issue a recalculate and deploy on the switch - after changing the mode. - - - getter: Return the deploy value. - - setter: Set the deploy value. - - setter: Raise ``ValueError`` if the value is not a boolean. - """ - return self._properties["deploy"] - - @deploy.setter - def deploy(self, value): - if not isinstance(value, bool): - msg = f"{self.class_name}.deploy must be a boolean. " - msg += f"Got type: {type(value).__name__}." - raise ValueError(msg) - self._properties["deploy"] = value - - @property - def fabric_name(self): - """ - The name of the fabric to which the switch belongs. - - - getter: Return the fabric_name. - - setter: Set the fabric_name. - - setter: Raise ``ValueError`` if the value is not a valid - fabric name. + ### Summary + The maintenance mode configurations to be sent to the controller. + + - getter: Return the config value. + - setter: Set the config value. + - setter: Raise ``ValueError`` if value is not a list. + - setter: Raise ``ValueError`` if value contains invalid content. + + ### Value structure + value is a ``list`` of ``dict``. Each dict must contain the following: + - ``deploy``: ``bool``. If True, the switch maintenance mode + will be deployed. + - ``fabric_name``: ``str``. The name of the switch's hosting fabric. + - ``ip_address``: ``str``. The ip address of the switch. + - ``mode``: ``str``. The intended maintenance mode. Must be one of + "maintenance" or "normal". + - ``serial_number``: ``str``. The serial number of the switch. + + ### Example + ```json + [ + { + "deploy": false, + "fabric_name": "MyFabric", + "ip_address": "172.22.150.2", + "mode": "maintenance", + "serial_number": "FCI1234567" + }, + { + "deploy": true, + "fabric_name": "YourFabric", + "ip_address": "172.22.150.3", + "mode": "normal", + "serial_number": "HMD2345678" + } + ] + ``` """ - return self._properties["fabric_name"] + return self._properties["config"] - @fabric_name.setter - def fabric_name(self, value): + @config.setter + def config(self, value): try: - self.conversion.validate_fabric_name(value) - except (TypeError, ValueError) as error: + self.verify_config_parameters(value) + except ValueError as error: raise ValueError(error) from error - self._properties["fabric_name"] = value - - @property - def ip_address(self): - """ - - The ip_address of the switch. Used only for more informative - error messages. - - Raise ``ValueError`` if the value is not a string. - """ - return self._properties["ip_address"] - - @ip_address.setter - def ip_address(self, value): - method_name = inspect.stack()[0][3] - - if not isinstance(value, str): - msg = f"{self.class_name}.{method_name} must be a string. " - msg += f"Got type: {type(value).__name__}." - self.log.debug(msg) - raise ValueError(msg) - self._properties["ip_address"] = value - - @property - def mode(self): - """ - The indended maintenance mode. - - getter: Return the mode. - - setter: Set the mode. - - setter: Raise ``ValueError`` if the value is not one of - "maintenance" or "normal". - """ - return self._properties["mode"] - - @mode.setter - def mode(self, value): - if value not in self.valid_modes: - msg = f"{self.class_name}.mode is invalid. " - msg += f"Got value {value}. " - msg += f"Expected one of {','.join(self.valid_modes)}." - raise ValueError(msg) - self._properties["mode"] = value + self._properties["config"] = value @property def rest_send(self): @@ -491,22 +631,3 @@ def results(self, value): self.log.debug(msg) raise TypeError(msg) self._properties["results"] = value - - @property - def serial_number(self): - """ - - The serial_number of the switch. - - Raise ``ValueError`` if the value is not a string. - """ - return self._properties["serial_number"] - - @serial_number.setter - def serial_number(self, value): - method_name = inspect.stack()[0][3] - - if not isinstance(value, str): - msg = f"{self.class_name}.{method_name} must be a string. " - msg += f"Got type: {type(value).__name__}." - self.log.debug(msg) - raise ValueError(msg) - self._properties["serial_number"] = value diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index ac90bc70d..a10679789 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -578,6 +578,7 @@ def get_have(self): def get_need(self): """ + ### Summary Build self.need for merged state. ### Caller @@ -585,34 +586,38 @@ def get_need(self): ### self.need structure ```json - { - "172.22.150.2": { - "deploy": false + [ + { + "deploy": false, "fabric_name": "MyFabric", + "ip_address": "172.22.150.2", "mode": "maintenance", "serial_number": "FCI1234567" }, - "172.22.150.3": { - "deploy": true + { + "deploy": true, "fabric_name": "YourFabric", + "ip_address": "172.22.150.3", "mode": "normal", "serial_number": "HMD2345678" } - } + ] """ - self.need = {} + self.need = [] for want in self.want: - key = want.get("ip_address", None) - if key not in self.have: + ip_address = want.get("ip_address", None) + if ip_address not in self.have: continue - serial_number = self.have[key]["serial_number"] - fabric_name = self.have[key]["fabric_name"] - if want.get("mode") != self.have[key]["mode"]: - self.need[key] = want - self.need[key].update({"deploy": want.get("deploy")}) - self.need[key].update({"fabric_name": fabric_name}) - self.need[key].update({"serial_number": serial_number}) - self.need[key].update({"mode": want.get("mode")}) + serial_number = self.have[ip_address]["serial_number"] + fabric_name = self.have[ip_address]["fabric_name"] + if want.get("mode") != self.have[ip_address]["mode"]: + need = want + need.update({"deploy": want.get("deploy")}) + need.update({"fabric_name": fabric_name}) + need.update({"ip_address": ip_address}) + need.update({"mode": want.get("mode")}) + need.update({"serial_number": serial_number}) + self.need.append(copy.copy(need)) def commit(self): """ @@ -627,7 +632,11 @@ def commit(self): self.get_want() self.get_have() self.get_need() - self.send_need() + try: + self.send_need() + except ValueError as error: + self.results.build_final_result() + self.ansible_module.fail_json(f"{error}", **self.results.final_result) def send_need(self) -> None: """ @@ -649,21 +658,14 @@ def send_need(self) -> None: instance = MaintenanceMode(self.params) instance.rest_send = RestSend(self.ansible_module) instance.results = self.results - for ip_address, switch in self.need.items(): - mode = switch.get("mode", None) - serial_number = switch.get("serial_number", None) - fabric_name = switch.get("fabric_name", None) - deploy = switch.get("deploy", False) - try: - instance.deploy = deploy - instance.fabric_name = fabric_name - instance.ip_address = ip_address - instance.mode = mode - instance.serial_number = serial_number - instance.commit() - except ValueError as error: - self.results.build_final_result() - self.ansible_module.fail_json(f"{error}", **self.results.final_result) + try: + instance.config = self.need + except ValueError as error: + raise ValueError(error) from error + try: + instance.commit() + except ValueError as error: + raise ValueError(error) from error class Query(Common): From cb7fbdfbd132b1a494db00e427b037d28ed70a1d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 24 May 2024 09:34:15 -1000 Subject: [PATCH 076/230] MaintenanceMode().change_system_mode() simplify, more... 1. MaintenanceMode().change_system_mode(): We originally thought that the maintenance-mode endpoint supported bulk update via comma-separate list of serial_number (similar to config-deploy). It doesn't. However, changing system mode is a very fast operation and so initiating this per-switch is not time consuming. Reverted change_system_mode() back to its original (simpler) implementation; plus a few enhancements. 2. Updated class docstring to include detailed information about what exceptions are raised for each public-facing property and method. 3. Add ControllerResponseError exceptions to change_system_mode() and deploy_switches() with useful error messages. 4. Reorder build_deploy_dict() to be closer to the method that uses it; deploy_switches(). 5. Remove unused imports 6. Remove unused vars --- .../module_utils/common/maintenance_mode.py | 245 +++++++++++------- 1 file changed, 150 insertions(+), 95 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 20365e062..c12181a87 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -20,26 +20,43 @@ import copy import inspect import logging -from typing import Dict from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import ( EpFabricConfigDeploy, EpMaintenanceModeDisable, EpMaintenanceModeEnable) from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError class MaintenanceMode: """ - ### Modify the maintenance mode state of switches. + ### Summary + - Modify the maintenance mode state of switches. + - Optionally deploy the changes. - - Raise ``ValueError`` for any caller errors, e.g. required properties - not being set before calling MaintenanceMode().commit(). - - Update MaintenanceMode().results to reflect success/failure of + ### Raises + - ``ValueError`` in the following methods: + - __init__() if params is missing mandatory parameters + ``check_mode`` or ``state``. + + - ``ValueError`` in the following properties: + - ``config`` if config contains invalid content. + + - ``ControllerResponseError`` in the following methods: + - ``commit`` if controller response != 200. + + - ``TypeError`` in the following properties: + - ``rest_send`` if value is not an instance of RestSend. + - ``results`` if value is not an instance of Results. + + ### Details + - Updates MaintenanceMode().results to reflect success/failure of the operation on the controller. - - For switches that are to be deployed, initiate a per-fabric - bulk config-deploy. + - For switches that are to be deployed, initiates a per-fabric + bulk switch config-deploy. - ### Example value for ``config`` in the Usage section below: + ### Example value for ``config`` in the ``Usage`` section below: ```json [ { @@ -70,8 +87,6 @@ class MaintenanceMode: "maintenance" or "normal". - ``serial_number``: ``str``. The serial number of the switch. - - ```python instance = MaintenanceMode(params) try: @@ -112,9 +127,6 @@ def __init__(self, params): # Populated in build_deploy_dict() self.deploy_dict = {} - # Populated in build_endpoints_list() - self.endpoints = [] - self.action_result: Dict[str, bool] = {} self.serial_number_to_ip_address = {} self.valid_modes = ["maintenance", "normal"] @@ -328,74 +340,6 @@ def verify_serial_number(self, item): msg += "serial_number must be present in config." raise ValueError(msg) - def build_deploy_dict(self): - """ - ### Summary - - Build the deploy_dict, keyed on fabric_name, with a list of - serial_numbers to deploy for each fabric. - """ - self.deploy_dict = {} - for item in self.config: - fabric_name = item.get("fabric_name") - serial_number = item.get("serial_number") - deploy = item.get("deploy") - if fabric_name not in self.deploy_dict: - self.deploy_dict[fabric_name] = [] - if deploy is True: - self.deploy_dict[fabric_name].append(serial_number) - - def build_endpoints_list(self): - """ - ### Summary - - Build the maintenance_mode endpoints to send to the controller. - This is a list of tuples, each containing the path, verb, and - comma-separated list of ip addresses. - i.e. [(path, verb, ip_addresses), (path, verb, ip_addresses), ...] - - Also populate self.serial_number_to_ip_address dict, keyed on - serial_number, and value of ip_address associated with - serial_number. This is used later in the commit() method. - - ### Raises - - ``ValueError`` if ``config`` is not set. - """ - method_name = inspect.stack()[0][3] - if self.config is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.config must be set " - msg += "before calling commit." - raise ValueError(msg) - - self.serial_number_to_ip_address = {} - # Populate dict to sort serial_numbers by fabric and mode - # This drives endpoint creation further below. - mode_dict = {} - for item in self.config: - fabric_name = item.get("fabric_name") - serial_number = item.get("serial_number") - mode = item.get("mode") - ip_address = item.get("ip_address") - self.serial_number_to_ip_address[serial_number] = ip_address - if fabric_name not in mode_dict: - mode_dict[fabric_name] = {} - if mode not in mode_dict[fabric_name]: - mode_dict[fabric_name][mode] = [] - mode_dict[fabric_name][mode].append(serial_number) - - # populate endpoints using mode_dict - self.endpoints = [] - for fabric, data in mode_dict.items(): - for mode, serial_numbers in data.items(): - for serial_number in serial_numbers: - ip_address = self.serial_number_to_ip_address[serial_number] - if mode == "normal": - instance = self.ep_maintenance_mode_disable - else: - instance = self.ep_maintenance_mode_enable - instance.fabric_name = fabric - instance.serial_number = serial_number - endpoint = (instance.path, instance.verb, ip_address, mode) - self.endpoints.append(copy.copy(endpoint)) - def verify_commit_parameters(self): """ ### Summary @@ -428,41 +372,70 @@ def commit(self): Initiates the maintenance mode change on the controller. ### Raises + - ``ValueError`` if ``config`` is not set. - ``ValueError`` if ``rest_send`` is not set. - ``ValueError`` if ``results`` is not set. + - ``ControllerResponseError`` if controller response != 200. """ try: self.verify_commit_parameters() except ValueError as error: raise ValueError(error) from error - self.change_system_mode() - self.deploy_switches() + try: + self.change_system_mode() + except ControllerResponseError as error: + raise ControllerResponseError(error) from error + except ValueError as error: + raise ValueError(error) from error + + try: + self.deploy_switches() + except ControllerResponseError as error: + raise ValueError(error) from error def change_system_mode(self): """ - Change the ``systemMode`` configuration for the switch. + ### Summary + Send the maintenance mode change request to the controller. ### Raises - - ``ValueError`` if endpoint resolution fails. + - ``ControllerResponseError`` if controller response != 200. """ - self.build_endpoints_list() - for endpoint in self.endpoints: - self.rest_send.path = endpoint[0] - self.rest_send.verb = endpoint[1] + method_name = inspect.stack()[0][3] + + for item in self.config: + # Build endpoint + mode = item.get("mode") + fabric_name = item.get("fabric_name") + ip_address = item.get("ip_address") + serial_number = item.get("serial_number") + if mode == "normal": + instance = self.ep_maintenance_mode_disable + else: + instance = self.ep_maintenance_mode_enable + instance.fabric_name = fabric_name + instance.serial_number = serial_number + + # Send request + self.rest_send.path = instance.path + self.rest_send.verb = instance.verb self.rest_send.payload = None self.rest_send.commit() - action = "maintenance_mode" + # Update diff result = self.rest_send.result_current["success"] if result is False: self.results.diff_current = {} else: self.results.diff_current = { - "ip_address": endpoint[2], - f"{action}": endpoint[3], + "fabric_name": fabric_name, + "ip_address": ip_address, + "maintenance_mode": mode, + "serial_number": serial_number, } + # register result self.results.action = self.action self.results.check_mode = self.check_mode self.results.state = self.state @@ -472,16 +445,89 @@ def change_system_mode(self): self.results.result_current = copy.deepcopy(self.rest_send.result_current) self.results.register_task_result() + if self.results.response_current["RETURN_CODE"] != 200: + msg = f"{self.class_name}.{method_name}: " + msg += "Unable to change system mode on switch: " + msg += f"fabric_name {fabric_name}, " + msg += f"ip_address {ip_address}, " + msg += f"serial_number {serial_number}. " + msg += f"Got response {self.results.response_current}" + raise ControllerResponseError(msg) + + def build_deploy_dict(self): + """ + ### Summary + - Build the deploy_dict + + ### Raises + None + + ### Structure + - key: fabric_name + - value: list of serial_numbers to deploy for each fabric + + ### Example + ```json + { + "MyFabric": ["CDM4593459", "CDM4593460"], + "YourFabric": ["CDM4593461", "CDM4593462"] + } + """ + self.deploy_dict = {} + for item in self.config: + fabric_name = item.get("fabric_name") + serial_number = item.get("serial_number") + deploy = item.get("deploy") + if fabric_name not in self.deploy_dict: + self.deploy_dict[fabric_name] = [] + if deploy is True: + self.deploy_dict[fabric_name].append(serial_number) + + def build_serial_number_to_ip_address(self): + """ + ### Summary + Populate self.serial_number_to_ip_address dict. + + ### Raises + None + + ### Structure + - key: switch serial_number + - value: associated switch ip_address + + ```json + { "CDM4593459": "192.168.1.2" } + ``` + ### Raises + None + + ### Notes + - ip_address and serial_number are added to the diff in the + ``deploy_switches()`` method. + """ + for item in self.config: + serial_number = item.get("serial_number") + ip_address = item.get("ip_address") + self.serial_number_to_ip_address[serial_number] = ip_address + def deploy_switches(self): """ + ### Summary Initiate config-deploy for the switches in ``self.deploy_dict``. + + ### Raises + - ``ControllerResponseError`` if controller response != 200. """ + method_name = inspect.stack()[0][3] self.build_deploy_dict() + self.build_serial_number_to_ip_address() ep_deploy = EpFabricConfigDeploy() - for fabric, serial_numbers in self.deploy_dict.items(): - # Start the config-deploy - ep_deploy.fabric_name = fabric + for fabric_name, serial_numbers in self.deploy_dict.items(): + # Build endpoint + ep_deploy.fabric_name = fabric_name ep_deploy.switch_id = serial_numbers + + # Send request self.rest_send.path = ep_deploy.path self.rest_send.verb = ep_deploy.verb self.rest_send.payload = None @@ -509,6 +555,15 @@ def deploy_switches(self): self.results.result_current = copy.deepcopy(self.rest_send.result_current) self.results.register_task_result() + if self.results.response_current["RETURN_CODE"] != 200: + msg = f"{self.class_name}.{method_name}: " + msg += "Unable to deploy switches: " + msg += f"fabric_name {fabric_name}, " + msg += "serial_numbers " + msg += f"{','.join(serial_numbers)}. " + msg += f"Got response {self.results.response_current}" + raise ControllerResponseError(msg) + # Use this if we cannot update maintenance mode in frozen fabrics # self._can_fabric_be_deployed() # if self.fabric_can_be_deployed is False: From 49c0e438fbdba87b49151a51827f6c0660f2d33f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 24 May 2024 12:45:37 -1000 Subject: [PATCH 077/230] Error handling, and query state content. 1. Merged(): Remove role key from self.have. 2. Merged(): fail_json() if switch is in inconsistent or migration states. 3. Merged(): fail_json() if fabric freezeMode is enabled for a switch's hosting fabric. 4. Query(): Add freezeMode state to query result diff with key deployment_disabled and value of True or False. 5. SwitchDetails(): Add freeze_mode property. --- plugins/module_utils/common/switch_details.py | 9 ++++ plugins/modules/dcnm_maintenance_mode.py | 49 ++++++++++++------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 45dbbd0a6..6bea14284 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -191,6 +191,15 @@ def fabric_name(self): """ return self._get("fabricName") + @property + def freeze_mode(self): + """ + - Return the ``freezeMode`` of the filtered switch's fabric, + if it exists. + - Return ``None`` otherwise. + """ + return self._get("freezeMode") + @property def hostname(self): """ diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index a10679789..302aa92a3 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -511,24 +511,15 @@ def get_have(self): "192.169.1.2": { fabric_name: "MyFabric", mode: "maintenance", - role: "spine", serial_number: "FCI1234567" }, "192.169.1.3": { fabric_name: "YourFabric", mode: "normal", - role: "leaf", serial_number: "FCH2345678" } } ``` - ### NOTES - - We are not currently using ``role``. We added it to improve - error messages, but will need to pass this to MaintenanceMode() - in order to do so. This will require adding a ``role`` property - to MaintenanceMode(). But ``role`` is not strictly needed for the - MaintenanceMode() class. Hence, we're not adding this now. Maybe - in a future release. """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.switch_details.rest_send = RestSend(self.ansible_module) @@ -538,15 +529,27 @@ def get_have(self): for switch in self.config.get("switches"): ip_address = switch.get("ip_address") self.switch_details.filter = ip_address + fabric_name = self.switch_details.fabric_name + + if self.switch_details.freeze_mode is True: + msg = f"{self.class_name}.{method_name}: " + msg += f"Fabric {fabric_name} is in freeze mode. " + msg += "Configuration changes are not allowed. " + msg += "Ensure that NDFC -> Topology -> Fabric -> Actions -> " + msg += "More -> Deployment Enable is selected." + self.ansible_module.fail_json(msg, **self.results.failed_result) + try: serial_number = self.switch_details.serial_number except ValueError as error: self.ansible_module.fail_json(f"{error}", **self.results.failed_result) + if serial_number is None: msg = f"{self.class_name}.{method_name}: " msg += f"Switch with ip_address {ip_address} " msg += "does not exist on the controller." self.ansible_module.fail_json(msg, **self.results.failed_result) + mode = self.switch_details.maintenance_mode if mode == "inconsistent": msg = f"{self.class_name}.{method_name}: " @@ -568,12 +571,10 @@ def get_have(self): msg += "manually modified to match the switch role in the " msg += "hosting fabric." self.ansible_module.fail_json(msg, **self.results.failed_result) - fabric_name = self.switch_details.fabric_name - role = self.switch_details.role + self.have[ip_address] = {} self.have[ip_address].update({"fabric_name": fabric_name}) self.have[ip_address].update({"mode": mode}) - self.have[ip_address].update({"role": role}) self.have[ip_address].update({"serial_number": serial_number}) def get_need(self): @@ -640,9 +641,12 @@ def commit(self): def send_need(self) -> None: """ - Caller: commit() - + ### Summary Build and send the payload to modify maintenance mode. + + ### Raises + - ``ValueError`` if MaintenanceMode() raises ``ValueError`` + """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable msg = f"{self.class_name}.{method_name}: entered. " @@ -656,7 +660,7 @@ def send_need(self) -> None: return instance = MaintenanceMode(self.params) - instance.rest_send = RestSend(self.ansible_module) + instance.rest_send = self.rest_send instance.results = self.results try: instance.config = self.need @@ -692,6 +696,9 @@ def get_have(self): Have is a dict, keyed on switch_ip, where each element is a dict with the following structure: - ``fabric_name``: The name of the switch's hosting fabric. + - ``freeze_mode``: The current state of the switch's hosting fabric. + If freeze_mode is True, configuration changes cannot be made to the + fabric or the switches within the fabric. - ``mode``: The current maintenance mode of the switch. - ``role``: The role of the switch in the hosting fabric. - ``serial_number``: The serial number of the switch. @@ -699,12 +706,14 @@ def get_have(self): ```json { "192.169.1.2": { + deployment_disabled: true fabric_name: "MyFabric", mode: "maintenance", role: "spine", serial_number: "FCI1234567" }, "192.169.1.3": { + deployment_disabled: false fabric_name: "YourFabric", mode: "normal", role: "leaf", @@ -731,17 +740,23 @@ def get_have(self): msg += "does not exist on the controller." self.ansible_module.fail_json(msg, **self.results.failed_result) + fabric_name = self.switch_details.fabric_name + freeze_mode = self.switch_details.freeze_mode mode = self.switch_details.maintenance_mode role = self.switch_details.switch_role - fabric_name = self.switch_details.fabric_name + self.have[ip_address] = {} + self.have[ip_address].update({"fabric_name": fabric_name}) + if freeze_mode is True: + self.have[ip_address].update({"deployment_disabled": True}) + else: + self.have[ip_address].update({"deployment_disabled": False}) self.have[ip_address].update({"mode": mode}) if role is not None: self.have[ip_address].update({"role": role}) else: self.have[ip_address].update({"role": "na"}) self.have[ip_address].update({"serial_number": serial_number}) - self.have[ip_address].update({"fabric_name": fabric_name}) def commit(self) -> None: """ From 89c9e41951a934d5f4cbbcdc05876facf5ada0da Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 24 May 2024 16:14:15 -1000 Subject: [PATCH 078/230] Handle read-only fabrics, more... All changes in this commit are within dcnm_maintenance_mode.py 1. Common().__init__: Remove several things that require AnsibleModule to be set and add them to parts of the code where AnsibleModule has already been set. 2. Throughout: Add try-except blocks around vulnerable calls. 3. Common(), Merge(), Query(): replace calls to fail_json() with exceptions and catch these in main() 4. freezeMode (returned by .../allswitches endpoint) is set to null for read-only LAN_Classic fabrics. Hence, we cannot use it for this (and maybe other) fabric type(s). Leverage FabricDetailsByName() and raise exception if IS_READ_ONLY == True for a switch's hosting fabric. 5. For all methods, add a Raises section to their docstrings indicating if and when they raise exceptions, and what type of exceptions are raised. 6. Query().__init__(): Change input parameter from ansible_module to params. 7. main(): else statement was using task.ansible_module, but there would be no instantiate task here. Fixed. --- plugins/modules/dcnm_maintenance_mode.py | 198 ++++++++++++++++++----- 1 file changed, 162 insertions(+), 36 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 302aa92a3..b3429c364 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -129,6 +129,8 @@ from typing import Any, Dict from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import \ MaintenanceMode @@ -144,6 +146,8 @@ Results from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ SwitchDetails +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ + FabricDetailsByName def json_pretty(msg): @@ -285,6 +289,11 @@ class Common: """ def __init__(self, params): + """ + ### Raises + - ``ValueError`` if params does not contain ``check_mode`` + - ``ValueError`` if params does not contain ``state`` + """ self.class_name = self.__class__.__name__ self.params = params self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -308,27 +317,30 @@ def __init__(self, params): self.results.state = self.state self.results.check_mode = self.check_mode + self.switch_details = SwitchDetails() + self.switch_details.results = self.results + self.params_spec = ParamsSpec() try: self.params_spec.params = self.params except ValueError as error: - self.ansible_module.fail_json(error, **self.results.failed_result) + raise ValueError(error) from error + try: self.params_spec.commit() except ValueError as error: - self.ansible_module.fail_json(error, **self.results.failed_result) + raise ValueError(error) from error msg = f"ENTERED Common().{method_name}: " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) - self.switch_details = SwitchDetails() - self.switch_details.results = self.results - # populated in self.validate_input() self.payloads = {} + # initialized in self.get_want() + self.validator = None # populated in self.get_want() self.validated_configs = [] @@ -338,8 +350,6 @@ def __init__(self, params): msg += f"got {type(self.config).__name__}" raise ValueError(msg) - self.validator = ParamsValidate(self.ansible_module) - self.validated = [] self.have = {} self.want = [] @@ -356,6 +366,13 @@ def get_want(self) -> None: ### Summary Build self.want, a list of validated playbook configurations. + ### Raises + - ``ValueError`` if self.ansible_module is not set + - ``ValueError`` if ParamsSpec() raises ``ValueError`` + - ``ValueError`` _merge_global_and_switch_configs() + raises ``ValueError`` + + ### Details 1. Merge the playbook global config into each switch config. 2. Validate the merged configs from step 1 against the param spec. 3. Populate self.want with the validated configs. @@ -377,15 +394,30 @@ def get_want(self) -> None: ] ``` """ - msg = "ENTERED" - self.log.debug(msg) + method_name = inspect.stack()[0][3] + + if self.ansible_module is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"self.ansible_module must be set before calling {method_name}" + raise ValueError(msg) + # Generate the params_spec used to validate the configs params_spec = ParamsSpec() - params_spec.params = self.params - params_spec.commit() + try: + params_spec.params = self.params + except ValueError as error: + raise ValueError(error) from error + + try: + params_spec.commit() + except ValueError as error: + raise ValueError(error) from error # Builds self.switch_configs - self._merge_global_and_switch_configs(self.config) + try: + self._merge_global_and_switch_configs(self.config) + except ValueError as error: + raise ValueError(error) from error # If a parameter is missing from the config, and the parameter # has a default value, merge the default value for the parameter @@ -400,6 +432,7 @@ def get_want(self) -> None: # validate the merged configs self.validated_configs = [] + self.validator = ParamsValidate(self.ansible_module) self.validator.params_spec = params_spec.params_spec for config in merged_configs: self.validator.parameters = config @@ -416,6 +449,9 @@ def _merge_global_and_switch_configs(self, config) -> None: Merge the global playbook config with each switch config and populate a list of merged configs (``self.switch_configs``). + ### Raises + - ``ValueError`` if playbook is missing list of switches + ### Merge rules - switch_config takes precedence over global_config. - If switch_config is missing a parameter, use parameter @@ -427,7 +463,7 @@ def _merge_global_and_switch_configs(self, config) -> None: if not config.get("switches"): msg = f"{self.class_name}.{method_name}: " msg += "playbook is missing list of switches" - self.ansible_module.fail_json(msg) + raise ValueError(msg) self.switch_configs = [] merged_configs = [] @@ -486,6 +522,7 @@ def __init__(self, params): method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fabric_details = FabricDetailsByName(self.params) msg = f"ENTERED Merged.{method_name}: " msg += f"state: {self.state}, " @@ -499,6 +536,15 @@ def get_have(self): ### Summary Build self.have, a dict containing the current mode of all switches. + ### Raises + - ``ValueError`` if self.ansible_module is not set + - ``ValueError`` if SwitchDetails() raises ``ControllerResponseError`` + or ``ValueError`` + - ``ValueError`` if the switch's hosting fabric is in ``freezeMode`` + - ``ValueError`` if the switch's maintenance mode is ``inconsistent`` + - ``ValueError`` if the switch's maintenance mode is ``migration`` + + ### self.have structure Have is a dict, keyed on switch_ip, where each element is a dict with the following structure: - ``fabric_name``: The name of the switch's hosting fabric. @@ -522,14 +568,31 @@ def get_have(self): ``` """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + if self.ansible_module is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"ansible_module must be set before calling {method_name}" + raise ValueError(msg) + self.switch_details.rest_send = RestSend(self.ansible_module) - self.switch_details.refresh() + try: + self.switch_details.refresh() + except (ControllerResponseError, ValueError) as error: + raise ValueError(error) from error + + self.fabric_details.rest_send = RestSend(self.ansible_module) + self.fabric_details.results = self.results + self.fabric_details.refresh() + self.have = {} # self.config has already been validated for switch in self.config.get("switches"): ip_address = switch.get("ip_address") self.switch_details.filter = ip_address - fabric_name = self.switch_details.fabric_name + + try: + fabric_name = self.switch_details.fabric_name + except ValueError as error: + raise ValueError(error) from error if self.switch_details.freeze_mode is True: msg = f"{self.class_name}.{method_name}: " @@ -537,18 +600,29 @@ def get_have(self): msg += "Configuration changes are not allowed. " msg += "Ensure that NDFC -> Topology -> Fabric -> Actions -> " msg += "More -> Deployment Enable is selected." - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) + + try: + self.fabric_details.filter = fabric_name + except ValueError as error: + raise ValueError(error) from error + + if self.fabric_details.is_read_only is True: + msg = f"{self.class_name}.{method_name}: " + msg += f"Fabric {fabric_name} is in read-only mode. " + msg += "Configuration changes are not allowed." + raise ValueError(msg) try: serial_number = self.switch_details.serial_number except ValueError as error: - self.ansible_module.fail_json(f"{error}", **self.results.failed_result) + raise ValueError(error) from error if serial_number is None: msg = f"{self.class_name}.{method_name}: " msg += f"Switch with ip_address {ip_address} " msg += "does not exist on the controller." - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) mode = self.switch_details.maintenance_mode if mode == "inconsistent": @@ -558,7 +632,8 @@ def get_have(self): msg += f"with ip_address {ip_address}. This is typically " msg += "resolved by initiating a switch Deploy Config on " msg += "the controller." - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) + if mode == "migration": msg = f"{self.class_name}.{method_name}: " msg += "Switch maintenance mode is in migration state for the " @@ -570,7 +645,7 @@ def get_have(self): msg += "Failing that, the switch configuration might need to be " msg += "manually modified to match the switch role in the " msg += "hosting fabric." - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) self.have[ip_address] = {} self.have[ip_address].update({"fabric_name": fabric_name}) @@ -582,8 +657,8 @@ def get_need(self): ### Summary Build self.need for merged state. - ### Caller - commit() + ### Raises + None ### self.need structure ```json @@ -622,7 +697,13 @@ def get_need(self): def commit(self): """ + ### Summary Commit the merged state request + + ### Raises + - ``ValueError`` if get_want() raises ``ValueError`` + - ``ValueError`` if get_have() raises ``ValueError`` + - ``ValueError`` if send_need() raises ``ValueError`` """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable msg = f"{self.class_name}.{method_name}: entered" @@ -630,14 +711,22 @@ def commit(self): self.rest_send = RestSend(self.ansible_module) - self.get_want() - self.get_have() + try: + self.get_want() + except ValueError as error: + raise ValueError(error) from error + + try: + self.get_have() + except ValueError as error: + raise ValueError(error) from error + self.get_need() + try: self.send_need() except ValueError as error: - self.results.build_final_result() - self.ansible_module.fail_json(f"{error}", **self.results.final_result) + raise ValueError(error) from error def send_need(self) -> None: """ @@ -677,9 +766,9 @@ class Query(Common): Handle query state """ - def __init__(self, ansible_module): + def __init__(self, params): self.class_name = self.__class__.__name__ - super().__init__(ansible_module) + super().__init__(params) self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -693,6 +782,12 @@ def get_have(self): ### Summary Build self.have, a dict containing the current mode of all switches. + ### Raises + - ``ValueError`` if self.ansible_module is not set + - ``ValueError`` if SwitchDetails() raises ``ControllerResponseError`` + - ``ValueError`` if SwitchDetails() raises ``ValueError`` + + ### self.have structure Have is a dict, keyed on switch_ip, where each element is a dict with the following structure: - ``fabric_name``: The name of the switch's hosting fabric. @@ -723,22 +818,34 @@ def get_have(self): ``` """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + if self.ansible_module is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"ansible_module must be set before calling {method_name}" + raise ValueError(msg) + self.switch_details.rest_send = RestSend(self.ansible_module) - self.switch_details.refresh() + + try: + self.switch_details.refresh() + except (ControllerResponseError, ValueError) as error: + raise ValueError(error) from error + self.have = {} # self.config has already been validated for switch in self.config.get("switches"): ip_address = switch.get("ip_address") self.switch_details.filter = ip_address + try: serial_number = self.switch_details.serial_number except ValueError as error: - self.ansible_module.fail_json(f"{error}", **self.results.failed_result) + raise ValueError(error) from error + if serial_number is None: msg = f"{self.class_name}.{method_name}: " msg += f"Switch with ip_address {ip_address} " msg += "does not exist on the controller." - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) fabric_name = self.switch_details.fabric_name freeze_mode = self.switch_details.freeze_mode @@ -763,9 +870,20 @@ def commit(self) -> None: ### Summary Query the switches in self.want that exist on the controller and update ``self.results`` with the query results. + + ### Raises + - ``ValueError`` if get_want() raises ``ValueError`` + - ``ValueError`` if get_have() raises ``ValueError`` """ - self.get_want() - self.get_have() + try: + self.get_want() + except ValueError as error: + raise ValueError(error) from error + + try: + self.get_have() + except ValueError as error: + raise ValueError(error) from error # If we got this far, the request was successful. self.results.diff_current = self.have @@ -821,15 +939,23 @@ def main(): if ansible_module.params["state"] == "merged": task = Merged(ansible_module.params) task.ansible_module = ansible_module - task.commit() + try: + task.commit() + except ValueError as error: + ansible_module.fail_json(f"{error}", **task.results.failed_result) + elif ansible_module.params["state"] == "query": task = Query(ansible_module.params) task.ansible_module = ansible_module - task.commit() + try: + task.commit() + except ValueError as error: + ansible_module.fail_json(f"{error}", **task.results.failed_result) + else: # We should never get here since the state parameter has # already been validated. - msg = f"Unknown state {task.ansible_module.params['state']}" + msg = f"Unknown state {ansible_module.params['state']}" ansible_module.fail_json(msg) task.results.build_final_result() From 4d3361f566e3d0b4eb38df88844dcb1acc98df17 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 24 May 2024 16:56:46 -1000 Subject: [PATCH 079/230] Query(): Update deployment_disabled for read-only fabrics. This commit changes only dcnm_maintenance_mode.py Query(): In the case of LAN_Classic fabrics (and perhaps other fabric types), leverage FabricDetailsByName() and reference fabric parameter IS_READ_ONLY to determine if the fabric is read-only. Update "deployment_disabled" to True if either IS_READ_ONLY == True or freezeMode == True. Previously, we were updating "deployment_disable" only in the case of freezeMode == True. --- plugins/modules/dcnm_maintenance_mode.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index b3429c364..fac0a2a5e 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -771,6 +771,7 @@ def __init__(self, params): super().__init__(params) self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.fabric_details = FabricDetailsByName(self.params) msg = "ENTERED Query(): " msg += f"state: {self.state}, " @@ -824,12 +825,18 @@ def get_have(self): raise ValueError(msg) self.switch_details.rest_send = RestSend(self.ansible_module) + self.fabric_details.rest_send = RestSend(self.ansible_module) try: self.switch_details.refresh() except (ControllerResponseError, ValueError) as error: raise ValueError(error) from error + try: + self.fabric_details.refresh() + except (ControllerResponseError, ValueError) as error: + raise ValueError(error) from error + self.have = {} # self.config has already been validated for switch in self.config.get("switches"): @@ -852,9 +859,15 @@ def get_have(self): mode = self.switch_details.maintenance_mode role = self.switch_details.switch_role + try: + self.fabric_details.filter = fabric_name + except ValueError as error: + raise ValueError(error) from error + fabric_read_only = self.fabric_details.is_read_only + self.have[ip_address] = {} self.have[ip_address].update({"fabric_name": fabric_name}) - if freeze_mode is True: + if freeze_mode is True or fabric_read_only is True: self.have[ip_address].update({"deployment_disabled": True}) else: self.have[ip_address].update({"deployment_disabled": False}) From 446c2d65067a4e93471271fbaca794000048b660 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 26 May 2024 10:35:29 -1000 Subject: [PATCH 080/230] SwitchDetails(): Improve docstrings 1. Add Raises section to all method docstrings. 2. For all properties that call SwitchDetails()._get(), add a note in the docstring that the property can potentially raise ValueError. --- plugins/module_utils/common/switch_details.py | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 6bea14284..3d47843c4 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -170,10 +170,17 @@ def _get(self, item): @property def filter(self): """ + ### Summary Set the query filter. + + ### Raises + None. However, if ``filter`` is not set, or ``filter`` is set to + a non-existent switch, ``ValueError`` will be raised when accessing + the various getter properties. - The filter should be the ip_address of the switch from which to - retrieve details. + ### Details + The filter should be the ip_address of the + switch from which to retrieve details. ``filter`` must be set before accessing this class's properties. """ @@ -188,6 +195,8 @@ def fabric_name(self): """ - Return the ``fabricName`` of the filtered switch, if it exists. - Return ``None`` otherwise. + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("fabricName") @@ -197,6 +206,8 @@ def freeze_mode(self): - Return the ``freezeMode`` of the filtered switch's fabric, if it exists. - Return ``None`` otherwise. + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("freezeMode") @@ -205,6 +216,8 @@ def hostname(self): """ - Return the ``hostName`` of the filtered switch, if it exists. - Return ``None`` otherwise. + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. ### NOTES - ``hostname`` is None for NDFC version 12.1.2e @@ -229,6 +242,8 @@ def is_non_nexus(self): - Return the ``isNonNexus`` status of the filtered switch, if it exists. - Return ``None`` otherwise - Example: false, true + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("isNonNexus") @@ -237,6 +252,8 @@ def logical_name(self): """ - Return the ``logicalName`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("logicalName") @@ -292,8 +309,12 @@ def maintenance_mode(self): @property def managable(self): """ + - Yes, managable is misspelled. It is spelled this way in the + controller response. - Return the ``managable`` status of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. - Example: false, true """ return self._get("managable") @@ -303,6 +324,8 @@ def mode(self): """ - Return the ``mode`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. - ``mode`` is converted from Titlecase to lowercase. - Example: maintenance, migration, normal, inconsistent """ @@ -316,6 +339,8 @@ def model(self): """ - Return the ``model`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("model") @@ -324,6 +349,8 @@ def oper_status(self): """ - Return the ``operStatus`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. - Example: Minor """ return self._get("operStatus") @@ -333,6 +360,8 @@ def platform(self): """ - Return the ``platform`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. ### NOTES - ``platform`` is derived from ``model``. @@ -348,6 +377,8 @@ def release(self): """ - Return the ``release`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. - Example: 10.2(5) """ return self._get("release") @@ -379,6 +410,8 @@ def role(self): """ - Return the ``switchRole`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("switchRole") @@ -387,6 +420,8 @@ def serial_number(self): """ - Return the ``serialNumber`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("serialNumber") @@ -395,6 +430,8 @@ def source_interface(self): """ - Return the ``sourceInterface`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("sourceInterface") @@ -403,6 +440,8 @@ def source_vrf(self): """ - Return the ``sourceVrf`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("sourceVrf") @@ -411,6 +450,8 @@ def status(self): """ - Return the ``status`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("status") @@ -419,6 +460,8 @@ def switch_db_id(self): """ - Return the ``switchDbID`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("switchDbID") @@ -427,6 +470,8 @@ def switch_role(self): """ - Return the ``switchRole`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("switchRole") @@ -435,6 +480,8 @@ def switch_uuid(self): """ - Return the ``swUUID`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("swUUID") @@ -443,6 +490,8 @@ def switch_uuid_id(self): """ - Return the ``swUUIDId`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("swUUIDId") @@ -451,5 +500,7 @@ def system_mode(self): """ - Return the ``systemMode`` of the filtered switch, if it exists. - Return ``None`` otherwise + - Raises ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. """ return self._get("systemMode") From 18ff32f3fe9776e64cabc0e3c8bfe1f5978b8e36 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 26 May 2024 10:40:14 -1000 Subject: [PATCH 081/230] SwitchDetails(): Fix PEP8 whitespace in blank line --- plugins/module_utils/common/switch_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 3d47843c4..2b16acc91 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -172,7 +172,7 @@ def filter(self): """ ### Summary Set the query filter. - + ### Raises None. However, if ``filter`` is not set, or ``filter`` is set to a non-existent switch, ``ValueError`` will be raised when accessing From e2e5425c2e0e10e2efbeb9af616b7b6a40fa6f4c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 26 May 2024 10:54:53 -1000 Subject: [PATCH 082/230] api: Add unit tests Add unit tests for the following classes: - EpFabricConfigDeploy - EpFabrics - EpMaintenanceModeDisable - EpMaintenanceModeEnable - Fabrics --- .../common/api/test_v1_api_fabrics.py | 375 +++++++++++++++++- 1 file changed, 367 insertions(+), 8 deletions(-) diff --git a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py index 5ed96bd84..3a019ad91 100644 --- a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py +++ b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py @@ -20,13 +20,48 @@ import pytest from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import ( EpFabricConfigDeploy, EpFabricConfigSave, EpFabricCreate, EpFabricDelete, - EpFabricDetails, EpFabricFreezeMode, EpFabricUpdate) + EpFabricDetails, EpFabricFreezeMode, EpFabrics, EpFabricUpdate, + EpMaintenanceModeDisable, EpMaintenanceModeEnable, Fabrics) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ does_not_raise PATH_PREFIX = "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics" FABRIC_NAME = "MyFabric" +SERIAL_NUMBER = "CHS12345678" TEMPLATE_NAME = "Easy_Fabric" +TICKET_ID = "MyTicket1234" + + +def test_ep_fabrics_00000(): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify __init__ method + - Correct class_name + - Correct default values + - Correct contents of required_properties + - Correct contents of properties dict + - Properties return values from properties dict + - path property raises ``ValueError`` when accessed, since + ``fabric_name`` is not yet set. + """ + with does_not_raise(): + instance = EpFabricConfigDeploy() + assert instance.class_name == "EpFabricConfigDeploy" + assert "fabric_name" in instance.required_properties + assert len(instance.required_properties) == 1 + assert instance.properties["force_show_run"] is False + assert instance.properties["include_all_msd_switches"] is False + assert instance.properties["switch_id"] is None + assert instance.properties["verb"] == "POST" + assert instance.force_show_run is False + assert instance.include_all_msd_switches is False + assert instance.switch_id is None + match = r"EpFabricConfigDeploy.path_fabric_name:\s+" + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement def test_ep_fabrics_00010(): @@ -89,6 +124,25 @@ def test_ep_fabrics_00040(): ### Class - EpFabricConfigDeploy + ### Summary + - Verify setting ``switch_id`` results in change to path. + """ + with does_not_raise(): + instance = EpFabricConfigDeploy() + instance.fabric_name = FABRIC_NAME + instance.switch_id = SERIAL_NUMBER + instance.force_show_run = True + path = f"{PATH_PREFIX}/{FABRIC_NAME}/config-deploy/{SERIAL_NUMBER}" + path += "?forceShowRun=True" + assert instance.path == path + assert instance.verb == "POST" + + +def test_ep_fabrics_00050(): + """ + ### Class + - EpFabricConfigDeploy + ### Summary - Verify ``ValueError`` is raised if path is accessed before setting ``fabric_name``. @@ -102,7 +156,7 @@ def test_ep_fabrics_00040(): instance.path # pylint: disable=pointless-statement -def test_ep_fabrics_00050(): +def test_ep_fabrics_00060(): """ ### Class - EpFabricConfigDeploy @@ -121,7 +175,7 @@ def test_ep_fabrics_00050(): instance.fabric_name = fabric_name # pylint: disable=pointless-statement -def test_ep_fabrics_00060(): +def test_ep_fabrics_00070(): """ ### Class - EpFabricConfigDeploy @@ -139,7 +193,7 @@ def test_ep_fabrics_00060(): instance.force_show_run = "NOT_BOOLEAN" # pylint: disable=pointless-statement -def test_ep_fabrics_00070(): +def test_ep_fabrics_00080(): """ ### Class - EpFabricConfigDeploy @@ -159,6 +213,42 @@ def test_ep_fabrics_00070(): ) +MATCH_00090 = r"EpFabricConfigDeploy.switch_id:\s+" +MATCH_00090 += r"Expected string or list for switch_id\.\s+" + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (SERIAL_NUMBER, False, does_not_raise()), + ([SERIAL_NUMBER], False, does_not_raise()), + (EpFabricCreate(), True, pytest.raises(TypeError, match=MATCH_00090)), + (None, True, pytest.raises(TypeError, match=MATCH_00090)), + (10, True, pytest.raises(TypeError, match=MATCH_00090)), + ([10], True, pytest.raises(TypeError, match=MATCH_00090)), + ({10}, True, pytest.raises(TypeError, match=MATCH_00090)), + ], +) +def test_ep_fabrics_00090(value, does_raise, expected): + """ + ### Class + - EpFabricConfigDeploy + + ### Summary + - Verify exception is not raised if ``switch_id`` is a string or list. + - Verify ``ValueError`` is raised if ``switch_id`` is not a str or list. + """ + with does_not_raise(): + instance = EpFabricConfigDeploy() + with expected: + instance.switch_id = value # pylint: disable=pointless-statement + if not does_raise: + if isinstance(value, list): + assert instance.switch_id == ",".join(value) + else: + assert instance.switch_id == value + + def test_ep_fabrics_00100(): """ ### Class @@ -185,9 +275,9 @@ def test_ep_fabrics_00110(): with does_not_raise(): instance = EpFabricConfigSave() instance.fabric_name = FABRIC_NAME - instance.ticket_id = "MyTicket1234" + instance.ticket_id = TICKET_ID ticket_id_path = f"{PATH_PREFIX}/{FABRIC_NAME}/config-save" - ticket_id_path += "?ticketId=MyTicket1234" + ticket_id_path += f"?ticketId={TICKET_ID}" assert instance.path == ticket_id_path assert instance.verb == "POST" @@ -203,9 +293,9 @@ def test_ep_fabrics_00120(): with does_not_raise(): instance = EpFabricConfigSave() instance.fabric_name = FABRIC_NAME - instance.ticket_id = "MyTicket1234" + instance.ticket_id = TICKET_ID ticket_id_path = f"{PATH_PREFIX}/{FABRIC_NAME}/config-save" - ticket_id_path += "?ticketId=MyTicket1234" + ticket_id_path += f"?ticketId={TICKET_ID}" assert instance.path == ticket_id_path assert instance.verb == "POST" @@ -607,3 +697,272 @@ def test_ep_fabrics_00770(): match += r"Expected one of:.*\." with pytest.raises(ValueError, match=match): instance.template_name = template_name # pylint: disable=pointless-statement + + +def test_ep_fabrics_00800(): + """ + ### Class + - EpFabrics + + ### Summary + - Verify __init__ method + - Correct class_name + """ + with does_not_raise(): + instance = EpFabrics() + assert instance.class_name == "EpFabrics" + + +def test_ep_fabrics_00810(): + """ + ### Class + - EpFabrics + + ### Summary + - Verify path and verb + """ + with does_not_raise(): + instance = EpFabrics() + assert instance.path == f"{PATH_PREFIX}" + assert instance.verb == "GET" + + +def test_ep_fabrics_03000(): + """ + ### Class + - EpMaintenanceModeEnable + + ### Summary + - Verify __init__ method + - Correct class_name + - Correct contents of required_properties + - Correct verb is returned + """ + with does_not_raise(): + instance = EpMaintenanceModeEnable() + assert instance.class_name == "EpMaintenanceModeEnable" + assert "fabric_name" in instance.required_properties + assert "serial_number" in instance.required_properties + assert instance.verb == "POST" + + +def test_ep_fabrics_03010(): + """ + ### Class + - EpMaintenanceModeEnable + + ### Summary + - verb property returns POST. + """ + with does_not_raise(): + instance = EpMaintenanceModeEnable() + assert instance.verb == "POST" + + +def test_ep_fabrics_03020(): + """ + ### Class + - EpMaintenanceModeEnable + + ### Summary + - Verify path property raises ``ValueError`` if accessed before setting + fabric_name. + """ + with does_not_raise(): + instance = EpMaintenanceModeEnable() + instance.serial_number = SERIAL_NUMBER + match = r"EpMaintenanceModeEnable.path_fabric_name_serial_number:\s+" + match += r"fabric_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_03030(): + """ + ### Class + - EpMaintenanceModeEnable + + ### Summary + - Verify path property raises ``ValueError`` if accessed before setting + serial_number. + """ + with does_not_raise(): + instance = EpMaintenanceModeEnable() + instance.fabric_name = FABRIC_NAME + match = r"EpMaintenanceModeEnable.path_fabric_name_serial_number:\s+" + match += r"serial_number must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_03040(): + """ + ### Class + - EpMaintenanceModeEnable + + ### Summary + - Verify path is set correctly if fabric_name and + serial_number are provided. + """ + with does_not_raise(): + instance = EpMaintenanceModeEnable() + instance.fabric_name = FABRIC_NAME + instance.serial_number = SERIAL_NUMBER + path = f"{PATH_PREFIX}/{FABRIC_NAME}/switches/{SERIAL_NUMBER}" + path += "/maintenance-mode" + assert instance.path == path + + +def test_ep_fabrics_03050(): + """ + ### Class + - EpMaintenanceModeEnable + + ### Summary + - Verify path is set correctly if fabric_name and + serial_number and ticket_id are provided. + """ + with does_not_raise(): + instance = EpMaintenanceModeEnable() + instance.fabric_name = FABRIC_NAME + instance.serial_number = SERIAL_NUMBER + instance.ticket_id = TICKET_ID + path = f"{PATH_PREFIX}/{FABRIC_NAME}/switches/{SERIAL_NUMBER}" + path += f"/maintenance-mode?ticketId={TICKET_ID}" + assert instance.path == path + + +def test_ep_fabrics_03100(): + """ + ### Class + - EpMaintenanceModeDisable + + ### Summary + - Verify __init__ method + - Correct class_name + - Correct contents of required_properties + """ + with does_not_raise(): + instance = EpMaintenanceModeDisable() + assert instance.class_name == "EpMaintenanceModeDisable" + assert "fabric_name" in instance.required_properties + assert "serial_number" in instance.required_properties + + +def test_ep_fabrics_03110(): + """ + ### Class + - EpMaintenanceModeDisable + + ### Summary + - verb property returns DELETE. + """ + with does_not_raise(): + instance = EpMaintenanceModeDisable() + assert instance.verb == "DELETE" + + +def test_ep_fabrics_03120(): + """ + ### Class + - EpMaintenanceModeDisable + + ### Summary + - Verify path property raises ``ValueError`` if accessed before setting + fabric_name. + """ + with does_not_raise(): + instance = EpMaintenanceModeDisable() + instance.serial_number = SERIAL_NUMBER + match = r"EpMaintenanceModeDisable.path_fabric_name_serial_number:\s+" + match += r"fabric_name must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_03130(): + """ + ### Class + - EpMaintenanceModeDisable + + ### Summary + - Verify path property raises ``ValueError`` if accessed before setting + serial_number. + """ + with does_not_raise(): + instance = EpMaintenanceModeDisable() + instance.fabric_name = FABRIC_NAME + match = r"EpMaintenanceModeDisable.path_fabric_name_serial_number:\s+" + match += r"serial_number must be set prior to accessing path\." + with pytest.raises(ValueError, match=match): + instance.path # pylint: disable=pointless-statement + + +def test_ep_fabrics_03140(): + """ + ### Class + - EpMaintenanceModeDisable + + ### Summary + - Verify path is set correctly if fabric_name and + serial_number are provided. + """ + with does_not_raise(): + instance = EpMaintenanceModeDisable() + instance.fabric_name = FABRIC_NAME + instance.serial_number = SERIAL_NUMBER + path = f"{PATH_PREFIX}/{FABRIC_NAME}/switches/{SERIAL_NUMBER}" + path += "/maintenance-mode" + assert instance.path == path + + +def test_ep_fabrics_03150(): + """ + ### Class + - EpMaintenanceModeDisable + + ### Summary + - Verify path is set correctly if fabric_name and + serial_number and ticket_id are provided. + """ + with does_not_raise(): + instance = EpMaintenanceModeDisable() + instance.fabric_name = FABRIC_NAME + instance.serial_number = SERIAL_NUMBER + instance.ticket_id = TICKET_ID + path = f"{PATH_PREFIX}/{FABRIC_NAME}/switches/{SERIAL_NUMBER}" + path += f"/maintenance-mode?ticketId={TICKET_ID}" + assert instance.path == path + + +MATCH_10000 = r"Fabrics.serial_number:\s+" +MATCH_10000 += r"Expected string for serial_number\.\s+" + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (SERIAL_NUMBER, False, does_not_raise()), + ([SERIAL_NUMBER], True, pytest.raises(TypeError, match=MATCH_10000)), + (EpFabricCreate(), True, pytest.raises(TypeError, match=MATCH_10000)), + (None, True, pytest.raises(TypeError, match=MATCH_10000)), + (10, True, pytest.raises(TypeError, match=MATCH_10000)), + ([10], True, pytest.raises(TypeError, match=MATCH_10000)), + ({10}, True, pytest.raises(TypeError, match=MATCH_10000)), + ], +) +def test_ep_fabrics_10000(value, does_raise, expected): + """ + ### Class + - Fabrics + + ### Summary + - Verify serial_number does not raise if set to string. + - Verify serial_number raises ``ValueError`` if not a string. + """ + with does_not_raise(): + instance = Fabrics() + with expected: + instance.serial_number = value # pylint: disable=pointless-statement + if not does_raise: + assert instance.serial_number == value From ac7c2908041e8d2a43eff2747dfd1d3583b811d3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 26 May 2024 10:58:56 -1000 Subject: [PATCH 083/230] Fix PEP8 errors, more... Unit tests failed because I forgot to add modified api fabrics.py that the UT were testing :-( --- .../rest/control/fabrics/fabrics.py | 227 ++++++++++-------- 1 file changed, 122 insertions(+), 105 deletions(-) diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py index 23a6b714a..c80cacd89 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py @@ -135,7 +135,7 @@ def serial_number(self): """ - getter: Return the switch serial_number. - setter: Set the switch serial_number. - - setter: Raise ``ValueError`` if serial_number is not a string. + - setter: Raise ``TypeError`` if serial_number is not a string. - Default: None """ return self.properties["serial_number"] @@ -147,7 +147,7 @@ def serial_number(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"Expected string for {method_name}. " msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) + raise TypeError(msg) self.properties["serial_number"] = value @property @@ -339,7 +339,7 @@ def switch_id(self): """ - getter: Return the switch_id value. - setter: Set the switch_id value. - - setter: Raise ``ValueError`` if switch_id is not a string or list. + - setter: Raise ``TypeError`` if switch_id is not a string or list. - Default: None - Optional - Notes: @@ -353,15 +353,22 @@ def switch_id(self): @switch_id.setter def switch_id(self, value): method_name = inspect.stack()[0][3] + + def error(param, param_type): + msg = f"{self.class_name}.{method_name}: " + msg += "Expected string or list for switch_id. " + msg += f"Got {param} with type {param_type}." + raise TypeError(msg) + if isinstance(value, str): pass elif isinstance(value, list): + for item in value: + if not isinstance(item, str): + error(item, type(item).__name__) value = ",".join(value) else: - msg = f"{self.class_name}.{method_name}: " - msg += f"Expected string or list for {method_name}. " - msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) + error(value, type(value).__name__) self.properties["switch_id"] = value @@ -654,44 +661,43 @@ def path(self): return f"{self.path_fabric_name}/freezemode" -class EpMaintenanceModeEnable(Fabrics): +# class EpFabricSummary() See module_utils/common/api/v1/rest/control/switches.py + + +class EpFabricUpdate(Fabrics): """ - ## V1 API - Fabrics().EpMaintenanceModeEnable() + ## V1 API - Fabrics().EpFabricUpdate() ### Description - Return endpoint to enable maintenance mode on a switch. + Return endpoint information. ### Raises - - ``ValueError``: If ``fabric_name`` is not set. - - ``ValueError``: If ``fabric_name`` is invalid. - - ``ValueError``: If ``serial_number`` is not set. - - ``ValueError``: If ``ticket_id`` is not a string. + - ``ValueError``: If fabric_name is not set. + - ``ValueError``: If fabric_name is invalid. + - ``ValueError``: If template_name is not set. + - ``ValueError``: If template_name is not a valid fabric template name. ### Path - - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode`` - - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode?ticketId={ticket_id}`` + ``/api/v1/lan-fabric/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` ### Verb - - POST + - PUT ### Parameters - fabric_name: string - set the ``fabric_name`` to be used in the path - required - - serial_number: string - - set the switch ``serial_number`` to be used in the path + - template_name: string + - set the ``template_name`` to be used in the path - required - - ticket_id: string - - optional unless Change Control is enabled - path: retrieve the path for the endpoint - verb: retrieve the verb for the endpoint ### Usage ```python - instance = EpMaintenanceModeEnable() + instance = EpFabricUpdate() instance.fabric_name = "MyFabric" - instance.serial_number = "CHM1234567" - instance.ticket_id = "MyTicket1234" + instance.template_name = "Easy_Fabric_IPFM" path = instance.path verb = instance.verb ``` @@ -702,7 +708,7 @@ def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.required_properties.add("fabric_name") - self.required_properties.add("serial_number") + self.required_properties.add("template_name") msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." msg += f"Fabrics.{self.class_name}" self.log.debug(msg) @@ -710,60 +716,39 @@ def __init__(self): @property def path(self): """ - - Endpoint for config-save. - - Set self.ticket_id if Change Control is enabled. + - Endpoint for fabric create. - Raise ``ValueError`` if fabric_name is not set. """ - _path = self.path_fabric_name_serial_number - _path += "/maintenance-mode" - if self.ticket_id: - _path += f"?ticketId={self.ticket_id}" - return _path + return self.path_fabric_name_template_name @property def verb(self): - return "POST" + return "PUT" -class EpMaintenanceModeDisable(Fabrics): +class EpFabrics(Fabrics): """ - ## V1 API - Fabrics().EpMaintenanceModeDisable() + ## V1 API - Fabrics().EpFabrics() ### Description - Return endpoint to remove switch from maintenance mode - (i.e. enable normal mode). + Return the endpoint to query fabrics. ### Raises - - ``ValueError``: If ``fabric_name`` is not set. - - ``ValueError``: If ``fabric_name`` is invalid. - - ``ValueError``: If ``serial_number`` is not set. - - ``ValueError``: If ``ticket_id`` is not a string. + - None ### Path - - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode`` - - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode?ticketId={ticket_id}`` + - ``/api/v1/lan-fabric/rest/control/fabrics`` ### Verb - - DELETE + - GET ### Parameters - - fabric_name: string - - set the ``fabric_name`` to be used in the path - - required - - serial_number: string - - set the switch ``serial_number`` to be used in the path - - required - - ticket_id: string - - optional unless Change Control is enabled - path: retrieve the path for the endpoint - verb: retrieve the verb for the endpoint ### Usage ```python - instance = EpMaintenanceModeDisable() - instance.fabric_name = "MyFabric" - instance.serial_number = "CHM1234567" - instance.ticket_id = "MyTicket1234" + instance = EpFabrics() path = instance.path verb = instance.verb ``` @@ -773,67 +758,58 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.required_properties.add("fabric_name") - self.required_properties.add("serial_number") + self._build_properties() msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." msg += f"Fabrics.{self.class_name}" self.log.debug(msg) - @property - def path(self): - """ - - Endpoint for config-save. - - Set self.ticket_id if Change Control is enabled. - - Raise ``ValueError`` if fabric_name is not set. - """ - _path = self.path_fabric_name_serial_number - _path += "/maintenance-mode" - if self.ticket_id: - _path += f"?ticketId={self.ticket_id}" - return _path + def _build_properties(self): + super()._build_properties() + self.properties["verb"] = "GET" @property - def verb(self): - return "DELETE" - - -# class EpFabricSummary() See module_utils/common/api/v1/rest/control/switches.py + def path(self): + return self.fabrics -class EpFabricUpdate(Fabrics): +class EpMaintenanceModeEnable(Fabrics): """ - ## V1 API - Fabrics().EpFabricUpdate() + ## V1 API - Fabrics().EpMaintenanceModeEnable() ### Description - Return endpoint information. + Return endpoint to enable maintenance mode on a switch. ### Raises - - ``ValueError``: If fabric_name is not set. - - ``ValueError``: If fabric_name is invalid. - - ``ValueError``: If template_name is not set. - - ``ValueError``: If template_name is not a valid fabric template name. + - ``ValueError``: If ``fabric_name`` is not set. + - ``ValueError``: If ``fabric_name`` is invalid. + - ``ValueError``: If ``serial_number`` is not set. + - ``ValueError``: If ``ticket_id`` is not a string. ### Path - ``/api/v1/lan-fabric/rest/control/fabrics/{FABRIC_NAME}/{TEMPLATE_NAME}`` + - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode`` + - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode?ticketId={ticket_id}`` ### Verb - - PUT + - POST ### Parameters - fabric_name: string - set the ``fabric_name`` to be used in the path - required - - template_name: string - - set the ``template_name`` to be used in the path + - serial_number: string + - set the switch ``serial_number`` to be used in the path - required + - ticket_id: string + - optional unless Change Control is enabled - path: retrieve the path for the endpoint - verb: retrieve the verb for the endpoint ### Usage ```python - instance = EpFabricUpdate() + instance = EpMaintenanceModeEnable() instance.fabric_name = "MyFabric" - instance.template_name = "Easy_Fabric_IPFM" + instance.serial_number = "CHM1234567" + instance.ticket_id = "MyTicket1234" path = instance.path verb = instance.verb ``` @@ -844,7 +820,7 @@ def __init__(self): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") self.required_properties.add("fabric_name") - self.required_properties.add("template_name") + self.required_properties.add("serial_number") msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." msg += f"Fabrics.{self.class_name}" self.log.debug(msg) @@ -852,39 +828,65 @@ def __init__(self): @property def path(self): """ - - Endpoint for fabric create. + - Path for maintenance-mode enable - Raise ``ValueError`` if fabric_name is not set. + - Raise ``ValueError`` if serial_number is not set. + - self.ticket_id is mandatory if Change Control is enabled. """ - return self.path_fabric_name_template_name + _path = self.path_fabric_name_serial_number + _path += "/maintenance-mode" + if self.ticket_id: + _path += f"?ticketId={self.ticket_id}" + return _path @property def verb(self): - return "PUT" + """ + - Return the verb for the endpoint. + - verb: POST + """ + return "POST" -class EpFabrics(Fabrics): +class EpMaintenanceModeDisable(Fabrics): """ - ## V1 API - Fabrics().EpFabrics() + ## V1 API - Fabrics().EpMaintenanceModeDisable() ### Description - Return the endpoint to query fabrics. + Return endpoint to remove switch from maintenance mode + (i.e. enable normal mode). ### Raises - - None + - ``ValueError``: If ``fabric_name`` is not set. + - ``ValueError``: If ``fabric_name`` is invalid. + - ``ValueError``: If ``serial_number`` is not set. + - ``ValueError``: If ``ticket_id`` is not a string. ### Path - - ``/api/v1/lan-fabric/rest/control/fabrics`` + - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode`` + - ``/fabrics/{fabric_name}/switches/{serial_number}/maintenance-mode?ticketId={ticket_id}`` ### Verb - - GET + - DELETE ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - serial_number: string + - set the switch ``serial_number`` to be used in the path + - required + - ticket_id: string + - optional unless Change Control is enabled - path: retrieve the path for the endpoint - verb: retrieve the verb for the endpoint ### Usage ```python - instance = EpFabrics() + instance = EpMaintenanceModeDisable() + instance.fabric_name = "MyFabric" + instance.serial_number = "CHM1234567" + instance.ticket_id = "MyTicket1234" path = instance.path verb = instance.verb ``` @@ -894,15 +896,30 @@ def __init__(self): super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._build_properties() + self.required_properties.add("fabric_name") + self.required_properties.add("serial_number") msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." msg += f"Fabrics.{self.class_name}" self.log.debug(msg) - def _build_properties(self): - super()._build_properties() - self.properties["verb"] = "GET" - @property def path(self): - return self.fabrics + """ + - Path for maintenance-mode disable + - Raise ``ValueError`` if fabric_name is not set. + - Raise ``ValueError`` if serial_number is not set. + - self.ticket_id is mandatory if Change Control is enabled. + """ + _path = self.path_fabric_name_serial_number + _path += "/maintenance-mode" + if self.ticket_id: + _path += f"?ticketId={self.ticket_id}" + return _path + + @property + def verb(self): + """ + - Return the endpoint verb. + - verb: DELETE + """ + return "DELETE" From 23f0d24563c3a71823f406564e037b902187d12a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 26 May 2024 13:17:11 -1000 Subject: [PATCH 084/230] Rename api unit test files to reflect the endpoint path Naming the files to directly match the endpoint path. This should make it easier to identify the corrent file to modify in the future. --- ...tes.py => test_api_v1_configtemplate_rest_config_templates.py} | 0 ...mage_mgnt.py => test_api_v1_imagemanagement_rest_imagemgnt.py} | 0 ...ade_ep.py => test_api_v1_imagemanagement_rest_imageupgrade.py} | 0 ...icy_mgnt.py => test_api_v1_imagemanagement_rest_policymgnt.py} | 0 ...t.py => test_api_v1_imagemanagement_rest_stagingmanagement.py} | 0 ..._fabrics.py => test_api_v1_lan_fabric_rest_control_fabrics.py} | 0 ...witches.py => test_api_v1_lan_fabric_rest_control_switches.py} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename tests/unit/module_utils/common/api/{test_v1_api_templates.py => test_api_v1_configtemplate_rest_config_templates.py} (100%) rename tests/unit/module_utils/common/api/{test_v1_api_image_mgnt.py => test_api_v1_imagemanagement_rest_imagemgnt.py} (100%) rename tests/unit/module_utils/common/api/{test_v1_api_image_upgrade_ep.py => test_api_v1_imagemanagement_rest_imageupgrade.py} (100%) rename tests/unit/module_utils/common/api/{test_v1_api_policy_mgnt.py => test_api_v1_imagemanagement_rest_policymgnt.py} (100%) rename tests/unit/module_utils/common/api/{test_v1_api_staging_management.py => test_api_v1_imagemanagement_rest_stagingmanagement.py} (100%) rename tests/unit/module_utils/common/api/{test_v1_api_fabrics.py => test_api_v1_lan_fabric_rest_control_fabrics.py} (100%) rename tests/unit/module_utils/common/api/{test_v1_api_switches.py => test_api_v1_lan_fabric_rest_control_switches.py} (100%) diff --git a/tests/unit/module_utils/common/api/test_v1_api_templates.py b/tests/unit/module_utils/common/api/test_api_v1_configtemplate_rest_config_templates.py similarity index 100% rename from tests/unit/module_utils/common/api/test_v1_api_templates.py rename to tests/unit/module_utils/common/api/test_api_v1_configtemplate_rest_config_templates.py diff --git a/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_imagemgnt.py similarity index 100% rename from tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py rename to tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_imagemgnt.py diff --git a/tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_imageupgrade.py similarity index 100% rename from tests/unit/module_utils/common/api/test_v1_api_image_upgrade_ep.py rename to tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_imageupgrade.py diff --git a/tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_policymgnt.py similarity index 100% rename from tests/unit/module_utils/common/api/test_v1_api_policy_mgnt.py rename to tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_policymgnt.py diff --git a/tests/unit/module_utils/common/api/test_v1_api_staging_management.py b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_stagingmanagement.py similarity index 100% rename from tests/unit/module_utils/common/api/test_v1_api_staging_management.py rename to tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_stagingmanagement.py diff --git a/tests/unit/module_utils/common/api/test_v1_api_fabrics.py b/tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_fabrics.py similarity index 100% rename from tests/unit/module_utils/common/api/test_v1_api_fabrics.py rename to tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_fabrics.py diff --git a/tests/unit/module_utils/common/api/test_v1_api_switches.py b/tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_switches.py similarity index 100% rename from tests/unit/module_utils/common/api/test_v1_api_switches.py rename to tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_switches.py From 65361e1af1ea11281a6e99314f490e0891e44d7b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 26 May 2024 13:38:39 -1000 Subject: [PATCH 085/230] MaintenanceMode(): Various cleanup, more... 1. MaintenanceMode().verify_config_parameters(): combine calls into single try-except block. 2, MaintenanceMode().change_system_mode(): combine calls into single try-except block. 3. MaintenanceMode().deploy_switches(): combine calls into single try-except block. 4. Remove commented code. 5. Update docstrings for several methods to indicate more precisely what exceptions are raised and for what reasons. --- .../module_utils/common/maintenance_mode.py | 159 ++++-------------- 1 file changed, 37 insertions(+), 122 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index c12181a87..561a0f1c5 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -42,6 +42,11 @@ class MaintenanceMode: - ``ValueError`` in the following properties: - ``config`` if config contains invalid content. + - ``rest_send`` if value is not an instance of RestSend. + - ``results`` if value is not an instance of Results. + - ``commit`` if config, rest_send, or results are not set. + - ``commit`` if ``EpMaintenanceModeEnable`` or + ``EpMaintenanceModeDisable`` raise ``ValueError``. - ``ControllerResponseError`` in the following methods: - ``commit`` if controller response != 200. @@ -149,91 +154,6 @@ def _init_properties(self): self._properties["rest_send"] = None self._properties["results"] = None - # def _can_fabric_be_deployed(self) -> None: - # """ - # - Set self.fabric_can_be_deployed to True if the fabric configuration - # can be deployed. - # - Set self.fabric_can_be_deployed to False otherwise. - # """ - # method_name = inspect.stack()[0][3] - - # self.fabric_can_be_deployed = False - - # deploy = self.payload.get("DEPLOY", None) - # if deploy is False or deploy is None: - # msg = f"Fabric {self.fabric_name} DEPLOY is False or None. " - # msg += "Skipping config-deploy." - # self.log.debug(msg) - # self.cannot_perform_action_reason = msg - # self.fabric_can_be_deployed = False - # self.action_failed = False - # return - - # try: - # self.fabric_summary.fabric_name = self.fabric_name - # except ValueError as error: - # msg = f"Fabric {self.fabric_name} is invalid. " - # msg += "Cannot deploy fabric. " - # msg += f"Error detail: {error}" - # self.log.debug(msg) - # self.cannot_perform_action_reason = msg - # self.fabric_can_be_deployed = False - # self.action_failed = True - # return - - # try: - # self.fabric_summary.refresh() - # except (ControllerResponseError, ValueError) as error: - # msg = f"{self.class_name}.{method_name}: " - # msg += "Error during FabricSummary().refresh(). " - # msg += f"Error detail: {error}" - # self.cannot_perform_action_reason = msg - # self.fabric_can_be_deployed = False - # self.action_failed = True - # return - - # if self.fabric_summary.fabric_is_empty is True: - # msg = f"Fabric {self.fabric_name} is empty. " - # msg += "Cannot deploy an empty fabric." - # self.log.debug(msg) - # self.cannot_perform_action_reason = msg - # self.fabric_can_be_deployed = False - # self.action_failed = False - # return - - # try: - # self.fabric_details.refresh() - # except ValueError as error: - # msg = f"{self.class_name}.{method_name}: " - # msg += "Error during FabricDetailsByName().refresh(). " - # msg += f"Error detail: {error}" - # self.cannot_perform_action_reason = msg - # self.fabric_can_be_deployed = False - # self.action_failed = True - # return - - # self.fabric_details.filter = self.fabric_name - - # if self.fabric_details.deployment_freeze is True: - # msg = f"Fabric {self.fabric_name} DEPLOYMENT_FREEZE == True. " - # msg += "Cannot deploy a fabric with deployment freeze enabled." - # self.log.debug(msg) - # self.cannot_perform_action_reason = msg - # self.fabric_can_be_deployed = False - # self.action_failed = False - # return - - # if self.fabric_details.is_read_only is True: - # msg = f"Fabric {self.fabric_name} IS_READ_ONLY == True. " - # msg += "Cannot deploy a read only fabric." - # self.log.debug(msg) - # self.cannot_perform_action_reason = msg - # self.fabric_can_be_deployed = False - # self.action_failed = False - # return - - # self.fabric_can_be_deployed = True - def verify_config_parameters(self, value): """ Verify that required parameters are present in config. @@ -375,7 +295,10 @@ def commit(self): - ``ValueError`` if ``config`` is not set. - ``ValueError`` if ``rest_send`` is not set. - ``ValueError`` if ``results`` is not set. - - ``ControllerResponseError`` if controller response != 200. + - ``ValueError`` for any exception raised by + - ``verify_commit_parameters()`` + - ``change_system_mode()`` + - ``deploy_switches()`` """ try: self.verify_commit_parameters() @@ -384,14 +307,8 @@ def commit(self): try: self.change_system_mode() - except ControllerResponseError as error: - raise ControllerResponseError(error) from error - except ValueError as error: - raise ValueError(error) from error - - try: self.deploy_switches() - except ControllerResponseError as error: + except (ControllerResponseError, ValueError, TypeError) as error: raise ValueError(error) from error def change_system_mode(self): @@ -401,6 +318,8 @@ def change_system_mode(self): ### Raises - ``ControllerResponseError`` if controller response != 200. + - ``ValueError`` if ``fabric_name`` is invalid. + - ``TypeError`` if ``serial_number`` is not a string. """ method_name = inspect.stack()[0][3] @@ -411,15 +330,22 @@ def change_system_mode(self): ip_address = item.get("ip_address") serial_number = item.get("serial_number") if mode == "normal": - instance = self.ep_maintenance_mode_disable + endpoint = self.ep_maintenance_mode_disable else: - instance = self.ep_maintenance_mode_enable - instance.fabric_name = fabric_name - instance.serial_number = serial_number + endpoint = self.ep_maintenance_mode_enable + + try: + endpoint.fabric_name = fabric_name + endpoint.serial_number = serial_number + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error resolving endpoint: " + msg += f"Error details: {error}." + raise ValueError(msg) from error # Send request - self.rest_send.path = instance.path - self.rest_send.verb = instance.verb + self.rest_send.path = endpoint.path + self.rest_send.verb = endpoint.verb self.rest_send.payload = None self.rest_send.commit() @@ -517,19 +443,26 @@ def deploy_switches(self): ### Raises - ``ControllerResponseError`` if controller response != 200. + - ``ValueError`` if endpoint cannot be resolved. """ method_name = inspect.stack()[0][3] self.build_deploy_dict() self.build_serial_number_to_ip_address() - ep_deploy = EpFabricConfigDeploy() + endpoint = EpFabricConfigDeploy() for fabric_name, serial_numbers in self.deploy_dict.items(): # Build endpoint - ep_deploy.fabric_name = fabric_name - ep_deploy.switch_id = serial_numbers + try: + endpoint.fabric_name = fabric_name + endpoint.switch_id = serial_numbers + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error resolving endpoint: " + msg += f"Error details: {error}." + raise ValueError(msg) from error # Send request - self.rest_send.path = ep_deploy.path - self.rest_send.verb = ep_deploy.verb + self.rest_send.path = endpoint.path + self.rest_send.verb = endpoint.verb self.rest_send.payload = None self.rest_send.commit() @@ -564,24 +497,6 @@ def deploy_switches(self): msg += f"Got response {self.results.response_current}" raise ControllerResponseError(msg) - # Use this if we cannot update maintenance mode in frozen fabrics - # self._can_fabric_be_deployed() - # if self.fabric_can_be_deployed is False: - # self.results.diff_current = {} - # self.results.action = self.action - # self.results.check_mode = self.check_mode - # self.results.state = self.state - # self.results.response_current = { - # "RETURN_CODE": 200, - # "MESSAGE": self.cannot_perform_action_reason, - # } - # if self.action_failed is True: - # self.results.result_current = {"changed": False, "success": False} - # else: - # self.results.result_current = {"changed": True, "success": True} - # self.results.register_task_result() - # return - @property def config(self): """ From e3cc0ad353d5f5d5cb26d3f734fd988b24be7d73 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 26 May 2024 14:02:43 -1000 Subject: [PATCH 086/230] Harden error handling Common() raises ValueError if params does not contain required keys or if the value of required keys are None. Merge()__init__() and Query().__init__() need to catch this. Also, in main() move Merge() and Query() class instantiation into the try-except block. --- plugins/modules/dcnm_maintenance_mode.py | 41 ++++++++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index fac0a2a5e..0f529cc68 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -514,12 +514,24 @@ def ansible_module(self, value): class Merged(Common): """ Handle merged state + + ### Raises + - ``ValueError`` if Common().__init__() raises ``ValueError`` """ def __init__(self, params): + """ + ### Raises + - ``ValueError`` if Common().__init__() raises ``ValueError`` + """ self.class_name = self.__class__.__name__ - super().__init__(params) - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] + try: + super().__init__(params) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"Error: {error}" + raise ValueError(msg) from error self.log = logging.getLogger(f"dcnm.{self.class_name}") self.fabric_details = FabricDetailsByName(self.params) @@ -764,11 +776,26 @@ def send_need(self) -> None: class Query(Common): """ Handle query state + + ### Raises + - ``ValueError`` if Common().__init__() raises ``ValueError`` + - ``ValueError`` if get_want() raises ``ValueError`` + - ``ValueError`` if get_have() raises ``ValueError`` """ def __init__(self, params): + """ + ### Raises + - ``ValueError`` if Common().__init__() raises ``ValueError`` + """ self.class_name = self.__class__.__name__ - super().__init__(params) + method_name = inspect.stack()[0][3] + try: + super().__init__(params) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"Error: {error}" + raise ValueError(msg) from error self.log = logging.getLogger(f"dcnm.{self.class_name}") self.fabric_details = FabricDetailsByName(self.params) @@ -950,17 +977,17 @@ def main(): ansible_module.params["check_mode"] = ansible_module.check_mode if ansible_module.params["state"] == "merged": - task = Merged(ansible_module.params) - task.ansible_module = ansible_module try: + task = Merged(ansible_module.params) + task.ansible_module = ansible_module task.commit() except ValueError as error: ansible_module.fail_json(f"{error}", **task.results.failed_result) elif ansible_module.params["state"] == "query": - task = Query(ansible_module.params) - task.ansible_module = ansible_module try: + task = Query(ansible_module.params) + task.ansible_module = ansible_module task.commit() except ValueError as error: ansible_module.fail_json(f"{error}", **task.results.failed_result) From 843ff7aaae1c38a13cff38d0b8f321e4ce681cb1 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 26 May 2024 16:30:20 -1000 Subject: [PATCH 087/230] MaintenanceMode: initial unit tests --- .../module_utils/common/maintenance_mode.py | 20 +- .../unit/module_utils/common/common_utils.py | 36 +- .../fixtures/responses_ConfigDeploy.json | 11 + .../fixtures/responses_MaintenanceMode.json | 12 + .../common/test_maintenance_mode.py | 410 ++++++++++++++++++ 5 files changed, 477 insertions(+), 12 deletions(-) create mode 100644 tests/unit/module_utils/common/fixtures/responses_ConfigDeploy.json create mode 100644 tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json create mode 100644 tests/unit/module_utils/common/test_maintenance_mode.py diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 561a0f1c5..428101e92 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -109,25 +109,23 @@ class MaintenanceMode: def __init__(self, params): self.class_name = self.__class__.__name__ + method_name = inspect.stack()[0][3] self.log = logging.getLogger(f"dcnm.{self.class_name}") self.params = params self.action = "maintenance_mode" - self.cannot_perform_action_reason = "" - self.action_failed = False - self.fabric_can_be_deployed = False self.check_mode = self.params.get("check_mode", None) if self.check_mode is None: - msg = f"{self.class_name}.__init__(): " - msg += "params is missing mandatory check_mode parameter." + msg = f"{self.class_name}.{method_name}: " + msg += "params is missing mandatory parameter: check_mode." raise ValueError(msg) self.state = self.params.get("state", None) if self.state is None: - msg = f"{self.class_name}.__init__(): " - msg += "params is missing mandatory state parameter." + msg = f"{self.class_name}.{method_name}: " + msg += "params is missing mandatory parameter: state." raise ValueError(msg) # Populated in build_deploy_dict() @@ -135,8 +133,6 @@ def __init__(self, params): self.serial_number_to_ip_address = {} self.valid_modes = ["maintenance", "normal"] - self.path = None - self.verb = None self._init_properties() self.conversion = ConversionUtils() @@ -347,8 +343,14 @@ def change_system_mode(self): self.rest_send.path = endpoint.path self.rest_send.verb = endpoint.verb self.rest_send.payload = None + msg = f"ZZZ: {self.class_name}.{method_name}: HERE" + self.log.debug(msg) self.rest_send.commit() + msg = f"ZZZ: {self.class_name}.{method_name}: " + msg += f"rest_send.response_current: {self.rest_send.response_current}" + self.log.debug(msg) + # Update diff result = self.rest_send.result_current["success"] if result is False: diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index 70db881ce..1d9520883 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -28,6 +28,8 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.common.controller_version import \ ControllerVersion from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log +from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import \ + MaintenanceMode from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ @@ -95,7 +97,7 @@ class MockAnsibleModule: supports_check_mode = True @staticmethod - def fail_json(msg) -> AnsibleFailJson: + def fail_json(msg, **kwargs) -> AnsibleFailJson: """ mock the fail_json method """ @@ -135,6 +137,14 @@ def log_fixture(): return Log(MockAnsibleModule) +@pytest.fixture(name="maintenance_mode") +def maintenance_mode_fixture(): + """ + return MaintenanceMode + """ + return MaintenanceMode(params) + + @pytest.fixture(name="merge_dicts") def merge_dicts_fixture(): """ @@ -169,9 +179,19 @@ def merge_dicts_data(key: str) -> Dict[str, str]: return data +def responses_config_deploy(key: str) -> Dict[str, str]: + """ + Return data in responses_ConfigDeploy.json + """ + response_file = "responses_ConfigDeploy" + response = load_fixture(response_file).get(key) + print(f"responses_config_deploy: {key} : {response}") + return response + + def responses_controller_features(key: str) -> Dict[str, str]: """ - Return ControllerFeatures controller responses + Return data in responses_ControllerFeatures.json """ response_file = "responses_ControllerFeatures" response = load_fixture(response_file).get(key) @@ -180,9 +200,19 @@ def responses_controller_features(key: str) -> Dict[str, str]: def responses_controller_version(key: str) -> Dict[str, str]: """ - Return ControllerVersion controller responses + Return data in responses_ControllerVersion.json """ response_file = "responses_ControllerVersion" response = load_fixture(response_file).get(key) print(f"responses_controller_version: {key} : {response}") return response + + +def responses_maintenance_mode(key: str) -> Dict[str, str]: + """ + Return data in responses_MaintenanceMode.json + """ + response_file = "responses_MaintenanceMode" + response = load_fixture(response_file).get(key) + print(f"responses_maintenance_mode: {key} : {response}") + return response diff --git a/tests/unit/module_utils/common/fixtures/responses_ConfigDeploy.json b/tests/unit/module_utils/common/fixtures/responses_ConfigDeploy.json new file mode 100644 index 000000000..415500b08 --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/responses_ConfigDeploy.json @@ -0,0 +1,11 @@ +{ + "test_maintenance_mode_00120a": { + "DATA": { + "status": "Configuration deployment completed." + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_Fabric/config-deploy/FDO22180ASJ?forceShowRun=False", + "RETURN_CODE": 200 + } +} \ No newline at end of file diff --git a/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json b/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json new file mode 100644 index 000000000..ab09b5e92 --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json @@ -0,0 +1,12 @@ +{ + "test_maintenance_mode_00120a": { + "DATA": { + "status": "Success" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_Fabric/switches/FDO22180ASJ/maintenance-mode", + "RETURN_CODE": 200, + "sequence_number": 1 + } +} \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py new file mode 100644 index 000000000..3d7756af4 --- /dev/null +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -0,0 +1,410 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-import +# pylint: disable=redefined-outer-name +# pylint: disable=protected-access +# pylint: disable=unused-argument +# pylint: disable=invalid-name + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import inspect + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import ( + EpMaintenanceModeDisable, EpMaintenanceModeEnable) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError +from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import \ + MaintenanceMode +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ + RestSend +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + MockAnsibleModule, ResponseGenerator, does_not_raise, + maintenance_mode_fixture, params, responses_config_deploy, + responses_maintenance_mode) + +FABRIC_NAME = "VXLAN_Fabric" +CONFIG = [ + { + "deploy": False, + "fabric_name": f"{FABRIC_NAME}", + "ip_address": "192.168.1.2", + "mode": "maintenance", + "serial_number": "FDO22180ASJ", + } +] + + +def test_maintenance_mode_00000(maintenance_mode) -> None: + """ + Classes and Methods + - MaintenanceMode + - __init__() + + Test + - Class attributes are initialized to expected values + - Exception is not raised + """ + with does_not_raise(): + instance = maintenance_mode + assert instance._properties["config"] is None + assert instance._properties["rest_send"] is None + assert instance._properties["results"] is None + assert instance.action == "maintenance_mode" + assert instance.class_name == "MaintenanceMode" + assert instance.config is None + assert instance.check_mode is False + assert instance.deploy_dict == {} + assert instance.serial_number_to_ip_address == {} + assert instance.valid_modes == ["maintenance", "normal"] + assert instance.state == "merged" + assert instance.rest_send is None + assert instance.results is None + assert isinstance(instance.conversion, ConversionUtils) + assert isinstance(instance.ep_maintenance_mode_disable, EpMaintenanceModeDisable) + assert isinstance(instance.ep_maintenance_mode_enable, EpMaintenanceModeEnable) + + +def test_maintenance_mode_00010() -> None: + """ + Classes and Methods + - MaintenanceMode + - __init__() + + Test + - ``ValueError`` is raised when params is missing check_mode key. + """ + params = {"state": "merged"} + match = r"MaintenanceMode\.__init__:\s+" + match += r"params is missing mandatory parameter: check_mode\." + with pytest.raises(ValueError, match=match): + instance = MaintenanceMode(params) # pylint: disable=unused-variable + + +def test_maintenance_mode_00020() -> None: + """ + Classes and Methods + - MaintenanceMode + - __init__() + + Test + - ``ValueError`` is raised when params is missing state key. + """ + params = {"check_mode": False} + match = r"MaintenanceMode\.__init__:\s+" + match += r"params is missing mandatory parameter: state\." + with pytest.raises(ValueError, match=match): + instance = MaintenanceMode(params) # pylint: disable=unused-variable + + +def test_maintenance_mode_00030(maintenance_mode) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - commit() + + Summary + - Verify MaintenanceMode().commit() raises ``ValueError`` when + ``config`` is not set. + + Code Flow - Setup + - MaintenanceMode() is instantiated + - Other required attributes are set + + Code Flow - Test + - ``MaintenanceMode().commit()`` is called without having first set + ``MaintenanceMode().config`` + + Expected Result + - ``ValueError`` is raised + - Exception message matches expected + """ + with does_not_raise(): + instance = maintenance_mode + instance.rest_send = RestSend(MockAnsibleModule()) + instance.results = Results() + + match = r"MaintenanceMode\.verify_commit_parameters: " + match += r"MaintenanceMode\.config must be set before calling\s+" + match += r"commit\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_maintenance_mode_00040(maintenance_mode) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - commit() + + Summary + - Verify MaintenanceMode().commit() raises ``ValueError`` + when ``rest_send`` is not set. + + Code Flow - Setup + - MaintenanceMode() is instantiated + - Other required attributes are set + + Code Flow - Test + - MaintenanceMode().commit() is called without having + first set MaintenanceMode().rest_send + + Expected Result + - ``ValueError`` is raised + - Exception message matches expected + """ + with does_not_raise(): + instance = maintenance_mode + instance.results = Results() + instance.config = CONFIG + + match = r"MaintenanceMode\.verify_commit_parameters: " + match += r"MaintenanceMode\.rest_send must be set before calling\s+" + match += r"commit\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_maintenance_mode_00050(maintenance_mode) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - commit() + + Summary + - Verify MaintenanceMode().commit() raises ``ValueError`` + when ``MaintenanceMode().results`` is not set. + + Code Flow - Setup + - MaintenanceMode() is instantiated + - Other required attributes are set + + Code Flow - Test + - MaintenanceMode().commit() is called without having + first set MaintenanceMode().results + + Expected Result + - ``ValueError`` is raised + - Exception message matches expected + """ + with does_not_raise(): + instance = maintenance_mode + instance.rest_send = RestSend(MockAnsibleModule) + instance.config = CONFIG + + match = r"MaintenanceMode\.verify_commit_parameters: " + match += r"MaintenanceMode\.results must be set before calling\s+" + match += r"commit\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +@pytest.mark.parametrize( + "mock_exception, expected_exception, mock_message", + [ + (ControllerResponseError, ValueError, "Bad controller response"), + (TypeError, ValueError, "Bad type"), + (ValueError, ValueError, "Bad value"), + ], +) +def test_maintenance_mode_00100( + monkeypatch, maintenance_mode, mock_exception, expected_exception, mock_message +) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - commit() + + Summary + - Verify MaintenanceMode().commit() raises ``ValueError`` when + ``MaintenanceMode().change_system_mode`` raises any of: + - ``ControllerResponseError`` + - ``TypeError`` + - ``ValueError`` + + + Code Flow - Setup + - MaintenanceMode() is instantiated + - Required attributes are set + - change_system_mode() is mocked to raise each of the above exceptions + + Code Flow - Test + - MaintenanceMode().commit() is called for each exception + + Expected Result + - ``ValueError`` is raised + - Exception message matches expected + """ + + def mock_change_system_mode(*args, **kwargs): + raise mock_exception(mock_message) + + with does_not_raise(): + instance = maintenance_mode + instance.config = CONFIG + instance.rest_send = RestSend(MockAnsibleModule) + instance.results = Results() + + monkeypatch.setattr(instance, "change_system_mode", mock_change_system_mode) + with pytest.raises(expected_exception, match=mock_message): + instance.commit() + + +@pytest.mark.parametrize( + "mock_exception, expected_exception, mock_message", + [ + (ControllerResponseError, ValueError, "Bad controller response"), + (ValueError, ValueError, "Bad value"), + ], +) +def test_maintenance_mode_00110( + monkeypatch, maintenance_mode, mock_exception, expected_exception, mock_message +) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - commit() + + Summary + - Verify MaintenanceMode().commit() raises ``ValueError`` when + ``MaintenanceMode().change_system_mode`` raises any of: + - ``ControllerResponseError`` + - ``ValueError`` + + + Code Flow - Setup + - MaintenanceMode() is instantiated + - Required attributes are set + - change_system_mode() is mocked to do nothing + - deploy_switches() is mocked to raise each of the above exceptions + + Code Flow - Test + - MaintenanceMode().commit() is called for each exception + + Expected Result + - ``ValueError`` is raised + - Exception message matches expected + """ + + def mock_change_system_mode(*args, **kwargs): + pass + + def mock_deploy_switches(*args, **kwargs): + raise mock_exception(mock_message) + + with does_not_raise(): + instance = maintenance_mode + instance.config = CONFIG + instance.rest_send = RestSend(MockAnsibleModule) + instance.results = Results() + + monkeypatch.setattr(instance, "change_system_mode", mock_change_system_mode) + monkeypatch.setattr(instance, "deploy_switches", mock_deploy_switches) + with pytest.raises(expected_exception, match=mock_message): + instance.commit() + + +def test_maintenance_mode_00120(monkeypatch, maintenance_mode) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - commit() + - change_system_mode() + - deploy_switches() + + Summary + - Verify commit() success case: + - RETURN_CODE is 200. + - Controller response contains expected structure and values. + + Code Flow - Setup + - MaintenanceMode() is instantiated + - dcnm_send() is patched to return the mocked controller responses + - Required attributes are set + - MaintenanceMode().commit() is called + - responses_MaintenanceMode contains a dict with: + - RETURN_CODE == 200 + - DATA == {"status": "Success"} + + Code Flow - Test + - MaintenanceMode().commit() is called + + Expected Result + - Exception is not raised + - instance.response_data returns expected data + - MaintenanceMode()._properties are updated + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." + PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + + def responses(): + yield responses_maintenance_mode(key) + yield responses_config_deploy(key) + + gen = ResponseGenerator(responses()) + + def mock_dcnm_send(*args, **kwargs): + item = gen.next + return item + + with does_not_raise(): + instance = maintenance_mode + instance.rest_send = RestSend(MockAnsibleModule()) + instance.rest_send.unit_test = True + instance.rest_send.timeout = 1 + instance.results = Results() + instance.config = CONFIG + + monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + + with does_not_raise(): + instance.commit() + + assert isinstance(instance.results.response, list) + assert isinstance(instance.results.result, list) + value = "Success" + assert instance.results.response[0].get("DATA", {}).get("status") == value + assert instance.results.response[0].get("MESSAGE", None) == "OK" + assert instance.results.response[0].get("RETURN_CODE", None) == 200 + assert instance.results.response[0].get("METHOD", None) == "POST" + value = "Configuration deployment completed." + assert instance.results.response[1].get("DATA", {}).get("status") == value + assert instance.results.response[1].get("MESSAGE", None) == "OK" + assert instance.results.response[1].get("RETURN_CODE", None) == 200 + assert instance.results.response[1].get("METHOD", None) == "POST" + assert instance.results.result[0].get("success", None) is True From 9f43e8531698a1d841964bdba86306f5039995aa Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 26 May 2024 16:49:21 -1000 Subject: [PATCH 088/230] test_maintenance_mode_00120: update, more... 1. test_maintenance_mode_00120: update missing checks 2. MaintenanceMode(): remove temporary debug statements --- .../module_utils/common/maintenance_mode.py | 6 ---- .../common/test_maintenance_mode.py | 28 +++++++++++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 428101e92..cc5f2e53f 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -343,14 +343,8 @@ def change_system_mode(self): self.rest_send.path = endpoint.path self.rest_send.verb = endpoint.verb self.rest_send.payload = None - msg = f"ZZZ: {self.class_name}.{method_name}: HERE" - self.log.debug(msg) self.rest_send.commit() - msg = f"ZZZ: {self.class_name}.{method_name}: " - msg += f"rest_send.response_current: {self.rest_send.response_current}" - self.log.debug(msg) - # Update diff result = self.rest_send.result_current["success"] if result is False: diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 3d7756af4..7fce8b29d 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -395,16 +395,40 @@ def mock_dcnm_send(*args, **kwargs): with does_not_raise(): instance.commit() + assert isinstance(instance.results.diff, list) + assert isinstance(instance.results.metadata, list) assert isinstance(instance.results.response, list) assert isinstance(instance.results.result, list) - value = "Success" - assert instance.results.response[0].get("DATA", {}).get("status") == value + assert instance.results.diff[0].get("fabric_name", None) == FABRIC_NAME + assert instance.results.diff[0].get("ip_address", None) == "192.168.1.2" + assert instance.results.diff[0].get("maintenance_mode", None) == "maintenance" + assert instance.results.diff[0].get("sequence_number", None) == 1 + assert instance.results.diff[0].get("serial_number", None) == "FDO22180ASJ" + + assert instance.results.diff[1].get("config_deploy", None) is True + assert instance.results.diff[1].get("sequence_number", None) == 2 + + assert instance.results.metadata[0].get("action", None) == "maintenance_mode" + assert instance.results.metadata[0].get("sequence_number", None) == 1 + assert instance.results.metadata[0].get("state", None) == "merged" + + assert instance.results.metadata[1].get("action", None) == "config_deploy" + assert instance.results.metadata[1].get("sequence_number", None) == 2 + assert instance.results.metadata[1].get("state", None) == "merged" + + assert instance.results.response[0].get("DATA", {}).get("status") == "Success" assert instance.results.response[0].get("MESSAGE", None) == "OK" assert instance.results.response[0].get("RETURN_CODE", None) == 200 assert instance.results.response[0].get("METHOD", None) == "POST" + value = "Configuration deployment completed." assert instance.results.response[1].get("DATA", {}).get("status") == value assert instance.results.response[1].get("MESSAGE", None) == "OK" assert instance.results.response[1].get("RETURN_CODE", None) == 200 assert instance.results.response[1].get("METHOD", None) == "POST" + + assert instance.results.result[0].get("changed", None) is True assert instance.results.result[0].get("success", None) is True + + assert instance.results.result[1].get("changed", None) is True + assert instance.results.result[1].get("success", None) is True From 2dfaa49b79bf624e8aa8ee8c781ac84640954420 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 27 May 2024 16:03:14 -1000 Subject: [PATCH 089/230] Deprecate common classes requiring AnsibleModule 1. Deprecate the following common classes that have AnsibleModule dependency. - MergeDicts() - merge_dicts.py - ParamsMergeDefaults() - params_merge_defaults.py - ParamsValidate() - params_validate.py 2. Add versions of the above that are not dependent on AnsibleModule. - MergeDicts() - merge_dicts_v2.py - ParamsMergeDefaults() - params_merge_defaults_v2.py - ParamsValidate() - params_validate_v2.py 3. Copied v1 unit tests and modified for the v2 versions. Over time, modules using the deprecated versions (dcnm_image_upgrade, dcnm_image_policy, dcnm_fabric) can be transitioned to the v2 versions. MaintenanceMode() is now using the v2 versions. --- .../module_utils/common/maintenance_mode.py | 5 +- plugins/module_utils/common/merge_dicts.py | 4 + plugins/module_utils/common/merge_dicts_v2.py | 173 ++++ .../common/params_merge_defaults.py | 4 + .../common/params_merge_defaults_v2.py | 205 ++++ .../module_utils/common/params_validate.py | 3 + .../module_utils/common/params_validate_v2.py | 701 ++++++++++++++ plugins/modules/dcnm_maintenance_mode.py | 504 +++++++--- .../unit/module_utils/common/common_utils.py | 32 +- .../common/fixtures/merge_dicts_v2.json | 147 +++ .../common/test_merge_dicts_v2.py | 375 ++++++++ .../common/test_params_validate_v2.py | 880 ++++++++++++++++++ 12 files changed, 2901 insertions(+), 132 deletions(-) create mode 100644 plugins/module_utils/common/merge_dicts_v2.py create mode 100644 plugins/module_utils/common/params_merge_defaults_v2.py create mode 100644 plugins/module_utils/common/params_validate_v2.py create mode 100644 tests/unit/module_utils/common/fixtures/merge_dicts_v2.json create mode 100644 tests/unit/module_utils/common/test_merge_dicts_v2.py create mode 100644 tests/unit/module_utils/common/test_params_validate_v2.py diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index cc5f2e53f..2061e6331 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -235,11 +235,12 @@ def verify_mode(self, item): method_name = inspect.stack()[0][3] if item.get("mode", None) is None: msg = f"{self.class_name}.{method_name}: " - msg += "mode must be present in config." + msg += "mode is mandatory, but is missing from the config." raise ValueError(msg) if item.get("mode", None) not in self.valid_modes: msg = f"{self.class_name}.{method_name}: " - msg += "mode must be one of 'maintenance' or 'normal'." + msg += f"mode must be one of {' or '.join(self.valid_modes)}. " + msg += f"Got {item.get('mode', None)}." raise ValueError(msg) def verify_serial_number(self, item): diff --git a/plugins/module_utils/common/merge_dicts.py b/plugins/module_utils/common/merge_dicts.py index 561a71afd..f9102f9eb 100644 --- a/plugins/module_utils/common/merge_dicts.py +++ b/plugins/module_utils/common/merge_dicts.py @@ -27,6 +27,10 @@ class MergeDicts: """ + ## DEPRECATED + Use ``MergeDicts`` from ``merge_dicts_v2.py`` for + all new development. + Merge two dictionaries. Given two dictionaries, dict1 and dict2, merge them into a diff --git a/plugins/module_utils/common/merge_dicts_v2.py b/plugins/module_utils/common/merge_dicts_v2.py new file mode 100644 index 000000000..5f7009519 --- /dev/null +++ b/plugins/module_utils/common/merge_dicts_v2.py @@ -0,0 +1,173 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy +import inspect +import logging +from collections.abc import MutableMapping as Map +from typing import Any, Dict + + +class MergeDicts: + """ + ### Summary + Merge two dictionaries. + + Given two dictionaries, dict1 and dict2, merge them into a + single dictionary, dict_merged, where keys in dict2 have + precedence over (will overwrite) keys in dict1. + + ### Raises + - ``TypeError`` if ``dict1`` is not a dictionary. + - ``TypeError`` if ``dict2`` is not a dictionary. + - ``ValueError`` if ``dict1`` has not been set before calling commit() + - ``ValueError`` if ``dict2`` has not been set before calling commit() + - ``ValueError`` if ``dict_merged`` is accessed before calling commit() + + ### Usage + ```python + try: + instance = MergeDicts() + instance.dict1 = { "foo": 1, "bar": 2 } + instance.dict2 = { "foo": 3, "baz": 4 } + instance.commit() + dict_merged = instance.dict_merged + except (TypeError, ValueError) as error: + handle_error(error) + print(dict_merged) + ``` + + ### Output + ```json + { foo: 3, bar: 2, baz: 4 } + ``` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED MergeDicts()") + + self._build_properties() + + def _build_properties(self) -> None: + self.properties = {} + self.properties["dict1"] = None + self.properties["dict2"] = None + self.properties["dict_merged"] = None + + def commit(self) -> None: + """ + ### Summary + Commit the merged dict. + + ### Raises + - ``ValueError`` if ``dict1`` or ``dict2`` has not been set. + """ + method_name = inspect.stack()[0][3] + if self.dict1 is None or self.dict2 is None: + msg = f"{self.class_name}.{method_name}: " + msg += "dict1 and dict2 must be set before calling commit()" + raise ValueError(msg) + + self.properties["dict_merged"] = self.merge_dicts(self.dict1, self.dict2) + + def merge_dicts( + self, dict1: Dict[Any, Any], dict2: Dict[Any, Any] + ) -> Dict[Any, Any]: + """ + Merge dict2 into dict1 and return dict1. + Keys in dict2 have precedence over keys in dict1. + """ + for key in dict2: + if ( + key in dict1 + and isinstance(dict1[key], Map) + and isinstance(dict2[key], Map) + ): + self.merge_dicts(dict1[key], dict2[key]) + else: + dict1[key] = dict2[key] + return copy.deepcopy(dict1) + + @property + def dict_merged(self): + """ + ### Summary + Returns the merged dictionary. + + ### Raises + - ``ValueError`` if ``dict_merged`` is accessed before + ``commit()`` has been called. + """ + method_name = inspect.stack()[0][3] + if self.properties["dict_merged"] is None: + msg = f"{self.class_name}.{method_name}: " + msg += "Call instance.commit() before calling " + msg += f"instance.{method_name}." + raise ValueError(msg) + return self.properties["dict_merged"] + + @property + def dict1(self): + """ + ### Summary + The dictionary into which ``dict2`` will be merged. + + ``dict1``'s keys will be overwritten by ``dict2``'s keys. + + ### Raises + - ``TypeError`` if ``value`` is not a dictionary. + """ + return self.properties["dict1"] + + @dict1.setter + def dict1(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid value. Expected type dict. " + msg += f"Got type {type(value)}." + raise TypeError(msg) + self.properties["dict1"] = copy.deepcopy(value) + + @property + def dict2(self): + """ + ### Summary + The dictionary which will be merged into ``dict1``. + + ``dict2``'s keys will overwrite by ``dict1``'s keys. + + ### Raises + - ``TypeError`` if ``value`` is not a dictionary. + """ + return self.properties["dict2"] + + @dict2.setter + def dict2(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid value. Expected type dict. " + msg += f"Got type {type(value)}." + raise TypeError(msg) + self.properties["dict2"] = copy.deepcopy(value) diff --git a/plugins/module_utils/common/params_merge_defaults.py b/plugins/module_utils/common/params_merge_defaults.py index cd28bc6f1..24c3e0bd7 100644 --- a/plugins/module_utils/common/params_merge_defaults.py +++ b/plugins/module_utils/common/params_merge_defaults.py @@ -27,6 +27,10 @@ class ParamsMergeDefaults: """ + ## DEPRECATED + Use ``ParamsMergeDefaults`` from ``params_merge_defaults_v2.py`` for + all new development. + Merge default parameters into parameters. Given a parameter specification (params_spec) and a playbook config diff --git a/plugins/module_utils/common/params_merge_defaults_v2.py b/plugins/module_utils/common/params_merge_defaults_v2.py new file mode 100644 index 000000000..f26ce2e08 --- /dev/null +++ b/plugins/module_utils/common/params_merge_defaults_v2.py @@ -0,0 +1,205 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy +import inspect +import logging +from collections.abc import MutableMapping as Map +from typing import Any, Dict + + +class ParamsMergeDefaults: + """ + ### Summary + Merge default parameters from ``param_spec`` into parameters. + + Given a parameter specification (``params_spec``) and a playbook config + (``parameters``) merge key/values from ``params_spec`` which have a default + associated with them into ``parameters`` if parameters is missing the + corresponding key/value. + + ### Raises + - ``ValueError`` if ``params_spec`` is None when calling commit(). + - ``TypeError`` if ``parameters`` is not a dict. + - ``TypeError`` if ``params_spec`` is not a dict. + + ### Usage + ```python + instance = ParamsMergeDefaults() + instance.params_spec = params_spec + instance.parameters = parameters + instance.commit() + merged_parameters = instance.merged_parameters + ``` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED ParamsMergeDefaults()") + + self._build_properties() + self._build_reserved_params() + + def _build_properties(self): + """ + Container for the properties of this class. + """ + self.properties = {} + self.properties["params_spec"] = None + self.properties["parameters"] = None + self.properties["merged_parameters"] = None + + def _build_reserved_params(self): + """ + These are reserved parameter names that are skipped + during merge. + """ + self.reserved_params = set() + self.reserved_params.add("choices") + self.reserved_params.add("default") + self.reserved_params.add("length_max") + self.reserved_params.add("no_log") + self.reserved_params.add("range_max") + self.reserved_params.add("range_min") + self.reserved_params.add("required") + self.reserved_params.add("type") + self.reserved_params.add("preferred_type") + + def _merge_default_params( + self, spec: Dict[str, Any], params: Dict[str, Any] + ) -> Dict[str, Any]: + """ + ### Summary + Merge default parameters into parameters. + + ### Callers + - ``commit()`` + + ### Returns + - A modified copy of params where missing parameters are added if: + 1. they are present in spec + 2. they have a default value defined in spec + """ + for spec_key, spec_value in spec.items(): + if spec_key in self.reserved_params: + continue + + if params.get(spec_key, None) is None and "default" not in spec_value: + continue + + if params.get(spec_key, None) is None and "default" in spec_value: + params[spec_key] = spec_value["default"] + + if isinstance(spec_value, Map): + params[spec_key] = self._merge_default_params( + spec_value, params[spec_key] + ) + + return copy.deepcopy(params) + + def commit(self) -> None: + """ + ### Summary + Merge default parameters into parameters and populate + self.merged_parameters. + + ### Raises + - ``ValueError`` if ``params_spec`` is None. + - ``ValueError`` if ``parameters`` is None. + """ + method_name = inspect.stack()[0][3] + + if self.params_spec is None: + msg = f"{self.class_name}.{method_name}: " + msg += "Cannot commit. params_spec is None." + raise ValueError(msg) + + if self.parameters is None: + msg = f"{self.class_name}.{method_name}: " + msg += "Cannot commit. parameters is None." + raise ValueError(msg) + + self.properties["merged_parameters"] = self._merge_default_params( + self.params_spec, self.parameters + ) + + @property + def merged_parameters(self): + """ + ### Summary + Getter for the merged parameters. + + ### Raises + - ``ValueError`` if ``merged_parameters`` is None, + indicating that commit() has not been called. + """ + if self.properties["merged_parameters"] is None: + msg = f"{self.class_name}.merged_parameters: " + msg += "Call instance.commit() before calling merged_parameters." + raise ValueError(msg) + return self.properties["merged_parameters"] + + @property + def parameters(self): + """ + ### Summary + The parameters into which defaults are merged. + + The merge consists of adding any missing parameters + (per a comparison with ``params_spec``) and setting their + value to the default value defined in ``params_spec``. + + ### Raises + - ``TypeError`` if ``parameters`` is not a dict. + """ + return self.properties["parameters"] + + @parameters.setter + def parameters(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid parameters. Expected type dict. " + msg += f"Got type {type(value)}." + raise TypeError(msg) + self.properties["parameters"] = value + + @property + def params_spec(self): + """ + ### Summary + The param specification used to validate the parameters + + ### Raises + - ``TypeError`` if ``params_spec`` is not a dict. + """ + return self.properties["params_spec"] + + @params_spec.setter + def params_spec(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid params_spec. Expected type dict. " + msg += f"Got type {type(value)}." + raise TypeError(msg) + self.properties["params_spec"] = value diff --git a/plugins/module_utils/common/params_validate.py b/plugins/module_utils/common/params_validate.py index 2c064d09d..4e18dd472 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -29,6 +29,9 @@ class ParamsValidate: """ + ## DEPRECATED + Use ``ParamsValidate`` from ``params_validate_v2.py`` for all new development. + ### Summary Validate playbook parameters. diff --git a/plugins/module_utils/common/params_validate_v2.py b/plugins/module_utils/common/params_validate_v2.py new file mode 100644 index 000000000..2ac019ce4 --- /dev/null +++ b/plugins/module_utils/common/params_validate_v2.py @@ -0,0 +1,701 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import inspect +import ipaddress +import logging +from collections.abc import MutableMapping as Map +from typing import Any, List + +from ansible.module_utils.common import validation + + +class ParamsValidate: + """ + ### Summary + Validate playbook parameters. + + ### Mandatory Properties + - ``parameters``: fully-merged dictionary of parameters + - ``params_spec``: Dictionary that describes each parameter + in parameters + + ### Usage + + Assume the following params_spec describing parameters + ``ip_address`` and ``foo`` . + - ``ip_address`` is a required parameter of type ipv4. + - ``foo`` is an optional parameter of type dict. + - ``foo`` contains a parameter named ``bar`` that is an optional + parameter of type str with a default value of bingo. + - ``bar`` can be assigned one of three values: bingo, bango, or bongo. + + ```python + params_spec: Dict[str, Any] = {} + params_spec["ip_address"] = {} + params_spec["ip_address"]["required"] = False + params_spec["ip_address"]["type"] = "ipv4" + params_spec["foo"] = {} + params_spec["foo"]["required"] = False + params_spec["foo"]["type"] = "dict" + params_spec["foo"]["bar"] = {} + params_spec["foo"]["bar"]["required"] = False + params_spec["foo"]["bar"]["type"] = "str" + params_spec["foo"]["bar"]["choices"] = ["bingo", "bango", "bongo"] + params_spec["foo"]["baz"] = {} + params_spec["foo"]["baz"]["required"] = False + params_spec["foo"]["baz"]["type"] = int + params_spec["foo"]["baz"]["range_min"] = 0 + params_spec["foo"]["baz"]["range_max"] = 10 + ``` + + Which describes the following YAML: + + ```yaml + ip_address: 1.2.3.4 + foo: + bar: bingo + baz: 10 + ``` + + ### Invocation + + Where parameters is a dictionary containing the playbook parameters. + Typically this retrieved from ``AnsibleModule`` with + ``AnsibleModule.params``. + + ```python + validator = ParamsValidate() + validator.parameters = AnsibleModule.params + validator.params_spec = params_spec + validator.commit() + ``` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + self.validation = validation + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED ParamsValidate()") + + self._build_properties() + self._build_reserved_params() + self._build_mandatory_param_spec_keys() + self._build_standard_types() + self._build_ipaddress_types() + self._build_valid_expected_types() + self._build_validations() + + def _build_properties(self): + """ + Set default values for the properties in this class + """ + self.properties = {} + self.properties["parameters"] = None + self.properties["params_spec"] = None + + def _build_reserved_params(self): + """ + These are reserved parameter names that are skipped + during validation. + """ + self.reserved_params = set() + self.reserved_params.add("choices") + self.reserved_params.add("default") + self.reserved_params.add("length_max") + self.reserved_params.add("no_log") + self.reserved_params.add("range_max") + self.reserved_params.add("range_min") + self.reserved_params.add("required") + self.reserved_params.add("type") + self.reserved_params.add("preferred_type") + + def _build_standard_types(self): + """ + Standard python types. These are used with + isinstance() since isinstance() requires the + actual type and not the string representation. + """ + self._standard_types = {} + self._standard_types["bool"] = bool + self._standard_types["dict"] = dict + self._standard_types["float"] = float + self._standard_types["int"] = int + self._standard_types["list"] = list + self._standard_types["set"] = set + self._standard_types["str"] = str + self._standard_types["tuple"] = tuple + + def _build_ipaddress_types(self): + """ + IP address types require special handling since + they cannot be verified using isinstance(). + """ + self._ipaddress_types = set() + self._ipaddress_types.add("ipv4") + self._ipaddress_types.add("ipv6") + self._ipaddress_types.add("ipv4_subnet") + self._ipaddress_types.add("ipv6_subnet") + + def _build_mandatory_param_spec_keys(self): + """ + Mandatory keys for every parameter in params_spec. + """ + self.mandatory_param_spec_keys = set() + self.mandatory_param_spec_keys.add("required") + self.mandatory_param_spec_keys.add("type") + + def _build_valid_expected_types(self): + """ + Valid values for the 'type' key in params_spec. + """ + self.valid_expected_types = set(self._standard_types.keys()).union( + self._ipaddress_types + ) + + def _build_validations(self): + """ + Map of validation functions keyed by the parameter + type they validate. + """ + self.validations = {} + self.validations["bool"] = validation.check_type_bool + self.validations["dict"] = validation.check_type_dict + self.validations["float"] = validation.check_type_float + self.validations["int"] = validation.check_type_int + self.validations["list"] = validation.check_type_list + self.validations["set"] = self._validate_set + self.validations["str"] = validation.check_type_str + self.validations["tuple"] = self._validate_tuple + self.validations["ipv4"] = self._validate_ipv4_address + self.validations["ipv6"] = self._validate_ipv6_address + self.validations["ipv4_subnet"] = self._validate_ipv4_subnet + self.validations["ipv6_subnet"] = self._validate_ipv6_subnet + + def commit(self) -> None: + """ + ### Summary + Verify that parameters in self.parameters conform to self.params_spec + + ### Raises + - ``ValueError`` if self.parameters is not set. + - ``ValueError`` if self.params_spec is not set. + - ``ValueError`` if a mandatory parameter is missing. + - ``ValueError`` if a parameter's type is not in the list of + valid types for that parameter. + - ``ValueError`` if a non-integer parameter is using range_min + or range_max. + - ``ValueError`` if a parameter's value is not in the list of + valid choices for that parameter. + - ``ValueError`` if an integer parameter's value is not within the + parameter's valid range. + """ + method_name = inspect.stack()[0][3] + if self.parameters is None: + msg = f"{self.class_name}.{method_name}: " + msg += "instance.parameters needs to be set " + msg += "prior to calling instance.commit()." + raise ValueError(msg) + + if self.params_spec is None: + msg = f"{self.class_name}.{method_name}: " + msg += "instance.params_spec needs to be set " + msg += "prior to calling instance.commit()." + raise ValueError(msg) + + try: + self._validate_parameters(self.params_spec, self.parameters) + except (TypeError, ValueError) as error: + raise ValueError(error) from error + + def _validate_parameters(self, spec, parameters): + """ + ### Summary + Recursively traverse parameters and verify conformity with spec + + ### Raises + - ``ValueError`` if a mandatory parameter is missing. + - ``ValueError`` if a parameter's type is not in the list of + valid types for that parameter. + - ``ValueError`` if a non-integer parameter is using range_min + or range_max. + - ``ValueError`` if a parameter's value is not in the list of + valid choices for that parameter. + - ``ValueError`` if an integer parameter's value is not within the + parameter's valid range. + - ``TypeError`` if range_min or range_max in the parameter specification + is not an integer. + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + try: + for param in spec: + if param in self.reserved_params: + continue + + if isinstance(spec[param], Map): + self._validate_parameters(spec[param], parameters.get(param, {})) + + if ( + parameters.get(param, None) is None + and spec[param].get("required", False) is True + ): + msg = f"{self.class_name}.{method_name}: " + msg += f"Playbook is missing mandatory parameter: {param}." + raise ValueError(msg) + + if isinstance(spec[param]["type"], list): + parameters[param] = self._verify_multitype( + spec[param], parameters, param + ) + else: + parameters[param] = self._verify_type( + spec[param]["type"], parameters, param + ) + + self._verify_choices( + spec[param].get("choices", None), parameters[param], param + ) + + if spec[param].get("type", None) != "int" and ( + spec[param].get("range_min", None) is not None + or spec[param].get("range_max", None) is not None + ): + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid param_spec for parameter '{param}'. " + msg += "range_min and range_max are only valid for " + msg += "parameters of type int. " + msg += f"Got type {spec[param]['type']} for param {param}." + raise ValueError(msg) + + if ( + spec[param].get("type", None) == "int" + and spec[param].get("range_min", None) is not None + and spec[param].get("range_max", None) is not None + ): + self._verify_integer_range( + spec[param].get("range_min", None), + spec[param].get("range_max", None), + parameters[param], + param, + ) + except (TypeError, ValueError) as error: + raise ValueError(error) from error + + def _verify_choices(self, choices: List[Any], value: Any, param: str) -> None: + """ + ### Summary + Verify that value is one of the choices + + ### Raises + - ``ValueError`` if a parameter's value is not in the list of + valid choices for that parameter. + """ + method_name = inspect.stack()[0][3] + if choices is None: + return + + if value not in choices: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid value for parameter '{param}'. " + msg += f"Expected one of {choices}. " + msg += f"Got {value}" + raise ValueError(msg) + + def _verify_integer_range( + self, range_min: int, range_max: int, value: int, param: str + ) -> None: + """ + ### Summary + Verify that value is within the range range_min to range_max + + ### Raises + - ``TypeError`` if range_min or range_max in the parameter + specification is not an integer. + - ``ValueError`` if the parameter's value is not within the + range range_min to range_max. + """ + method_name = inspect.stack()[0][3] + + for range_value in [range_min, range_max]: + if not isinstance(range_value, int): + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid specification for parameter '{param}'. " + msg += "range_min and range_max must be integers. Got " + msg += f"range_min '{range_min}' type {type(range_min)}, " + msg += f"range_max '{range_max}' type {type(range_max)}." + raise TypeError(msg) + + if value < range_min or value > range_max: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid value for parameter '{param}'. " + msg += f"Expected value between {range_min} and {range_max}. " + msg += f"Got {value}" + raise ValueError(msg) + + def _verify_type(self, expected_type: str, params: Any, param: str): + """ + ### Summary + Verify that value's type matches the expected type + + ### Raises + - ``ValueError`` if expected_type is not in self.valid_expected_types. + - ``TypeError`` if value's type does not match the expected type. + """ + try: + self._verify_expected_type(expected_type, param) + except ValueError as error: + raise ValueError(error) from error + + value = params[param] + if expected_type in self._ipaddress_types: + try: + self._ipaddress_guard(expected_type, value, param) + except TypeError as error: + self._invalid_type(expected_type, value, param, error) + + try: + return_value = self.validations[expected_type](value) + except (ValueError, TypeError) as err: + self._invalid_type(expected_type, value, param, err) + + return return_value + + def _ipaddress_guard(self, expected_type, value: Any, param: str) -> None: + """ + ### Summary + Guard against int and bool types for ipv4, ipv6, ipv4_subnet, + and ipv6_subnet type. + + ### Raises + - ``TypeError`` if value's type is int or bool and expected_type + is one of self._ipaddress_types. + + ### Discussion + The ipaddress module accepts int and bool types and converts + them to IP addresses or networks. E.g. True becomes 0.0.0.1, + False becomes 0.0.0.0, 1 becomes 0.0.0.1, etc. Because of + this, we need to fail int and bool values if expected_type is + one of ipv4, ipv6, ipv4_subnet, or ipv6_subnet. + """ + method_name = inspect.stack()[0][3] + if type(value) not in [int, bool]: + return + if expected_type not in self._ipaddress_types: + return + + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected type {expected_type}. " + msg += f"Got type {type(value)} for " + msg += f"param {param} with value {value}." + raise TypeError(msg) + + def _invalid_type( + self, expected_type: str, value: Any, param: str, error: str = "" + ) -> None: + """ + ### Summary + Error message for invalid type + + ### Raises + - ``TypeError``with error message. Always raises. + """ + method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid type for parameter '{param}'. " + msg += f"Expected {expected_type}. " + msg += f"Got '{value}'. " + msg += f"Error detail: {error}" + raise TypeError(msg) + + def _verify_multitype( # pylint: disable=inconsistent-return-statements + self, spec: Any, params: Any, param: str + ) -> Any: + """ + ### Summary + Verify that value's type matches one of the types in expected_types + + ### Raises + - ``ValueError`` if value's specification does not contain + a ``preferred_type`` key. + - ``TypeError`` if value's type does not match any of the + expected types. + + ### NOTES + 1. We've disabled inconsistent-return-statements. We're pretty + sure this method is correct. + """ + method_name = inspect.stack()[0][3] + + # preferred_type is mandatory for multitype + try: + self._verify_preferred_type_param_spec_is_present(spec, param) + except KeyError as error: + raise ValueError(error) from error + + # try to convert value to the preferred_type + preferred_type = spec["preferred_type"] + + (result, value) = self._verify_preferred_type_for_standard_types( + preferred_type, params[param] + ) + if result is True: + return value + + (result, value) = self._verify_preferred_type_for_ipaddress_types( + preferred_type, params[param] + ) + if result is True: + return value + + # Couldn't convert value to the preferred_type. Try the other types. + value = params[param] + + expected_types = spec.get("type", []) + + if preferred_type in expected_types: + # We've already tried preferred_type, so remove it + expected_types.remove(preferred_type) + + for expected_type in expected_types: + if expected_type in self._ipaddress_types and type(value) in [int, bool]: + # These are invalid, so skip them + continue + + try: + value = self.validations[expected_type](value) + return value + except (ValueError, TypeError): + pass + + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid type for parameter '{param}'. " + msg += f"Expected one of {expected_types}. " + msg += f"Got '{value}'." + raise TypeError(msg) + + def _verify_preferred_type_param_spec_is_present( + self, spec: Any, param: str + ) -> None: + """ + ### Summary + Verify that spec contains the key 'preferred_type' + + ### Raises + - ``KeyError`` if spec does not contain the key 'preferred_type' + """ + method_name = inspect.stack()[0][3] + if spec.get("preferred_type", None) is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid param_spec for parameter '{param}'. " + msg += "If type is a list, preferred_type must be specified." + raise KeyError(msg) + + def _verify_preferred_type_for_standard_types( + self, preferred_type: str, value: Any + ) -> tuple: + """ + If preferred_type is one of the standard python types + we use isinstance() to check if we are able to convert + the value to preferred_type + """ + standard_type_success = True + if preferred_type not in self._standard_types: + return (False, value) + try: + value = self.validations[preferred_type](value) + except (ValueError, TypeError): + standard_type_success = False + + if standard_type_success is True: + if isinstance(value, self._standard_types[preferred_type]): + return (True, value) + return (False, value) + + def _verify_preferred_type_for_ipaddress_types( + self, preferred_type: str, value: Any + ) -> tuple: + """ + We can't use isinstance() to verify ipaddress types. + Hence, we check these types separately. + """ + ip_type_success = True + if preferred_type not in self._ipaddress_types: + return (False, value) + try: + value = self.validations[preferred_type](value) + except (ValueError, TypeError): + ip_type_success = False + if ip_type_success is True: + return (True, value) + return (False, value) + + @staticmethod + def _validate_ipv4_address(value: Any) -> Any: + """ + verify that value is an IPv4 address + """ + try: + ipaddress.IPv4Address(value) + return value + except ipaddress.AddressValueError as err: + raise ValueError(f"invalid IPv4 address: {err}") from err + + @staticmethod + def _validate_ipv4_subnet(value: Any) -> Any: + """ + verify that value is an IPv4 network + """ + try: + ipaddress.IPv4Network(value) + return value + except ipaddress.AddressValueError as err: + raise ValueError(f"invalid IPv4 network: {err}") from err + + @staticmethod + def _validate_ipv6_address(value: Any) -> Any: + """ + verify that value is an IPv6 address + """ + try: + ipaddress.IPv6Address(value) + return value + except ipaddress.AddressValueError as err: + raise ValueError(f"invalid IPv6 address: {err}") from err + + @staticmethod + def _validate_ipv6_subnet(value: Any) -> Any: + """ + verify that value is an IPv6 network + """ + try: + ipaddress.IPv6Network(value) + return value + except ipaddress.AddressValueError as err: + raise ValueError(f"invalid IPv6 network: {err}") from err + + @staticmethod + def _validate_set(value: Any) -> Any: + """ + verify that value is a set + """ + if not isinstance(value, set): + raise TypeError(f"expected set, got {type(value)}") + return value + + @staticmethod + def _validate_tuple(value: Any) -> Any: + """ + verify that value is a tuple + """ + if not isinstance(value, tuple): + raise TypeError(f"expected tuple, got {type(value)}") + return value + + def _verify_mandatory_param_spec_keys(self, params_spec: dict) -> None: + """ + ### Summary + Recurse over params_spec dictionary and verify that the + specification for each param contains the mandatory keys + defined in self.mandatory_param_spec_keys + + ### Raises + - ``ValueError`` if a mandatory key is missing from a + parameter specification. + """ + method_name = inspect.stack()[0][3] + for param in params_spec: + if not isinstance(params_spec[param], Map): + continue + if param in self.reserved_params: + continue + self._verify_mandatory_param_spec_keys(params_spec[param]) + for key in self.mandatory_param_spec_keys: + if key in params_spec[param]: + continue + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid params_spec. Missing mandatory key " + msg += f"'{key}' for param '{param}'." + raise ValueError(msg) + + def _verify_expected_type(self, expected_type: str, param: str) -> None: + """ + ### Summary + Verify that expected_type is valid. + + ### Raises + - ``ValueError`` if expected_type is not in + self.valid_expected_types. + """ + method_name = inspect.stack()[0][3] + if expected_type in self.valid_expected_types: + return + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid 'type' in params_spec for parameter '{param}'. " + msg += "Expected one of " + msg += f"'{','.join(sorted(self.valid_expected_types))}'. " + msg += f"Got '{expected_type}'." + raise ValueError(msg) + + @property + def parameters(self): + """ + ### Summary + The parameters to validate. + parameters have the same structure as params_spec. + + ### Raises + - ``TypeError`` if ``parameters`` is not a dict. + """ + return self.properties["parameters"] + + @parameters.setter + def parameters(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid parameters. Expected type dict. " + msg += f"Got type {type(value)}." + raise TypeError(msg) + self.properties["parameters"] = value + + @property + def params_spec(self): + """ + ### Summary + The param specification used to validate the parameters. + + ### Raises + - ``TypeError`` if ``params_spec`` is not a dict. + - ``ValueError`` if params_spec is missing mandatory keys. + """ + return self.properties["params_spec"] + + @params_spec.setter + def params_spec(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "Invalid params_spec. Expected type dict. " + msg += f"Got type {type(value)}." + raise TypeError(msg) + self._verify_mandatory_param_spec_keys(value) + self.properties["params_spec"] = value diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 0f529cc68..21f14dca7 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -134,11 +134,11 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import \ MaintenanceMode -from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ MergeDicts -from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults_v2 import \ ParamsMergeDefaults -from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate_v2 import \ ParamsValidate from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ RestSend @@ -282,6 +282,370 @@ def params(self, value: Dict[str, Any]) -> None: raise ValueError(msg) self._properties["params"] = value +class Want: + """ + ### Summary + Build self.want, a list of validated playbook configurations. + + ### Raises + - ``ValueError`` if ParamsSpec() raises ``ValueError`` + - ``ValueError`` _merge_global_and_switch_configs() + raises ``ValueError`` + + ### Details + 1. Merge the playbook global config into each switch config. + 2. Validate the merged configs from step 1 against the param spec. + 3. Populate self.want with the validated configs. + + ### Usage + ```python + instance = Want() + instance.params = ansible_module.params + instance.params_spec = ParamsSpec() + instance.results = Results() + instance.items_key = "switches" + instance.validator = ParamsValidate() + instance.commit() + want = instance.want + ``` + ### self.want structure + + ```json + [ + { + "ip_address": "192.168.1.2", + "mode": "maintenance", + "deploy": false + }, + { + "ip_address": "192.168.1.3", + "mode": "normal", + "deploy": true + } + ] + ``` + """ + def __init__(self): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED Want()") + + self._properties = {} + self._properties["config"] = None + self._properties["items_key"] = None + self._properties["params"] = None + self._properties["params_spec"] = None + self._properties["results"] = None + self._properties["validator"] = None + self._properties["want"] = [] + + self.switch_configs = [] + self.validator = None + + def generate_params_spec(self) -> None: + """ + ### Summary + Generate the params_spec used to validate the configs + + ### Raises + - ``ValueError`` if self.params is not set + - ``ValueError`` if self.params_spec is not set + """ + # Generate the params_spec used to validate the configs + if self.params is None: + msg = f"{self.class_name}.generate_params_spec(): " + msg += "self.params is required" + raise ValueError(msg) + if self.params_spec is None: + msg = f"{self.class_name}.generate_params_spec(): " + msg += "self.params_spec is required" + raise ValueError(msg) + + try: + self.params_spec.params = self.params + except ValueError as error: + raise ValueError(error) from error + + try: + self.params_spec.commit() + except ValueError as error: + raise ValueError(error) from error + + def validate_configs(self) -> None: + """ + ### Summary + Validate the merged configs against the param spec + and populate self.want with the validated configs. + + ### Raises + - ``ValueError`` if self.validator is not set + + """ + if self.validator is None: + msg = f"{self.class_name}.validate_configs(): " + msg += "self.validator is required" + raise ValueError(msg) + + self.validator.params_spec = self.params_spec.params_spec + for config in self.merged_configs: + self.validator.parameters = config + self.validator.commit() + self.want.append(copy.deepcopy(config)) + + def build_merged_configs(self) -> None: + """ + ### Summary + If a parameter is missing from the config, and the parameter + has a default value, merge the default value for the parameter + into the config. + """ + self.merged_configs = [] + merge_defaults = ParamsMergeDefaults() + merge_defaults.params_spec = self.params_spec.params_spec + for config in self.item_configs: + merge_defaults.parameters = config + merge_defaults.commit() + self.merged_configs.append(merge_defaults.merged_parameters) + + msg = f"{self.class_name}.build_merged_configs(): " + msg += f"merged_configs: {json.dumps(self.merged_configs, indent=4, sort_keys=True)}" + self.log.debug(msg) + + def commit(self) -> None: + """ + ### Summary + Build self.want, a list of validated playbook configurations. + + ### Raises + - ``ValueError`` if self.params is not set + - ``ValueError`` if self.params_spec is not set + - ``ValueError`` if self.validator is not set + - ``ValueError`` if self.params_spec raises ``ValueError`` + - ``ValueError`` if _merge_global_and_switch_configs() + raises ``ValueError`` + + ### Details + See class docstring. + + ### self.want structure + See class docstring. + """ + method_name = inspect.stack()[0][3] + + if self.validator is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"self.validator must be set before calling {method_name}" + raise ValueError(msg) + + try: + self.generate_params_spec() + except ValueError as error: + raise ValueError(error) from error + + try: + self._merge_global_and_item_configs() + except ValueError as error: + raise ValueError(error) from error + + self.build_merged_configs() + + try: + self.validate_configs() + except ValueError as error: + raise ValueError(error) from error + + def _merge_global_and_item_configs(self) -> None: + """ + ### Summary + Builds self.item_configs from self.config + + Merge the global playbook config with each item config and + populate a list of merged configs (``self.item_configs``). + + ### Raises + - ``ValueError`` if self.config is not set + - ``ValueError`` if self.items_key is not set + - ``ValueError`` if playbook is missing list of items + - ``ValueError`` if merge_dicts raises ``TypeError`` or ``ValueError`` + + ### Merge rules + - item_config takes precedence over global_config. + - If item_config is missing a parameter, use parameter + from global_config. + - If item_config has a parameter, use it. + """ + method_name = inspect.stack()[0][3] + + if self.config is None: + msg = f"{self.class_name}.{method_name}: " + msg += "self.config is required" + raise ValueError(msg) + if self.items_key is None: + msg = f"{self.class_name}.{method_name}: " + msg += "self.items_key is required" + raise ValueError(msg) + if not self.config.get(self.items_key): + msg = f"{self.class_name}.{method_name}: " + msg += f"playbook is missing list of {self.items_key}" + raise ValueError(msg) + + self.item_configs = [] + merged_configs = [] + for item in self.config[self.items_key]: + # we need to rebuild global_config in this loop + # because merge_dicts modifies it in place + global_config = copy.deepcopy(self.config) + global_config.pop(self.items_key, None) + + msg = f"{self.class_name}.{method_name}: " + msg += "global_config: " + msg += f"{json.dumps(global_config, indent=4, sort_keys=True)}" + self.log.debug(msg) + + msg = f"{self.class_name}.{method_name}: " + msg += "switch PRE_MERGE: " + msg += f"{json.dumps(item, indent=4, sort_keys=True)}" + self.log.debug(msg) + + merge_dicts = MergeDicts() + try: + merge_dicts.dict1 = global_config + merge_dicts.dict2 = item + merge_dicts.commit() + item_config = merge_dicts.dict_merged + except(TypeError, ValueError) as error: + raise ValueError(error) from error + + msg = f"{self.class_name}.{method_name}: " + msg += "switch POST_MERGE: " + msg += f"{json.dumps(item_config, indent=4, sort_keys=True)}" + self.log.debug(msg) + + merged_configs.append(item_config) + self.item_configs = copy.copy(merged_configs) + + @property + def config(self): + """ + ### Summary + The playbook configuration to be processed. + + ``config`` is processed by ``_merge_global_and_switch_configs()`` + to build ``switch_configs``. + + - getter: return config + - setter: set config + - setter: raise ``ValueError`` if value is not a dict + """ + return self._properties["config"] + + @config.setter + def config(self, value) -> None: + if not isinstance(value,dict): + msg = f"{self.class_name}.config.setter: " + msg += "expected dict for value. " + msg += f"got {type(value).__name__}." + raise ValueError(msg) + self._properties["config"] = value + + @property + def items_key(self) -> str: + """ + Expects value to be the key for the list of items in the + playbook config. + + - getter: return the items_key + - setter: set the items_key + - setter: raise ``ValueError`` if value is not a string + """ + return self._properties["items_key"] + + @items_key.setter + def items_key(self, value: str) -> None: + """ + - setter: set the items_key + """ + if not isinstance(value, str): + msg = f"{self.class_name}.items_key.setter: " + msg += "expected string type for value. " + msg += f"got {type(value).__name__}." + raise ValueError(msg) + self._properties["items_key"] = value + + @property + def want(self) -> Dict[str, Any]: + """ + return the want list + """ + return self._properties["want"] + + @property + def params(self) -> Dict[str, Any]: + """ + Expects value to be the return value of + ``AnsibleModule.params`` property. + + - getter: return the params + - setter: set the params + - setter: raise ``ValueError`` if value is not a dict + """ + return self._properties["params"] + + @params.setter + def params(self, value: Dict[str, Any]) -> None: + """ + - setter: set the params + """ + if not isinstance(value, dict): + msg = f"{self.class_name}.params.setter: " + msg += "expected dict type for value. " + msg += f"got {type(value).__name__}." + raise ValueError(msg) + self._properties["params"] = value + + @property + def params_spec(self): + """ + ### Summary + Expects value to be an instance of ParamsSpec(). + + ``params_spec`` is passed to ``validator`` to validate the + playbook config. + + - getter: return the params_spec instance + - setter: set the params_spec instance + - setter: raise ``ValueError`` if value is not an instance + of ParamsSpec() + """ + return self._properties["params_spec"] + + @params_spec.setter + def params_spec(self, value) -> None: + """ + - setter: set the params_spec instance + """ + if not isinstance(value, ParamsSpec): + msg = f"{self.class_name}.params_spec.setter: " + msg += "expected ParamsSpec() instance for value. " + msg += f"got {type(value).__name__}." + raise ValueError(msg) + self._properties["params_spec"] = value + + @property + def validator(self) -> Any: + """ + getter: return the validator + setter: set the validator + """ + return self._properties["validator"] + + @validator.setter + def validator(self, value: Any) -> None: + """ + setter: set the validator + """ + self._properties["validator"] = value class Common: """ @@ -362,136 +726,18 @@ def _init_properties(self): self._properties["ansible_module"] = None def get_want(self) -> None: - """ - ### Summary - Build self.want, a list of validated playbook configurations. - - ### Raises - - ``ValueError`` if self.ansible_module is not set - - ``ValueError`` if ParamsSpec() raises ``ValueError`` - - ``ValueError`` _merge_global_and_switch_configs() - raises ``ValueError`` - - ### Details - 1. Merge the playbook global config into each switch config. - 2. Validate the merged configs from step 1 against the param spec. - 3. Populate self.want with the validated configs. - - ### self.want structure - - ```json - [ - { - "ip_address": "192.168.1.2", - "mode": "maintenance", - "deploy": false - }, - { - "ip_address": "192.168.1.3", - "mode": "normal", - "deploy": true - } - ] - ``` - """ - method_name = inspect.stack()[0][3] - - if self.ansible_module is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"self.ansible_module must be set before calling {method_name}" - raise ValueError(msg) - - # Generate the params_spec used to validate the configs - params_spec = ParamsSpec() - try: - params_spec.params = self.params - except ValueError as error: - raise ValueError(error) from error - - try: - params_spec.commit() - except ValueError as error: - raise ValueError(error) from error - - # Builds self.switch_configs - try: - self._merge_global_and_switch_configs(self.config) - except ValueError as error: - raise ValueError(error) from error - - # If a parameter is missing from the config, and the parameter - # has a default value, merge the default value for the parameter - # into the config. - merged_configs = [] - merge_defaults = ParamsMergeDefaults(self.ansible_module) - merge_defaults.params_spec = params_spec.params_spec - for config in self.switch_configs: - merge_defaults.parameters = config - merge_defaults.commit() - merged_configs.append(merge_defaults.merged_parameters) - - # validate the merged configs - self.validated_configs = [] - self.validator = ParamsValidate(self.ansible_module) - self.validator.params_spec = params_spec.params_spec - for config in merged_configs: - self.validator.parameters = config - self.validator.commit() - self.want.append(copy.deepcopy(config)) - + instance = Want() + instance.config = self.config + instance.items_key = "switches" + instance.params = self.params + instance.params_spec = ParamsSpec() + instance.validator = ParamsValidate() + instance.commit() + self.want = instance.want # Exit if there's nothing to do if len(self.want) == 0: self.ansible_module.exit_json(**self.results.ok_result) - def _merge_global_and_switch_configs(self, config) -> None: - """ - ### Summary - Merge the global playbook config with each switch config and - populate a list of merged configs (``self.switch_configs``). - - ### Raises - - ``ValueError`` if playbook is missing list of switches - - ### Merge rules - - switch_config takes precedence over global_config. - - If switch_config is missing a parameter, use parameter - from global_config. - - If switch_config has a parameter, use it. - """ - method_name = inspect.stack()[0][3] - - if not config.get("switches"): - msg = f"{self.class_name}.{method_name}: " - msg += "playbook is missing list of switches" - raise ValueError(msg) - - self.switch_configs = [] - merged_configs = [] - for switch in config["switches"]: - # we need to rebuild global_config in this loop - # because merge_dicts modifies it in place - global_config = copy.deepcopy(config) - global_config.pop("switches", None) - msg = ( - f"global_config: {json.dumps(global_config, indent=4, sort_keys=True)}" - ) - self.log.debug(msg) - - msg = f"switch PRE_MERGE : {json.dumps(switch, indent=4, sort_keys=True)}" - self.log.debug(msg) - - merge_dicts = MergeDicts(self.ansible_module) - merge_dicts.dict1 = global_config - merge_dicts.dict2 = switch - merge_dicts.commit() - switch_config = merge_dicts.dict_merged - - msg = f"switch POST_MERGE: {json.dumps(switch_config, indent=4, sort_keys=True)}" - self.log.debug(msg) - - merged_configs.append(switch_config) - self.switch_configs = copy.copy(merged_configs) - @property def ansible_module(self): """ diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index 1d9520883..a24239de5 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -32,8 +32,12 @@ MaintenanceMode from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ MergeDicts +from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ + MergeDicts as MergeDictsV2 from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ ParamsValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate_v2 import \ + ParamsValidate as ParamsValidateV2 from .fixture import load_fixture @@ -153,6 +157,14 @@ def merge_dicts_fixture(): return MergeDicts(MockAnsibleModule) +@pytest.fixture(name="merge_dicts_v2") +def merge_dicts_v2_fixture(): + """ + return MergeDicts() version 2 + """ + return MergeDictsV2() + + @pytest.fixture(name="params_validate") def params_validate_fixture(): """ @@ -161,6 +173,14 @@ def params_validate_fixture(): return ParamsValidate(MockAnsibleModule) +@pytest.fixture(name="params_validate_v2") +def params_validate_v2_fixture(): + """ + return ParamsValidate version 2 + """ + return ParamsValidateV2() + + @contextmanager def does_not_raise(): """ @@ -171,7 +191,7 @@ def does_not_raise(): def merge_dicts_data(key: str) -> Dict[str, str]: """ - Return data for merge_dicts unit tests + Return data from merge_dicts.json for merge_dicts unit tests. """ data_file = "merge_dicts" data = load_fixture(data_file).get(key) @@ -179,6 +199,16 @@ def merge_dicts_data(key: str) -> Dict[str, str]: return data +def merge_dicts_v2_data(key: str) -> Dict[str, str]: + """ + Return data from merge_dicts_v2.json for merge_dicts_v2 unit tests. + """ + data_file = "merge_dicts_v2" + data = load_fixture(data_file).get(key) + print(f"merge_dicts_v2_data: {key} : {data}") + return data + + def responses_config_deploy(key: str) -> Dict[str, str]: """ Return data in responses_ConfigDeploy.json diff --git a/tests/unit/module_utils/common/fixtures/merge_dicts_v2.json b/tests/unit/module_utils/common/fixtures/merge_dicts_v2.json new file mode 100644 index 000000000..d9b5162ce --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/merge_dicts_v2.json @@ -0,0 +1,147 @@ +{ + "test_merge_dicts_v2_00500": { + "TEST_NOTES": [ + "keys from dict1 and dict2 are different", + "keys from dict1 and dict2 are merged unchanged." + ], + "dict1": { + "foo": 1 + }, + "dict2": { + "bar": 3 + }, + "dict_merged": { + "foo": 1, + "bar": 3 + } + }, + "test_merge_dicts_v2_00510": { + "TEST_NOTES": [ + "dict1 and dict2 keys are the same", + "dict2 overwrites dict1" + ], + "dict1": { + "foo": 1 + }, + "dict2": { + "foo": 2 + }, + "dict_merged": { + "foo": 2 + } + }, + "test_merge_dicts_v2_00520": { + "TEST_NOTES": [ + "dict1 and dict2 keys are the same", + "dict2 overwrites dict1, even though dict1 keys value is a dict" + ], + "dict1": { + "foo": { + "bar": 1 + } + }, + "dict2": { + "foo": 2 + }, + "dict_merged": { + "foo": 2 + } + }, + "test_merge_dicts_v2_00530": { + "TEST_NOTES": [ + "dict1 and dict2 contain the same top-level keys", + "these keys both have a value that is a dict", + "dict1 nested-dict keys are the same as dict2 nested-dict keys", + "dict_merged nested-dict keys contain the values from dict2" + ], + "dict1": { + "foo": { + "bar": 1, + "baz": 1 + } + }, + "dict2": { + "foo": { + "bar": 2, + "baz": 2 + } + }, + "dict_merged": { + "foo": { + "bar": 2, + "baz": 2 + } + } + }, + "test_merge_dicts_v2_00540": { + "TEST_NOTES": [ + "dict1 and dict2 contain the same top-level keys", + "these keys both have a value that is a dict", + "dict1 nested-dict keys are different from dict2 nested-dict keys", + "dict_merged contains all keys from dict1 and dict2 with values unchanged" + ], + "dict1": { + "foo": { + "bar": 1 + } + }, + "dict2": { + "foo": { + "baz": 2 + } + }, + "dict_merged": { + "foo": { + "bar": 1, + "baz": 2 + } + } + }, + "test_merge_dicts_v2_00550": { + "TEST_NOTES": [ + "dict1 is empty", + "dict2 overwrites dict1", + "dict_merged == dict2" + ], + "dict1": {}, + "dict2": { + "foo": 3, + "baz": { + "bar": 10, + "key1": "value1", + "key2": "value2" + } + }, + "dict_merged": { + "foo": 3, + "baz": { + "bar": 10, + "key1": "value1", + "key2": "value2" + } + } + }, + "test_merge_dicts_v2_00560": { + "TEST_NOTES": [ + "dict2 is empty", + "dict_merge == dict1" + ], + "dict1": { + "foo": 3, + "baz": { + "bar": 10, + "key1": "value1", + "key2": "value2" + } + }, + "dict2": {}, + "dict_merged": { + "foo": 3, + "baz": { + "bar": 10, + "key1": "value1", + "key2": "value2" + } + } + } +} \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_merge_dicts_v2.py b/tests/unit/module_utils/common/test_merge_dicts_v2.py new file mode 100644 index 000000000..1734f88d0 --- /dev/null +++ b/tests/unit/module_utils/common/test_merge_dicts_v2.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. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +from typing import Any, Dict + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ + MergeDicts +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + does_not_raise, merge_dicts_v2_data, merge_dicts_v2_fixture) + + +def test_merge_dicts_v2_00000(merge_dicts_v2) -> None: + """ + ### Method + - ``__init__`` + + ### Test + - Verify Class attributes are initialized to expected values. + """ + with does_not_raise(): + instance = merge_dicts_v2 + assert isinstance(instance, MergeDicts) + assert isinstance(instance.properties, dict) + assert instance.class_name == "MergeDicts" + assert instance.properties.get("dict1", "foo") is None + assert instance.properties.get("dict2", "foo") is None + assert instance.properties.get("dict_merged", "foo") is None + + +MATCH_00100 = "MergeDicts.dict1: Invalid value. Expected type dict. Got type " + + +@pytest.mark.parametrize( + "value, expected", + [ + ({}, does_not_raise()), + ([], pytest.raises(TypeError, match=MATCH_00100)), + ((), pytest.raises(TypeError, match=MATCH_00100)), + (None, pytest.raises(TypeError, match=MATCH_00100)), + (1, pytest.raises(TypeError, match=MATCH_00100)), + (1.1, pytest.raises(TypeError, match=MATCH_00100)), + ("foo", pytest.raises(TypeError, match=MATCH_00100)), + (True, pytest.raises(TypeError, match=MATCH_00100)), + (False, pytest.raises(TypeError, match=MATCH_00100)), + ], +) +def test_merge_dicts_v2_00100(merge_dicts_v2, value, expected) -> None: + """ + ### Property + - ``dict1`` + + ### Test + - Verify ``dict1`` raises ``TypeError`` if passed anything other + than a dict. + """ + with does_not_raise(): + instance = merge_dicts_v2 + with expected: + instance.dict1 = value + + +MATCH_00200 = "MergeDicts.dict2: Invalid value. Expected type dict. Got type " + + +@pytest.mark.parametrize( + "value, expected", + [ + ({}, does_not_raise()), + ([], pytest.raises(TypeError, match=MATCH_00200)), + ((), pytest.raises(TypeError, match=MATCH_00200)), + (None, pytest.raises(TypeError, match=MATCH_00200)), + (1, pytest.raises(TypeError, match=MATCH_00200)), + (1.1, pytest.raises(TypeError, match=MATCH_00200)), + ("foo", pytest.raises(TypeError, match=MATCH_00200)), + (True, pytest.raises(TypeError, match=MATCH_00200)), + (False, pytest.raises(TypeError, match=MATCH_00200)), + ], +) +def test_merge_dicts_v2_00200(merge_dicts_v2, value, expected) -> None: + """ + ### Property + - ``dict2`` + + ### Test + - Verify ``dict2`` raises ``TypeError`` if passed anything other + than a dict. + """ + with does_not_raise(): + instance = merge_dicts_v2 + with expected: + instance.dict2 = value + + +MATCH_00300 = "MergeDicts.commit: " +MATCH_00300 += "dict1 and dict2 must be set " +MATCH_00300 += r"before calling commit\(\)" + + +@pytest.mark.parametrize( + "dict1, dict2, expected", + [ + ({}, {}, does_not_raise()), + (None, {}, pytest.raises(ValueError, match=MATCH_00300)), + ({}, None, pytest.raises(ValueError, match=MATCH_00300)), + ], +) +def test_merge_dicts_v2_00300(merge_dicts_v2, dict1, dict2, expected) -> None: + """ + ### Method + - ``commit`` + + ### Test + - Verify ``commit`` raises ``ValueError`` when dict1 or dict2 have not + been set. + """ + with does_not_raise(): + instance = merge_dicts_v2 + if dict1 is not None: + instance.dict1 = dict1 + if dict2 is not None: + instance.dict2 = dict2 + with expected: + instance.commit() + + +def test_merge_dicts_v2_00400(merge_dicts_v2) -> None: + """ + ### Property + - ``dict_merged`` + + ### Test + - Verify that ``dict_merged`` raises ``ValueError`` when accessed before + calling ``commit``. + """ + with does_not_raise(): + instance = merge_dicts_v2 + + match = "MergeDicts.dict_merged: " + match += r"Call instance\.commit\(\) before " + match += r"calling instance\.dict_merged\." + + with pytest.raises(ValueError, match=match): + instance.dict_merged # pylint: disable=pointless-statement + + +# The remaining tests verify various combinations of dict1 and dict2 +# using the following merge rules: +# 1. non-dict keys in dict1 are overwritten by dict2 +# if they exist in dict2 +# 2. non-dict keys in dict1 are not overwritten by dict2 +# if they do not exist in dict2 +# 3. if a key exists in both dict1 and dict2 and that key's value +# is a dict in both dict1 and dict2, the function recurses into +# the dict and applies the first two rules to the nested dict +# 4. in all other cases, dict2 overwrites dict1. For example, if +# a key exists in both dict1 and dict2 and that key's value +# is a dict in dict1 but not in dict2, the key is overwritten +# by dict2 (similar to rule 1) +def test_merge_dicts_v2_00500(merge_dicts_v2) -> None: + """ + ### Property + - ``dict1` + - ``dict2`` + - ``dict_merged`` + + ### Method + - ``commit`` + + ### Test + - ``dict1`` contains one top-level key foo with non-dict value. + - ``dict2`` contains one top-level key bar with non-dict value. + - ``dict_merged`` contains both top-level keys with unchanged values. + """ + key = "test_merge_dicts_v2_00500" + data = merge_dicts_v2_data(key) + + with does_not_raise(): + instance = merge_dicts_v2 + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_v2_00510(merge_dicts_v2) -> None: + """ + ### Property + - ``dict1` + - ``dict2`` + - ``dict_merged`` + + ### Method + - ``commit`` + + ### Test + - ``dict1`` contains one top-level key foo with non-dict value. + - ``dict2`` contains one top-level key foo with non-dict value. + - ``dict_merged`` contains one top-level key foo with value + from ``dict2``. + """ + key = "test_merge_dicts_v2_00510" + data = merge_dicts_v2_data(key) + + with does_not_raise(): + instance = merge_dicts_v2 + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_v2_00520(merge_dicts_v2) -> None: + """ + ### Property + - ``dict1` + - ``dict2`` + - ``dict_merged`` + + ### Method + - ``commit`` + + ### Test + - ``dict1`` contains one top-level key foo with dict value. + - ``dict2`` contains one top-level key foo with non-dict value. + - ``dict_merged`` contains one top-level key foo with value + from ``dict2``. + """ + key = "test_merge_dicts_v2_00520" + data = merge_dicts_v2_data(key) + + with does_not_raise(): + instance = merge_dicts_v2 + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_v2_00530(merge_dicts_v2) -> None: + """ + ### Property + - ``dict1` + - ``dict2`` + - ``dict_merged`` + + ### Method + - ``commit`` + + ### Test + - ``dict1`` contains one top-level key foo that is a dict. + - ``dict2`` contains one top-level key foo that is a dict. + - the keys in both nested dicts are the same. + - ``dict_merged`` contains one top-level key foo + that is a dict containing key/values from ``dict2``. + """ + key = "test_merge_dicts_v2_00530" + data = merge_dicts_v2_data(key) + + with does_not_raise(): + instance = merge_dicts_v2 + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_v2_00540(merge_dicts_v2) -> None: + """ + ### Property + - ``dict1` + - ``dict2`` + - ``dict_merged`` + + ### Method + - ``commit`` + + ### Test + - ``dict1`` contains one top-level key foo that is a dict. + - ``dict2`` contains one top-level key foo that is a dict. + - The keys in ``dict1``/``dict2`` nested dicts differ. + - ``dict_merged`` contains one top-level key foo + that is a dict containing keys from both ``dict1`` + and ``dict2``, with values unchanged. + """ + key = "test_merge_dicts_v2_00540" + data = merge_dicts_v2_data(key) + + with does_not_raise(): + instance = merge_dicts_v2 + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_v2_00550(merge_dicts_v2) -> None: + """ + ### Property + - ``dict1` + - ``dict2`` + - ``dict_merged`` + + ### Method + - ``commit`` + + ### Test + - ``dict1`` is empty. + - ``dict2`` contains several keys with a combination of + dict and non-dict values. + - ``dict_merged`` contains the contents of dict2. + """ + key = "test_merge_dicts_v2_00550" + data = merge_dicts_v2_data(key) + + with does_not_raise(): + instance = merge_dicts_v2 + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") + + +def test_merge_dicts_v2_00560(merge_dicts_v2) -> None: + """ + ### Property + - ``dict1` + - ``dict2`` + - ``dict_merged`` + + ### Method + - ``commit`` + + ### Test + - ``dict2`` is empty. + - ``dict1`` contains several keys with a combination of + dict and non-dict values. + - ``dict_merged`` contains the contents of dict1. + """ + key = "test_merge_dicts_v2_00560" + data = merge_dicts_v2_data(key) + + with does_not_raise(): + instance = merge_dicts_v2 + instance.dict1 = data.get("dict1") + instance.dict2 = data.get("dict2") + instance.commit() + assert instance.dict_merged == data.get("dict_merged") diff --git a/tests/unit/module_utils/common/test_params_validate_v2.py b/tests/unit/module_utils/common/test_params_validate_v2.py new file mode 100644 index 000000000..e8046679d --- /dev/null +++ b/tests/unit/module_utils/common/test_params_validate_v2.py @@ -0,0 +1,880 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +from typing import Any, Dict + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate_v2 import \ + ParamsValidate +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + does_not_raise, params_validate_v2_fixture) + + +def test_params_validate_v2_00000(params_validate_v2) -> None: + """ + ### Method + - ``__init__`` + + ## Test + - Class attributes are initialized to expected values. + """ + with does_not_raise(): + instance = params_validate_v2 + assert isinstance(instance, ParamsValidate) + assert isinstance(instance.properties, dict) + assert isinstance(instance.reserved_params, set) + assert instance.reserved_params == { + "choices", + "default", + "length_max", + "no_log", + "preferred_type", + "range_max", + "range_min", + "required", + "type", + } + assert instance.mandatory_param_spec_keys == {"required", "type"} + assert instance.class_name == "ParamsValidate" + assert instance.properties.get("parameters", "foo") is None + assert instance.properties.get("params_spec", "foo") is None + + +def test_params_validate_v2_00100(params_validate_v2) -> None: + """ + ### Property + - ``params_spec`` + + ### Test + - ``params_spec`` accepts a valid minimum specification + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + + +def test_params_validate_v2_00110(params_validate_v2) -> None: + """ + ### Property + - ``params_spec`` + + ### Test + - ``params_spec`` raises ``TypeError`` when passed a value + that is not a dict. + """ + match = "ParamsValidate.params_spec: " + match += "Invalid params_spec. Expected type dict. Got type " + match += r"\\." + + with pytest.raises(TypeError, match=match): + instance = params_validate_v2 + instance.params_spec = "foo" + + +@pytest.mark.parametrize( + "present_key, present_key_value, missing_key", + [ + ("required", True, "type"), + ("type", "int", "required"), + ], +) +def test_params_validate_v2_00120( + params_validate_v2, present_key, present_key_value, missing_key +) -> None: + """ + ### Property + - ``params_spec`` + + ### Test + - ``params_spec`` calls ``ValueError`` when specification is missing + a mandatory key. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"][f"{present_key}"] = present_key_value + + match = "ParamsValidate._verify_mandatory_param_spec_keys: " + match += "Invalid params_spec. " + match += f"Missing mandatory key '{missing_key}' for param 'foo'." + + with pytest.raises(ValueError, match=match): + instance = params_validate_v2 + instance.params_spec = params_spec + + +def test_params_validate_v2_00200(params_validate_v2) -> None: + """ + ### Property + - ``parameters`` + + ### Test + - ``parameters`` accepts a valid dict. + """ + with does_not_raise(): + instance = params_validate_v2 + instance.parameters = {"foo": "bar"} + + +def test_params_validate_v2_00210(params_validate_v2) -> None: + """ + ### Property + - ``parameters`` + + ### Test + - ``parameters`` raises ``TypeError`` when passed a value that + is not a dict. + """ + match = "ParamsValidate.parameters: " + match += "Invalid parameters. Expected type dict. Got type " + match += r"\\." + + with pytest.raises(TypeError, match=match): + instance = params_validate_v2 + instance.parameters = [1, 2, 3] + + +def test_params_validate_v2_00300(params_validate_v2) -> None: + """ + ### Method + - ``commit`` + + ### Test + - ``commit`` raises ``ValueError`` if ``parameters`` has not + been set. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + + match = "ParamsValidate.commit: " + match += "instance.parameters needs to be set prior to calling " + match += r"instance.commit\(\)\." + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_params_validate_v2_00310(params_validate_v2) -> None: + """ + ### Method + - ``commit`` + + ### Test + - ``commit`` raises ``ValueError`` if ``params_spec`` has not + been set. + """ + parameters = {} + parameters["foo"] = "bar" + + match = "ParamsValidate.commit: " + match += "instance.params_spec needs to be set prior to calling " + match += r"instance.commit\(\)\." + + with does_not_raise(): + instance = params_validate_v2 + instance.parameters = parameters + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_params_validate_v2_00320(params_validate_v2) -> None: + """ + ### Method + - ``commit`` + - ``validate_parameters`` + - ``verify_choices`` + + ### Test + - happy path for ``params_spec`` and ``parameters`` + """ + with does_not_raise(): + instance = params_validate_v2 + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + params_spec["foo"]["choices"] = ["bar", "baz"] + + parameters = {} + parameters["foo"] = "bar" + + with does_not_raise(): + instance.params_spec = params_spec + instance.parameters = parameters + instance.commit() + + +def test_params_validate_v2_00400(params_validate_v2) -> None: + """ + ### Method + - ``commit`` + - ``validate_parameters`` + + ### Test + - ``validate_parameters`` raises ``ValueError`` if parameters + is missing a required parameter. + """ + with does_not_raise(): + instance = params_validate_v2 + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + params_spec["foo"]["choices"] = ["bar", "baz"] + + parameters = {} + parameters["bar"] = "baz" + + with does_not_raise(): + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._validate_parameters: " + match += "Playbook is missing mandatory parameter: foo." + + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_params_validate_v2_00500(params_validate_v2) -> None: + """ + ### Method + - ``commit`` + - ``verify_choices`` + + ### Test + - Exception is not raised when ``parameter`` value is + a valid choice. + """ + with does_not_raise(): + instance = params_validate_v2 + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + params_spec["foo"]["choices"] = ["bar", "baz"] + + parameters = {} + parameters["foo"] = "baz" + + with does_not_raise(): + instance.params_spec = params_spec + instance.parameters = parameters + instance.commit() + + +def test_params_validate_v2_00510(params_validate_v2) -> None: + """ + ### Method + - ``commit`` + - ``verify_choices`` + + ### Test + - ``ValueError`` is raised when ``parameter`` value is + not a valid choice. + """ + with does_not_raise(): + instance = params_validate_v2 + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + params_spec["foo"]["choices"] = ["bar", "baz"] + + parameters = {} + parameters["foo"] = "bing" + + with does_not_raise(): + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._verify_choices: " + match += "Invalid value for parameter 'foo'. " + match += r"Expected one of \['bar', 'baz'\]. Got bing" + + with pytest.raises(ValueError, match=match): + instance.commit() + + +@pytest.mark.parametrize( + "value, expected_type", + [("bing", "int"), ("1", "ipv4"), (False, "set"), (True, "tuple"), ("bar", "bool")], +) +def test_params_validate_v2_00600(params_validate_v2, value, expected_type) -> None: + """ + ### Method + - ``commit`` + - ``verify_type`` + + ### Test + - Behavior when parameter value's type is not convertable to expected_type. + + ### NOTES + 1. value == bool and type in [ipv4, ipv6, ipv4_subnet, ipv6_subnet] + is tested separately (see ipaddress_guard test) + 2. If expected_type is "str" ANY value (dict, tuple, float, int, etc) + will succeed. Hence, for expected_type == "str" there are no invalid + values. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = expected_type + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._invalid_type: " + match += "Invalid type for parameter 'foo'. " + match += f"Expected {expected_type}. Got '{value}'. " + + with pytest.raises(ValueError, match=match): + instance.commit() + + +@pytest.mark.parametrize( + "value, expected_type", + [ + (1, "int"), + ("1", "int"), + (1.0, "float"), + ("1.0", "float"), + ("foo", "str"), + (1, "str"), + ([1, 2, "3"], "list"), + (1, "list"), + ((1, 2, 3), "tuple"), + ({"foo": "bar"}, "dict"), + ("foo=1, bar=2", "dict"), + ({"foo", "bar"}, "set"), + ("1.1.1.1", "ipv4"), + ("1.1.1.0/24", "ipv4_subnet"), + ("2001:1:1::fe", "ipv6"), + ("2001:1:1::/64", "ipv6_subnet"), + ], +) +def test_params_validate_v2_00610(params_validate_v2, value, expected_type) -> None: + """ + ### Method + - ``commit`` + - ``verify_type`` + + ### Test + - Verify exception is not raised if parameter type is valid. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = expected_type + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + instance.commit() + + +def test_params_validate_v2_00620(params_validate_v2) -> None: + """ + ### Method + - ``commit`` + - ``verify_type`` + + ### Test + - Verify that ``verify_type`` raises ``ValueError`` if type is not a valid type. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["required"] = True + params_spec["foo"]["type"] = "bad_type" + + parameters = {} + parameters["foo"] = "bar" + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._verify_expected_type: " + match += "Invalid 'type' in params_spec for parameter 'foo'. " + match += "Expected one of " + match += f"'{','.join(sorted(instance.valid_expected_types))}'. " + match += "Got 'bad_type'." + + with pytest.raises(ValueError, match=match): + instance.commit() + + +@pytest.mark.parametrize( + "value, expected_type, preferred_type", + [ + # preferred type != value's "native" type + ("1", ["int", "str"], "int"), + (1, ["int", "str", "list"], "list"), + ("1", ["int", "str", "list"], "list"), + (1.145, ["int", "list", "float"], "list"), + # preferred_type == value's "native" type + ("1", ["int", "str"], "str"), + (1, ["int", "str"], "int"), + ([1, 2, 3], ["int", "str", "list"], "list"), + (1.456, ["int", "str", "float"], "float"), + (False, ["int", "str", "bool"], "bool"), + ("1.1.1.1", ["int", "str", "ipv4"], "ipv4"), + # any type is convertable to str + (1, ["int", "str"], "str"), + ([1, 2, 3], ["int", "str", "list"], "str"), + ((1, 2, 3), ["int", "str", "list"], "str"), + ({1, 2, 3}, ["int", "str", "list"], "str"), + ({"foo": "bar"}, ["int", "str", "dict"], "str"), + (False, ["int", "str", "bool"], "str"), + (1.456, ["int", "str", "float"], "str"), + ], +) +def test_params_validate_v2_00700( + params_validate_v2, value, expected_type, preferred_type +) -> None: + """ + ### Method + - ``commit`` + - ``_verify_multitype`` + + ### Test + - Verify ``_verify_multitype`` converts parameter value to + preferred_type. + + NOTES: + 1. ansible.module_utils.common.validation can/will convert + any type to type str. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = expected_type + params_spec["foo"]["preferred_type"] = preferred_type + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + instance.commit() + if preferred_type in instance._ipaddress_types: # pylint: disable=protected-access + assert isinstance(instance.parameters["foo"], str) + else: + assert isinstance( + instance.parameters["foo"], instance._standard_types[preferred_type] + ) # pylint: disable=protected-access + + +@pytest.mark.parametrize( + "value, type_to_verify, preferred_type", + [ + ("1", ["dict", "ipv4"], "dict"), + ("1", ["dict", "ipv4"], "ipv4"), + ], +) +def test_params_validate_v2_00710( + params_validate_v2, value, type_to_verify, preferred_type +) -> None: + """ + ### Method + - ``commit`` + - ``verify_type`` + - ``_verify_multitype`` + + ### Test + - Verify behavior when parameter type is invalid. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["preferred_type"] = preferred_type + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._verify_multitype: " + match += "Invalid type for parameter 'foo'. " + match += r"Expected one of .*?. " + match += f"Got '{value}'." + + with pytest.raises(ValueError, match=match): + instance.commit() + + +@pytest.mark.parametrize( + "value, type_to_verify, preferred_type", + [ + ("1", ["dict", "ipv4"], "dict"), + ("1", ["dict", "ipv4"], "ipv4"), + ], +) +def test_params_validate_v2_00720( + params_validate_v2, value, type_to_verify, preferred_type +) -> None: + """ + ### Method + - ``commit`` + - ``verify_type`` + - ``_verify_multitype`` + + ### Test + - Verify behavior when parameter type is invalid in multi-level + parameters. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = ["int", "str"] + params_spec["foo"]["preferred_type"] = "int" + params_spec["foo"]["required"] = True + params_spec["bar"] = {} + params_spec["bar"]["type"] = "dict" + params_spec["bar"]["required"] = False + params_spec["bar"]["baz"] = {} + params_spec["bar"]["baz"]["type"] = type_to_verify + params_spec["bar"]["baz"]["preferred_type"] = preferred_type + params_spec["bar"]["baz"]["required"] = True + + parameters = {} + parameters["foo"] = 1 + parameters["bar"] = {} + parameters["bar"]["baz"] = value + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._verify_multitype: " + match += "Invalid type for parameter 'baz'. " + match += r"Expected one of .*?. " + match += f"Got '{value}'." + + with pytest.raises(ValueError, match=match): + instance.commit() + + +@pytest.mark.parametrize( + "value, type_to_verify, preferred_type", + [ + ("1", ["dict", "tuple", "list"], "dict"), + ], +) +def test_params_validate_v2_00730( + params_validate_v2, value, type_to_verify, preferred_type +) -> None: + """ + ### Method + - ``commit`` + - ``verify_type`` + - ``_verify_multitype`` + + ### Test + - Verify behavior when parameter value cannot be converted to the + preferred_type, but can be converted to another type in + ``_verify_multitype`` + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["preferred_type"] = preferred_type + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + instance.commit() + + +def test_params_validate_v2_00740(params_validate_v2) -> None: + """ + ### Method + - ``commit`` + - ``_verify_multitype`` + - ``_verify_preferred_type`` + + ### Test + - Verify behavior when the preferred_type key is missing from spec + when spec.type is a list of types. + + NOTES: + 1. preferred_type is mandatory when spec.type is a list of types. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = ["int", "str"] + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = 1 + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + match = "ParamsValidate._verify_preferred_type_param_spec_is_present: " + match += "Invalid param_spec for parameter 'foo'. " + match += "If type is a list, preferred_type must be specified." + with pytest.raises(ValueError, match=match): + instance.commit() + + +@pytest.mark.parametrize( + "value, type_to_verify", + [ + (1, "ipv4"), + (1, "ipv6"), + (1, "ipv4_subnet"), + (1, "ipv6_subnet"), + (True, "ipv4"), + (True, "ipv6"), + (True, "ipv4_subnet"), + (True, "ipv6_subnet"), + ], +) +def test_params_validate_v2_00800(params_validate_v2, value, type_to_verify) -> None: + """ + ### Method + - ``commit`` + - ``verify_type`` + - ``ipaddress_guard`` + + ### Test + - Verify that ``ValueError`` is raised if type is in + [ipv4, ipv6, ipv4_subnet, ipv6_subnet] and value is bool or int. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = type_to_verify + params_spec["foo"]["required"] = True + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._ipaddress_guard: " + match += f"Expected type {type_to_verify}. " + match += f"Got type {type(value)} for param foo with value {value}." + with pytest.raises(ValueError, match=match): + instance.commit() + + +@pytest.mark.parametrize( + "value, range_min, range_max", + [ + (1, 1, 10), + (5, 1, 10), + (10, 1, 10), + ], +) +def test_params_validate_v2_00900( + params_validate_v2, value, range_min, range_max +) -> None: + """ + ### Method + - ``commit`` + - ``_verify_integer_range`` + + ### Test + - Verify exception is not raised when parameter (int) is within + range_min and range_max. + """ + with does_not_raise(): + instance = params_validate_v2 + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "int" + params_spec["foo"]["required"] = True + params_spec["foo"]["range_min"] = range_min + params_spec["foo"]["range_max"] = range_max + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance.params_spec = params_spec + instance.parameters = parameters + instance.commit() + + +@pytest.mark.parametrize( + "value, range_min, range_max", + [ + (-1, 1, 10), + (0, 1, 10), + (11, 1, 10), + ], +) +def test_params_validate_v2_00910( + params_validate_v2, value, range_min, range_max +) -> None: + """ + ### Method + - ``commit`` + - ``_verify_integer_range`` + + ### Test + - Verify ``ValueError`` is raised if parameter (int) is not within + range_min and range_max + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "int" + params_spec["foo"]["required"] = True + params_spec["foo"]["range_min"] = range_min + params_spec["foo"]["range_max"] = range_max + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._verify_integer_range: " + match += "Invalid value for parameter 'foo'. " + match += f"Expected value between 1 and 10. Got {value}" + + with pytest.raises(ValueError, match=match): + instance.commit() + + +@pytest.mark.parametrize( + "value, range_min, range_max", + [ + (-1, "foo", 10), + (0, 1, "bar"), + (11, [], {}), + ], +) +def test_params_validate_v2_00920( + params_validate_v2, value, range_min, range_max +) -> None: + """ + ### Method + - ``commit`` + - ``_verify_integer_range`` + + ### Test + - Negative. Verify ``ValueError`` is raised if range_min or range_max + is not an integer. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "int" + params_spec["foo"]["required"] = True + params_spec["foo"]["range_min"] = range_min + params_spec["foo"]["range_max"] = range_max + + parameters = {} + parameters["foo"] = value + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._verify_integer_range: " + match += "Invalid specification for parameter 'foo'. " + match += "range_min and range_max must be integers. Got " + match += rf"range_min '.*?' type {type(range_min)}, " + match += rf"range_max '.*?' type {type(range_max)}." + + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_params_validate_v2_00930(params_validate_v2) -> None: + """ + ### Method + - ``commit`` + - ``_verify_integer_range`` + + ### Test + - Negative: Verify ``ValueError`` is raised if specification for non-int parameter + contains range_min and range_max. + """ + params_spec = {} + params_spec["foo"] = {} + params_spec["foo"]["type"] = "str" + params_spec["foo"]["required"] = True + params_spec["foo"]["range_min"] = 1 + params_spec["foo"]["range_max"] = 10 + + parameters = {} + parameters["foo"] = "bar" + + with does_not_raise(): + instance = params_validate_v2 + instance.params_spec = params_spec + instance.parameters = parameters + + match = "ParamsValidate._validate_parameters: " + match += "Invalid param_spec for parameter 'foo'. " + match += "range_min and range_max are only valid for " + match += "parameters of type int. Got type str for param foo." + + with pytest.raises(ValueError, match=match): + instance.commit() From e29cad644912afcc6b36b3a41c7d88bfc8bed25e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 27 May 2024 16:29:50 -1000 Subject: [PATCH 090/230] Fix PEP8 errors, more... 1. Fix PEP8 errors from the last commit. 2. Common().get_want(): Add try-except block around Want(). --- plugins/modules/dcnm_maintenance_mode.py | 62 +++++++++++------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 21f14dca7..bb0347dfe 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -282,6 +282,7 @@ def params(self, value: Dict[str, Any]) -> None: raise ValueError(msg) self._properties["params"] = value + class Want: """ ### Summary @@ -304,7 +305,7 @@ class Want: instance.params_spec = ParamsSpec() instance.results = Results() instance.items_key = "switches" - instance.validator = ParamsValidate() + instance.validator = ParamsValidate() instance.commit() want = instance.want ``` @@ -325,6 +326,7 @@ class Want: ] ``` """ + def __init__(self): self.class_name = self.__class__.__name__ @@ -340,7 +342,8 @@ def __init__(self): self._properties["validator"] = None self._properties["want"] = [] - self.switch_configs = [] + self.merged_configs = [] + self.item_configs = [] self.validator = None def generate_params_spec(self) -> None: @@ -361,7 +364,7 @@ def generate_params_spec(self) -> None: msg = f"{self.class_name}.generate_params_spec(): " msg += "self.params_spec is required" raise ValueError(msg) - + try: self.params_spec.params = self.params except ValueError as error: @@ -514,7 +517,7 @@ def _merge_global_and_item_configs(self) -> None: merge_dicts.dict2 = item merge_dicts.commit() item_config = merge_dicts.dict_merged - except(TypeError, ValueError) as error: + except (TypeError, ValueError) as error: raise ValueError(error) from error msg = f"{self.class_name}.{method_name}: " @@ -542,7 +545,7 @@ def config(self): @config.setter def config(self, value) -> None: - if not isinstance(value,dict): + if not isinstance(value, dict): msg = f"{self.class_name}.config.setter: " msg += "expected dict for value. " msg += f"got {type(value).__name__}." @@ -647,6 +650,7 @@ def validator(self, value: Any) -> None: """ self._properties["validator"] = value + class Common: """ Common methods, properties, and resources for all states. @@ -684,17 +688,6 @@ def __init__(self, params): self.switch_details = SwitchDetails() self.switch_details.results = self.results - self.params_spec = ParamsSpec() - try: - self.params_spec.params = self.params - except ValueError as error: - raise ValueError(error) from error - - try: - self.params_spec.commit() - except ValueError as error: - raise ValueError(error) from error - msg = f"ENTERED Common().{method_name}: " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" @@ -703,37 +696,39 @@ def __init__(self, params): # populated in self.validate_input() self.payloads = {} - # initialized in self.get_want() - self.validator = None - # populated in self.get_want() - self.validated_configs = [] - self.config = self.params.get("config") if not isinstance(self.config, dict): msg = "expected dict type for self.config. " msg += f"got {type(self.config).__name__}" raise ValueError(msg) - self.validated = [] self.have = {} - self.want = [] self.query = [] - # populated in self._merge_global_and_switch_configs() - self.switch_configs = [] + self.want = [] def _init_properties(self): self._properties = {} self._properties["ansible_module"] = None def get_want(self) -> None: - instance = Want() - instance.config = self.config - instance.items_key = "switches" - instance.params = self.params - instance.params_spec = ParamsSpec() - instance.validator = ParamsValidate() - instance.commit() - self.want = instance.want + """ + ### Summary + Build self.want, a list of validated playbook configurations. + + ### Raises + - ``ValueError`` if Want() instance raises ``ValueError`` + """ + try: + instance = Want() + instance.config = self.config + instance.items_key = "switches" + instance.params = self.params + instance.params_spec = ParamsSpec() + instance.validator = ParamsValidate() + instance.commit() + self.want = instance.want + except ValueError as error: + raise ValueError(error) from error # Exit if there's nothing to do if len(self.want) == 0: self.ansible_module.exit_json(**self.results.ok_result) @@ -781,6 +776,7 @@ def __init__(self, params): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.fabric_details = FabricDetailsByName(self.params) + self.rest_send = None msg = f"ENTERED Merged.{method_name}: " msg += f"state: {self.state}, " From d6c9b68421f1e2db51d020cb64222fab4bb9701f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 28 May 2024 08:53:39 -1000 Subject: [PATCH 091/230] dcnm_maintenance_mode: remove deprecated imports 1. dcnm_maintenance_mode.py: Remove deprecated (PEP 585) imports from typing since our minimum Python version is now 3.9. 2. Want().want: Change type hint to list instead of Dict[str, Any] 3. Want().params_spec.setter: Do not require instance of ParamsSpec to validate input. 3. Want().validator.setter: Implement input validation. 4. Merged().get_need(): Raise ValueError if switch does not exist. 5. ParamsValidate() (v2)._ipaddress_guard(): Modify TypeError message to include the pretty name for the type. 6. ParamsValidate() (v2).parameters setter: Modify TypeError message to include the pretty name for the type. 7. Update unit tests to reflect the above changes. --- .../module_utils/common/maintenance_mode.py | 22 ++-- .../module_utils/common/params_validate_v2.py | 4 +- plugins/modules/dcnm_maintenance_mode.py | 119 ++++++++++++------ .../common/test_params_validate_v2.py | 4 +- 4 files changed, 96 insertions(+), 53 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 2061e6331..ee818ae29 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -558,16 +558,17 @@ def rest_send(self): @rest_send.setter def rest_send(self, value): method_name = inspect.stack()[0][3] - _class_name = None + _class_have = None + _class_need = "RestSend" msg = f"{self.class_name}.{method_name}: " - msg += "value must be an instance of RestSend. " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." try: - _class_name = value.class_name + _class_have = value.class_name except AttributeError as error: msg += f"Error detail: {error}." raise TypeError(msg) from error - if _class_name != "RestSend": - self.log.debug(msg) + if _class_have != _class_need: raise TypeError(msg) self._properties["rest_send"] = value @@ -584,17 +585,16 @@ def results(self): @results.setter def results(self, value): method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "Results" msg = f"{self.class_name}.{method_name}: " - msg += "value must be an instance of Results. " + msg += f"value must be an instance of {_class_need}. " msg += f"Got value {value} of type {type(value).__name__}." - _class_name = None try: - _class_name = value.class_name + _class_have = value.class_name except AttributeError as error: msg += f" Error detail: {error}." - self.log.debug(msg) raise TypeError(msg) from error - if _class_name != "Results": - self.log.debug(msg) + if _class_have != _class_need: raise TypeError(msg) self._properties["results"] = value diff --git a/plugins/module_utils/common/params_validate_v2.py b/plugins/module_utils/common/params_validate_v2.py index 2ac019ce4..680cad707 100644 --- a/plugins/module_utils/common/params_validate_v2.py +++ b/plugins/module_utils/common/params_validate_v2.py @@ -404,7 +404,7 @@ def _ipaddress_guard(self, expected_type, value: Any, param: str) -> None: msg = f"{self.class_name}.{method_name}: " msg += f"Expected type {expected_type}. " - msg += f"Got type {type(value)} for " + msg += f"Got type {type(value).__name__} for " msg += f"param {param} with value {value}." raise TypeError(msg) @@ -673,7 +673,7 @@ def parameters(self, value): if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " msg += "Invalid parameters. Expected type dict. " - msg += f"Got type {type(value)}." + msg += f"Got type {type(value).__name__}." raise TypeError(msg) self.properties["parameters"] = value diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index bb0347dfe..b8f539d9c 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -126,7 +126,6 @@ import json import logging from os import environ -from typing import Any, Dict from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ @@ -199,7 +198,7 @@ def __init__(self): self._properties = {} self._properties["params"] = None - self._params_spec: Dict[str, Any] = {} + self._params_spec: dict = {} self.valid_states = ["merged", "query"] @@ -224,11 +223,11 @@ def commit(self): if self.params["state"] == "query": self._build_params_spec_for_query_state() - def _build_params_spec_for_merged_state(self) -> Dict[str, Any]: + def _build_params_spec_for_merged_state(self) -> dict: """ Build the parameter specifications for ``merged`` state. """ - self._params_spec: Dict[str, Any] = {} + self._params_spec: dict = {} self._params_spec["ip_address"] = {} self._params_spec["ip_address"]["required"] = True self._params_spec["ip_address"]["type"] = "ipv4" @@ -242,24 +241,24 @@ def _build_params_spec_for_merged_state(self) -> Dict[str, Any]: self._params_spec["deploy"]["type"] = "bool" self._params_spec["deploy"]["default"] = False - def _build_params_spec_for_query_state(self) -> Dict[str, Any]: + def _build_params_spec_for_query_state(self) -> None: """ Build the parameter specifications for ``query`` state. """ - self._params_spec: Dict[str, Any] = {} + self._params_spec: dict = {} self._params_spec["ip_address"] = {} self._params_spec["ip_address"]["required"] = True self._params_spec["ip_address"]["type"] = "ipv4" @property - def params_spec(self) -> Dict[str, Any]: + def params_spec(self) -> dict: """ return the parameter specification """ return self._params_spec @property - def params(self) -> Dict[str, Any]: + def params(self) -> dict: """ Expects value to be the return value of ``AnsibleModule.params`` property. @@ -271,7 +270,7 @@ def params(self) -> Dict[str, Any]: return self._properties["params"] @params.setter - def params(self, value: Dict[str, Any]) -> None: + def params(self, value: dict) -> None: """ - setter: set the params """ @@ -577,26 +576,34 @@ def items_key(self, value: str) -> None: self._properties["items_key"] = value @property - def want(self) -> Dict[str, Any]: + def want(self) -> list[dict]: """ - return the want list + ### Summary + Return the want list. See class docstring for structure details. """ return self._properties["want"] @property - def params(self) -> Dict[str, Any]: + def params(self) -> dict: """ - Expects value to be the return value of - ``AnsibleModule.params`` property. + ### Summary + The return value of ``AnsibleModule.params`` property + (or equivalent dict). This is passed to ``params_spec`` + and used in playbook config validation. - - getter: return the params - - setter: set the params - - setter: raise ``ValueError`` if value is not a dict + ### Raises + - setter: raise ``ValueError`` if value is not a ``dict``. + + ### getter + Return params + + ### setter + Set params """ return self._properties["params"] @params.setter - def params(self, value: Dict[str, Any]) -> None: + def params(self, value: dict) -> None: """ - setter: set the params """ @@ -611,43 +618,74 @@ def params(self, value: Dict[str, Any]) -> None: def params_spec(self): """ ### Summary - Expects value to be an instance of ParamsSpec(). + The parameter specification used to validate the playbook config. + Expects value to be an instance of ``ParamsSpec()``. ``params_spec`` is passed to ``validator`` to validate the playbook config. - - getter: return the params_spec instance - - setter: set the params_spec instance - - setter: raise ``ValueError`` if value is not an instance + ### Raises + - setter: raise ``TypeError`` if value is not an instance of ParamsSpec() + + ### getter + Return params_spec + + ### setter + Set params_spec """ return self._properties["params_spec"] @params_spec.setter def params_spec(self, value) -> None: - """ - - setter: set the params_spec instance - """ - if not isinstance(value, ParamsSpec): - msg = f"{self.class_name}.params_spec.setter: " - msg += "expected ParamsSpec() instance for value. " - msg += f"got {type(value).__name__}." - raise ValueError(msg) + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "ParamsSpec" + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) self._properties["params_spec"] = value @property - def validator(self) -> Any: + def validator(self): """ - getter: return the validator - setter: set the validator + ### Summary + ``validator`` is used to validate the playbook config. + Expects value to be an instance of ``ParamsValidate()``. + + ### Raises + - setter: ``TypeError`` if value is not an instance of ``ParamsValidate()`` + + ### getter + Return validator + + ### setter + Set validator """ return self._properties["validator"] @validator.setter - def validator(self, value: Any) -> None: - """ - setter: set the validator - """ + def validator(self, value) -> None: + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "ParamsValidate" + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) self._properties["validator"] = value @@ -933,11 +971,16 @@ def get_need(self): } ] """ + method_name = inspect.stack()[0][3] self.need = [] for want in self.want: ip_address = want.get("ip_address", None) if ip_address not in self.have: - continue + msg = f"{self.class_name}.{method_name}: " + msg += f"Switch {ip_address} in fabric {fabric_name} " + msg += "not found on the controller." + raise ValueError(msg) + serial_number = self.have[ip_address]["serial_number"] fabric_name = self.have[ip_address]["fabric_name"] if want.get("mode") != self.have[ip_address]["mode"]: diff --git a/tests/unit/module_utils/common/test_params_validate_v2.py b/tests/unit/module_utils/common/test_params_validate_v2.py index e8046679d..db2de0786 100644 --- a/tests/unit/module_utils/common/test_params_validate_v2.py +++ b/tests/unit/module_utils/common/test_params_validate_v2.py @@ -158,7 +158,7 @@ def test_params_validate_v2_00210(params_validate_v2) -> None: """ match = "ParamsValidate.parameters: " match += "Invalid parameters. Expected type dict. Got type " - match += r"\\." + match += r"list\." with pytest.raises(TypeError, match=match): instance = params_validate_v2 @@ -715,7 +715,7 @@ def test_params_validate_v2_00800(params_validate_v2, value, type_to_verify) -> match = "ParamsValidate._ipaddress_guard: " match += f"Expected type {type_to_verify}. " - match += f"Got type {type(value)} for param foo with value {value}." + match += f"Got type {type(value).__name__} for param foo with value {value}." with pytest.raises(ValueError, match=match): instance.commit() From beaf8f9b30b9bc80ce93931676f973642dcf359b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 28 May 2024 09:07:45 -1000 Subject: [PATCH 092/230] Want().want: Fix type hint --- plugins/modules/dcnm_maintenance_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index b8f539d9c..023db5210 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -576,7 +576,7 @@ def items_key(self, value: str) -> None: self._properties["items_key"] = value @property - def want(self) -> list[dict]: + def want(self) -> list: """ ### Summary Return the want list. See class docstring for structure details. From 5fa9442324eda6e6b51256ad0e29a15bae965b22 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 28 May 2024 10:12:00 -1000 Subject: [PATCH 093/230] MaintenanceMode: Add type hints, more... 1. MaintenanceMode: Add method return value type hints 2. MaintenanceMode: In several properties and methods, raise TypeError rather than ValueError if type is invalid. 3. MaintenanceMode: Update docstrings Below, Want() is in modules/dcnm_maintenance_mode.py 4. Want(): In several properties and methods, raise TypeError rather than ValueError if type is invalid. 5. Want(): Update docstrings 6. test_maintenance_mode_00110: Fix docstring --- .../module_utils/common/maintenance_mode.py | 145 ++++++++++++------ plugins/modules/dcnm_maintenance_mode.py | 52 ++++--- .../common/test_maintenance_mode.py | 2 +- 3 files changed, 128 insertions(+), 71 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index ee818ae29..446d903b3 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -150,12 +150,13 @@ def _init_properties(self): self._properties["rest_send"] = None self._properties["results"] = None - def verify_config_parameters(self, value): + def verify_config_parameters(self, value) -> None: """ + ### Summary Verify that required parameters are present in config. ### Raises - - ``ValueError`` if ``config`` is not a list. + - ``TypeError`` if ``config`` is not a list. - ``ValueError`` if ``config`` contains invalid content. ### NOTES @@ -171,7 +172,7 @@ def verify_config_parameters(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"{self.class_name}.config must be a list. " msg += f"Got type: {type(value).__name__}." - raise ValueError(msg) + raise TypeError(msg) for item in value: try: @@ -183,10 +184,15 @@ def verify_config_parameters(self, value): except ValueError as error: raise ValueError(error) from error - def verify_deploy(self, item): + def verify_deploy(self, item) -> None: """ - - Raise ``ValueError`` if ``deploy`` is not present. - - Raise ``ValueError`` if ``deploy`` is not a boolean. + ### Summary + Verify the ``deploy`` parameter. + + ### Raises + - ``ValueError`` if: + - ``deploy`` is not present. + - `deploy`` is not a boolean. """ method_name = inspect.stack()[0][3] if item.get("deploy", None) is None: @@ -198,10 +204,15 @@ def verify_deploy(self, item): msg += "deploy must be a boolean." raise ValueError(msg) - def verify_fabric_name(self, item): + def verify_fabric_name(self, item) -> None: """ - - Raise ``ValueError`` if ``fabric_name`` is not present. - - Raise ``ValueError`` if ``fabric_name`` is not a valid fabric name. + ### Summary + Validate the ``fabric_name`` parameter. + + ### Raises + - ``ValueError`` if: + - ``fabric_name`` is not present. + - ``fabric_name`` is not a valid fabric name. """ method_name = inspect.stack()[0][3] if item.get("fabric_name", None) is None: @@ -213,9 +224,14 @@ def verify_fabric_name(self, item): except (TypeError, ValueError) as error: raise ValueError(error) from error - def verify_ip_address(self, item): + def verify_ip_address(self, item) -> None: """ - - Raise ``ValueError`` if ``ip_address`` is not present. + ### Summary + Validate the ``ip_address`` parameter. + + ### Raises + - ``ValueError`` if: + - ``ip_address`` is not present. """ method_name = inspect.stack()[0][3] if item.get("ip_address", None) is None: @@ -223,14 +239,15 @@ def verify_ip_address(self, item): msg += "ip_address must be present in config." raise ValueError(msg) - def verify_mode(self, item): + def verify_mode(self, item) -> None: """ ### Summary Validate the ``mode`` parameter. ### Raises - - ``ValueError`` if ``mode`` is not present. - - ``ValueError`` if ``mode`` is not one of "maintenance" or "normal". + - ``ValueError`` if: + - ``mode`` is not present. + - ``mode`` is not one of "maintenance" or "normal". """ method_name = inspect.stack()[0][3] if item.get("mode", None) is None: @@ -243,13 +260,14 @@ def verify_mode(self, item): msg += f"Got {item.get('mode', None)}." raise ValueError(msg) - def verify_serial_number(self, item): + def verify_serial_number(self, item) -> None: """ ### Summary Validate the ``serial_number`` parameter. ### Raises - - ``ValueError`` if ``serial_number`` is not present. + - ``ValueError`` if: + - ``serial_number`` is not present. """ method_name = inspect.stack()[0][3] if item.get("serial_number", None) is None: @@ -257,14 +275,16 @@ def verify_serial_number(self, item): msg += "serial_number must be present in config." raise ValueError(msg) - def verify_commit_parameters(self): + def verify_commit_parameters(self) -> None: """ ### Summary Verify that required parameters are present before calling commit. ### Raises - - ``ValueError`` if ``rest_send`` is not set. - - ``ValueError`` if ``results`` is not set. + - ``ValueError`` if: + - ``config`` is not set. + - ``rest_send`` is not set. + - ``results`` is not set. """ method_name = inspect.stack()[0][3] if self.config is None: @@ -283,19 +303,20 @@ def verify_commit_parameters(self): msg += "before calling commit." raise ValueError(msg) - def commit(self): + def commit(self) -> None: """ ### Summary Initiates the maintenance mode change on the controller. ### Raises - - ``ValueError`` if ``config`` is not set. - - ``ValueError`` if ``rest_send`` is not set. - - ``ValueError`` if ``results`` is not set. - - ``ValueError`` for any exception raised by - - ``verify_commit_parameters()`` - - ``change_system_mode()`` - - ``deploy_switches()`` + - ``ValueError`` if + - ``config`` is not set. + - ``rest_send`` is not set. + - ``results`` is not set. + - any exception is raised by: + - ``verify_commit_parameters()`` + - ``change_system_mode()`` + - ``deploy_switches()`` """ try: self.verify_commit_parameters() @@ -308,15 +329,19 @@ def commit(self): except (ControllerResponseError, ValueError, TypeError) as error: raise ValueError(error) from error - def change_system_mode(self): + def change_system_mode(self) -> None: """ ### Summary Send the maintenance mode change request to the controller. ### Raises - - ``ControllerResponseError`` if controller response != 200. - - ``ValueError`` if ``fabric_name`` is invalid. - - ``TypeError`` if ``serial_number`` is not a string. + - ``ControllerResponseError`` if: + - controller response != 200. + - ``ValueError`` if: + - ``fabric_name`` is invalid. + - endpoint cannot be resolved. + - ``TypeError`` if: + - ``serial_number`` is not a string. """ method_name = inspect.stack()[0][3] @@ -377,7 +402,7 @@ def change_system_mode(self): msg += f"Got response {self.results.response_current}" raise ControllerResponseError(msg) - def build_deploy_dict(self): + def build_deploy_dict(self) -> None: """ ### Summary - Build the deploy_dict @@ -406,7 +431,7 @@ def build_deploy_dict(self): if deploy is True: self.deploy_dict[fabric_name].append(serial_number) - def build_serial_number_to_ip_address(self): + def build_serial_number_to_ip_address(self) -> None: """ ### Summary Populate self.serial_number_to_ip_address dict. @@ -433,14 +458,16 @@ def build_serial_number_to_ip_address(self): ip_address = item.get("ip_address") self.serial_number_to_ip_address[serial_number] = ip_address - def deploy_switches(self): + def deploy_switches(self) -> None: """ ### Summary Initiate config-deploy for the switches in ``self.deploy_dict``. ### Raises - - ``ControllerResponseError`` if controller response != 200. - - ``ValueError`` if endpoint cannot be resolved. + - ``ControllerResponseError`` if: + - controller response != 200. + - ``ValueError`` if: + - endpoint cannot be resolved. """ method_name = inspect.stack()[0][3] self.build_deploy_dict() @@ -495,15 +522,21 @@ def deploy_switches(self): raise ControllerResponseError(msg) @property - def config(self): + def config(self) -> list: """ ### Summary The maintenance mode configurations to be sent to the controller. - - getter: Return the config value. - - setter: Set the config value. - - setter: Raise ``ValueError`` if value is not a list. - - setter: Raise ``ValueError`` if value contains invalid content. + ### Raises + - setter: ``ValueError`` if: + - value is not a list. + - value contains invalid content. + + ### getter + Return ``config``. + + ### setter + Set ``config``. ### Value structure value is a ``list`` of ``dict``. Each dict must contain the following: @@ -548,10 +581,17 @@ def config(self, value): @property def rest_send(self): """ - - getter: Return an instance of the RestSend class. - - setter: Set an instance of the RestSend class. - - setter: Raise ``TypeError`` if the value is not an - instance of RestSend. + ### Summary + An instance of the RestSend class. + + ### Raises + - setter: ``TypeError`` if the value is not an instance of RestSend. + + ### getter + Return an instance of the RestSend class. + + ### setter + Set an instance of the RestSend class. """ return self._properties["rest_send"] @@ -575,10 +615,17 @@ def rest_send(self, value): @property def results(self): """ - - getter: Return an instance of the Results class. - - setter: Set an instance of the Results class. - - setter: Raise ``TypeError`` if the value is not an - instance of Results. + ### Summary + An instance of the Results class. + + ### Raises + - setter: ``TypeError`` if the value is not an instance of Results. + + ### getter + Return an instance of the Results class. + + ### setter + Set an instance of the Results class. """ return self._properties["results"] diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 023db5210..285d45687 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -288,9 +288,12 @@ class Want: Build self.want, a list of validated playbook configurations. ### Raises - - ``ValueError`` if ParamsSpec() raises ``ValueError`` - - ``ValueError`` _merge_global_and_switch_configs() - raises ``ValueError`` + - ``ValueError`` in the following cases: + - ``commit()`` is issued before setting mandatory properties + - When passing invalid values to property setters + - ``TypeError`` in the following cases: + - When passing invalid types to property setters + ### Details 1. Merge the playbook global config into each switch config. @@ -299,14 +302,17 @@ class Want: ### Usage ```python - instance = Want() - instance.params = ansible_module.params - instance.params_spec = ParamsSpec() - instance.results = Results() - instance.items_key = "switches" - instance.validator = ParamsValidate() - instance.commit() - want = instance.want + try: + instance = Want() + instance.params = ansible_module.params + instance.params_spec = ParamsSpec() + instance.results = Results() + instance.items_key = "switches" + instance.validator = ParamsValidate() + instance.commit() + want = instance.want + except (TypeError, ValueError) as error: + handle_error(error) ``` ### self.want structure @@ -420,12 +426,16 @@ def commit(self) -> None: Build self.want, a list of validated playbook configurations. ### Raises - - ``ValueError`` if self.params is not set - - ``ValueError`` if self.params_spec is not set - - ``ValueError`` if self.validator is not set - - ``ValueError`` if self.params_spec raises ``ValueError`` - - ``ValueError`` if _merge_global_and_switch_configs() - raises ``ValueError`` + - ``ValueError`` if: + - self.config is not set + - self.item_key is not set + - self.params is not set + - self.params_spec is not set + - self.validator is not set + - self.params_spec raises ``ValueError`` + - _merge_global_and_switch_configs() raises ``ValueError`` + - merge_dicts() raises `TypeError``` or ``ValueError`` + - playbook is missing list of items ### Details See class docstring. @@ -548,7 +558,7 @@ def config(self, value) -> None: msg = f"{self.class_name}.config.setter: " msg += "expected dict for value. " msg += f"got {type(value).__name__}." - raise ValueError(msg) + raise TypeError(msg) self._properties["config"] = value @property @@ -572,7 +582,7 @@ def items_key(self, value: str) -> None: msg = f"{self.class_name}.items_key.setter: " msg += "expected string type for value. " msg += f"got {type(value).__name__}." - raise ValueError(msg) + raise TypeError(msg) self._properties["items_key"] = value @property @@ -611,7 +621,7 @@ def params(self, value: dict) -> None: msg = f"{self.class_name}.params.setter: " msg += "expected dict type for value. " msg += f"got {type(value).__name__}." - raise ValueError(msg) + raise TypeError(msg) self._properties["params"] = value @property @@ -765,7 +775,7 @@ def get_want(self) -> None: instance.validator = ParamsValidate() instance.commit() self.want = instance.want - except ValueError as error: + except (TypeError, ValueError) as error: raise ValueError(error) from error # Exit if there's nothing to do if len(self.want) == 0: diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 7fce8b29d..8681446f6 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -298,7 +298,7 @@ def test_maintenance_mode_00110( Summary - Verify MaintenanceMode().commit() raises ``ValueError`` when - ``MaintenanceMode().change_system_mode`` raises any of: + ``MaintenanceMode().deploy_switches`` raises any of: - ``ControllerResponseError`` - ``ValueError`` From ed9c22519f0125290843e6a8b08034f491999718 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 28 May 2024 16:42:18 -1000 Subject: [PATCH 094/230] MaintenanceModeInfo: new class, more... 1. In module_utils/common/maintenance_mode.py - MaintenanceModeInfo: New class to retrieve maintenance mode info. 2. In dcnm_maintenance_mode.py - Merge().get_have(): Rewrite to leverage MaintenanceModeInfo() - Query().get_have(): Rewrite to leverage MaintenanceModeInfo() --- .../module_utils/common/maintenance_mode.py | 612 ++++++++++++++++++ plugins/modules/dcnm_maintenance_mode.py | 258 ++++---- 2 files changed, 733 insertions(+), 137 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 446d903b3..2a35066fe 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -27,6 +27,11 @@ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError +from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ + SwitchDetails +# Used in MaintenanceModeInfo +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ + FabricDetailsByName class MaintenanceMode: @@ -645,3 +650,610 @@ def results(self, value): if _class_have != _class_need: raise TypeError(msg) self._properties["results"] = value + + +class MaintenanceModeInfo: + """ + ### Summary + - Retrieve the maintenance mode state of switches. + + ### Raises + - ``TypeError`` in the following public properties: + - ``config`` if value is not a list. + - ``rest_send`` if value is not an instance of RestSend. + - ``results`` if value is not an instance of Results. + + - ``ValueError`` in the following public methods: + - ``refresh()`` if: + - ``config`` has not been set. + - ``rest_send`` has not been set. + - ``results`` has not been set. + + ### Details + Updates ``MaintenanceModeInfo().results`` to reflect success/failure of + the operation on the controller. + + Example value for ``config`` in the ``Usage`` section below: + ```json + ["192.168.1.2", "192.168.1.3"] + ``` + + Example value for ``info`` in the ``Usage`` section below: + ```json + { + "192.169.1.2": { + deployment_disabled: true + fabric_freeze_mode: true, + fabric_name: "MyFabric", + fabric_read_only: true + mode: "maintenance", + role: "spine", + serial_number: "FCI1234567" + }, + "192.169.1.3": { + deployment_disabled: false, + fabric_freeze_mode: false, + fabric_name: "YourFabric", + fabric_read_only: false + mode: "normal", + role: "leaf", + serial_number: "FCH2345678" + } + } + ``` + + ### Usage + ```python + instance = MaintenanceModeInfo(AnsibleModule.params) + try: + instance.config = config + instance.rest_send = RestSend(ansible_module) + instance.results = Results() + instance.refresh() + except (TypeError, ValueError) as error: + handle_error(error) + deployment_disabled = instance.deployment_disabled + deployment_disabled = instance.deployment_disabled + fabric_freeze_mode = instance.fabric_freeze_mode + fabric_name = instance.fabric_name + fabric_read_only = instance.fabric_read_only + info = instance.info + mode = instance.mode + role = instance.role + serial_number = instance.serial_number + ``` + """ + + def __init__(self, params): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.action = "maintenance_mode_have" + + self.params = params + self.conversions = ConversionUtils() + self.fabric_details = FabricDetailsByName(self.params) + self.switch_details = SwitchDetails() + + self._init_properties() + + msg = "ENTERED MaintenanceModeInfo(): " + self.log.debug(msg) + + def _init_properties(self): + self._properties = {} + self._properties["config"] = None + self._properties["info"] = None + self._properties["rest_send"] = None + self._properties["results"] = None + + def verify_refresh_parameters(self) -> None: + """ + ### Summary + Verify that required parameters are present before + calling ``refresh()``. + + ### Raises + - ``ValueError`` if: + - ``config`` is not set. + - ``rest_send`` is not set. + - ``results`` is not set. + """ + method_name = inspect.stack()[0][3] + if self.config is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.config must be set " + msg += "before calling refresh." + raise ValueError(msg) + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.rest_send must be set " + msg += "before calling refresh." + raise ValueError(msg) + if self.results is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.results must be set " + msg += "before calling refresh." + raise ValueError(msg) + + def refresh(self): + """ + ### Summary + Build ``self.info``, a dict containing the current maintenance mode + status of all switches in self.config. + + ### Raises + - ``ValueError`` if: + - ``SwitchDetails()`` raises ``ControllerResponseError`` + - ``SwitchDetails()`` raises ``ValueError`` + - ``FabricDetails()`` raises ``ControllerResponseError`` + - switch with ``ip_address`` does not exist on the controller. + + ### self.info structure + info is a dict, keyed on switch_ip, where each element is a dict + with the following structure: + - ``fabric_name``: The name of the switch's hosting fabric. + - ``freeze_mode``: The current state of the switch's hosting fabric. + If freeze_mode is True, configuration changes cannot be made to the + fabric or the switches within the fabric. + - ``mode``: The current maintenance mode of the switch. + - ``role``: The role of the switch in the hosting fabric. + - ``serial_number``: The serial number of the switch. + + ```json + { + "192.169.1.2": { + fabric_deployment_disabled: true + fabric_freeze_mode: true, + fabric_name: "MyFabric", + fabric_read_only: true + mode: "maintenance", + role: "spine", + serial_number: "FCI1234567" + }, + "192.169.1.3": { + fabric_deployment_disabled: false, + fabric_freeze_mode: false, + fabric_name: "YourFabric", + fabric_read_only: false + mode: "normal", + role: "leaf", + serial_number: "FCH2345678" + } + } + ``` + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + self.verify_refresh_parameters() + + self.switch_details.rest_send = self.rest_send + self.fabric_details.rest_send = self.rest_send + + self.switch_details.results = self.results + self.fabric_details.results = self.results + + try: + self.switch_details.refresh() + except (ControllerResponseError, ValueError) as error: + raise ValueError(error) from error + + try: + self.fabric_details.refresh() + except (ControllerResponseError, ValueError) as error: + raise ValueError(error) from error + + info = {} + # self.config has already been validated + for ip_address in self.config: + self.switch_details.filter = ip_address + + try: + serial_number = self.switch_details.serial_number + except ValueError as error: + raise ValueError(error) from error + + if serial_number is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"Switch with ip_address {ip_address} " + msg += "does not exist on the controller." + raise ValueError(msg) + + fabric_name = self.switch_details.fabric_name + freeze_mode = self.switch_details.freeze_mode + mode = self.switch_details.maintenance_mode + role = self.switch_details.switch_role + + try: + self.fabric_details.filter = fabric_name + except ValueError as error: + raise ValueError(error) from error + fabric_read_only = self.fabric_details.is_read_only + + info[ip_address] = {} + info[ip_address].update({"fabric_name": fabric_name}) + if freeze_mode is True: + info[ip_address].update({"fabric_freeze_mode": True}) + else: + info[ip_address].update({"fabric_freeze_mode": False}) + if fabric_read_only is True: + info[ip_address].update({"fabric_read_only": True}) + else: + info[ip_address].update({"fabric_read_only": False}) + if freeze_mode is True or fabric_read_only is True: + info[ip_address].update({"fabric_deployment_disabled": True}) + else: + info[ip_address].update({"fabric_deployment_disabled": False}) + info[ip_address].update({"mode": mode}) + if role is not None: + info[ip_address].update({"role": role}) + else: + info[ip_address].update({"role": "na"}) + info[ip_address].update({"serial_number": serial_number}) + self.info = copy.deepcopy(info) + + def _get(self, item): + """ + Return the value of the item from the filtered switch. + + ### Raises + - ``ValueError`` if ``filter`` is not set. + - ``ValueError`` if ``filter`` is not in the controller response. + - ``ValueError`` if item is not in the filtered switch dict. + """ + method_name = inspect.stack()[0][3] + + if self.filter is None: + msg = f"{self.class_name}.{method_name}: " + msg += "set instance.filter before accessing " + msg += f"property {item}." + raise ValueError(msg) + + if self.filter not in self._properties["info"]: + msg = f"{self.class_name}.{method_name}: " + msg += f"Switch with ip_address {self.filter} does not exist on " + msg += "the controller." + raise ValueError(msg) + + if item not in self._properties["info"][self.filter]: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.filter} does not have a key named {item}." + raise ValueError(msg) + + return self.conversions.make_boolean( + self.conversions.make_none(self._properties["info"][self.filter].get(item)) + ) + + @property + def filter(self): + """ + ### Summary + Set the query filter. + + ### Raises + None. However, if ``filter`` is not set, or ``filter`` is set to + an ip_address for a switch that does not exist on the controller, + ``ValueError`` will be raised when accessing the various getter + properties. + + ### Details + The filter should be the ip_address of the switch from which to + retrieve details. + + ``filter`` must be set before accessing this class's properties. + """ + return self._properties.get("filter") + + @filter.setter + def filter(self, value): + self._properties["filter"] = value + + @property + def config(self) -> list: + """ + ### Summary + A list of switch ip addresses for which maintenance mode state + will be retrieved. + + ### Raises + - setter: ``TypeError`` if: + - ``config`` is not a ``list``. + - Elements of ``config`` are not ``str``. + + ### getter + Return ``config``. + + ### setter + Set ``config``. + + ### Value structure + value is a ``list`` of ip addresses + + ### Example + ```json + ["172.22.150.2", "172.22.150.3"] + ``` + """ + return self._properties["config"] + + @config.setter + def config(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, list): + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.config must be a list. " + msg += f"Got type: {type(value).__name__}." + raise TypeError(msg) + + for item in value: + if not isinstance(item, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.config must be a list of strings " + msg += "containing ip addresses. " + msg += f"Got type: {type(item).__name__}." + raise TypeError(msg) + self._properties["config"] = value + + @property + def fabric_deployment_disabled(self): + """ + ### Summary + The current ``fabric_deployment_disabled`` state of the + filtered switch's hosting fabric. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``deployment_disabled`` is not in the filtered switch dict. + + ### Valid values + - ``True``: The fabric is in a state where configuration changes + cannot be made. + - ``False``: The fabric is in a state where configuration changes + can be made. + """ + return self._get("fabric_deployment_disabled") + + @property + def fabric_freeze_mode(self): + """ + ### Summary + The freezeMode state of the fabric in which the + filtered switch resides. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``fabric_name`` is not in the filtered switch dict. + + ### Valid values + - ``True``: The fabric is in a state where configuration changes + cannot be made. + - ``False``: The fabric is in a state where configuration changes + can be made. + """ + return self._get("fabric_freeze_mode") + + @property + def fabric_name(self): + """ + ### Summary + The name of the fabric in which the + filtered switch resides. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``fabric_name`` is not in the filtered switch dict. + """ + return self._get("fabric_name") + + @property + def fabric_read_only(self): + """ + ### Summary + The read-only state of the fabric in which the + filtered switch resides. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``fabric_name`` is not in the filtered switch dict. + + ### Valid values + - ``True``: The fabric is in a state where configuration changes + cannot be made. + - ``False``: The fabric is in a state where configuration changes + can be made. + """ + return self._get("fabric_freeze_mode") + + @property + def info(self) -> dict: + """ + ### Summary + Return or set the current maintenance mode state of the switches + represented by the ip_addresses in self.config. + + ### Raises + - ``ValueError`` if: + - ``refresh()`` has not been called before accessing ``info``. + + ### getter + Return ``info``. + + ### setter + Set ``info``. + + ### ``info`` structure + ``info`` is a dict, keyed on switch_ip, where each element is a dict + with the following structure: + - ``fabric_deployment_disabled``: The current state of the switch's + hosting fabric. If fabric_deployment_disabled is True, + configuration changes cannot be made to the fabric or the switches + within the fabric. + - ``fabric_name``: The name of the switch's hosting fabric. + - ``fabric_freeze_mode``: The current state of the switch's + hosting fabric. If freeze_mode is True, configuration changes + cannot be made to the fabric or the switches within the fabric. + - ``fabric_read_only``: The current state of the switch's + hosting fabric. If fabric_read_only is True, configuration changes + cannot be made to the fabric or the switches within the fabric. + - ``mode``: The current maintenance mode of the switch. + - ``role``: The role of the switch in the hosting fabric. + - ``serial_number``: The serial number of the switch. + + ### Example info dict + ```json + { + "192.169.1.2": { + fabric_deployment_disabled: true + fabric_freeze_mode: true, + fabric_name: "MyFabric", + fabric_read_only: true + mode: "maintenance", + role: "spine", + serial_number: "FCI1234567" + }, + "192.169.1.3": { + fabric_deployment_disabled: false + fabric_freeze_mode: false, + fabric_name: "YourFabric", + fabric_read_only: false + mode: "normal", + role: "leaf", + serial_number: "FCH2345678" + } + } + ``` + """ + method_name = inspect.stack()[0][3] + if self._properties["info"] is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.refresh() must be called before " + msg += f"accessing {self.class_name}.{method_name}." + raise ValueError(msg) + return copy.deepcopy(self._properties["info"]) + + @info.setter + def info(self, value: dict): + if not isinstance(value, dict): + msg = f"{self.class_name}.info.setter: " + msg += "value must be a dict. " + msg += f"Got value {value} of type {type(value).__name__}." + raise TypeError(msg) + self._properties["info"] = value + + @property + def mode(self): + """ + ### Summary + The current maintenance mode of the filtered switch. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``mode`` is not in the filtered switch dict. + """ + return self._get("mode") + + @property + def rest_send(self): + """ + ### Summary + An instance of the RestSend class. + + ### Raises + - setter: ``TypeError`` if the value is not an instance of RestSend. + + ### getter + Return an instance of the RestSend class. + + ### setter + Set an instance of the RestSend class. + """ + return self._properties["rest_send"] + + @rest_send.setter + def rest_send(self, value): + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "RestSend" + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) + self._properties["rest_send"] = value + + @property + def results(self): + """ + ### Summary + An instance of the Results class. + + ### Raises + - setter: ``TypeError`` if the value is not an instance of Results. + + ### getter + Return an instance of the Results class. + + ### setter + Set an instance of the Results class. + """ + return self._properties["results"] + + @results.setter + def results(self, value): + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "Results" + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) + self._properties["results"] = value + + @property + def role(self): + """ + ### Summary + The role of the filtered switch in the hosting fabric. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``role`` is not in the filtered switch dict. + """ + return self._get("role") + + @property + def serial_number(self): + """ + ### Summary + The serial number of the filtered switch. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``serial_number`` is not in the filtered switch dict. + """ + return self._get("serial_number") diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 285d45687..c3544d08d 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -128,11 +128,9 @@ from os import environ from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ - ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log -from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import \ - MaintenanceMode +from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import ( + MaintenanceMode, MaintenanceModeInfo) from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults_v2 import \ @@ -349,7 +347,6 @@ def __init__(self): self.merged_configs = [] self.item_configs = [] - self.validator = None def generate_params_spec(self) -> None: """ @@ -840,30 +837,43 @@ def get_have(self): ### Raises - ``ValueError`` if self.ansible_module is not set - - ``ValueError`` if SwitchDetails() raises ``ControllerResponseError`` - or ``ValueError`` - - ``ValueError`` if the switch's hosting fabric is in ``freezeMode`` - - ``ValueError`` if the switch's maintenance mode is ``inconsistent`` - - ``ValueError`` if the switch's maintenance mode is ``migration`` + - ``ValueError`` if MaintenanceModeInfo() raises ``ValueError`` ### self.have structure Have is a dict, keyed on switch_ip, where each element is a dict with the following structure: - ``fabric_name``: The name of the switch's hosting fabric. + - ``fabric_freeze_mode``: The current ``freezeMode`` state of the switch's + hosting fabric. If ``freeze_mode`` is True, configuration changes cannot + be made to the fabric or the switches within the fabric. + - ``fabric_read_only``: The current ``IS_READ_ONLY`` state of the switch's + hosting fabric. If ``fabric_read_only`` is True, configuration changes cannot + be made to the fabric or the switches within the fabric. - ``mode``: The current maintenance mode of the switch. - - ``role``: The role of the switch in the hosting fabric. + Possible values include: , ``inconsistent``, ``maintenance``, + ``migration``, ``normal``. + - ``role``: The role of the switch in the hosting fabric, e.g. + ``spine``, ``leaf``, ``border_gateway``, etc. - ``serial_number``: The serial number of the switch. ```json { "192.169.1.2": { + fabric_deployment_disabled: true + fabric_freeze_mode: true, fabric_name: "MyFabric", + fabric_read_only: true mode: "maintenance", + role: "spine", serial_number: "FCI1234567" }, "192.169.1.3": { + fabric_deployment_disabled: false + fabric_freeze_mode: false, fabric_name: "YourFabric", + fabric_read_only: false mode: "normal", + role: "leaf", serial_number: "FCH2345678" } } @@ -875,84 +885,96 @@ def get_have(self): msg += f"ansible_module must be set before calling {method_name}" raise ValueError(msg) - self.switch_details.rest_send = RestSend(self.ansible_module) - try: - self.switch_details.refresh() - except (ControllerResponseError, ValueError) as error: - raise ValueError(error) from error - - self.fabric_details.rest_send = RestSend(self.ansible_module) - self.fabric_details.results = self.results - self.fabric_details.refresh() - - self.have = {} - # self.config has already been validated - for switch in self.config.get("switches"): - ip_address = switch.get("ip_address") - self.switch_details.filter = ip_address - - try: - fabric_name = self.switch_details.fabric_name - except ValueError as error: - raise ValueError(error) from error - - if self.switch_details.freeze_mode is True: - msg = f"{self.class_name}.{method_name}: " - msg += f"Fabric {fabric_name} is in freeze mode. " - msg += "Configuration changes are not allowed. " - msg += "Ensure that NDFC -> Topology -> Fabric -> Actions -> " - msg += "More -> Deployment Enable is selected." - raise ValueError(msg) - - try: - self.fabric_details.filter = fabric_name - except ValueError as error: - raise ValueError(error) from error - - if self.fabric_details.is_read_only is True: - msg = f"{self.class_name}.{method_name}: " - msg += f"Fabric {fabric_name} is in read-only mode. " - msg += "Configuration changes are not allowed." - raise ValueError(msg) - - try: - serial_number = self.switch_details.serial_number - except ValueError as error: - raise ValueError(error) from error + instance = MaintenanceModeInfo(self.ansible_module.params) + instance.rest_send = RestSend(self.ansible_module) + instance.results = self.results + instance.config = [ + item["ip_address"] for item in self.config.get("switches", {}) + ] + instance.refresh() + self.have = instance.info - if serial_number is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"Switch with ip_address {ip_address} " - msg += "does not exist on the controller." - raise ValueError(msg) + def bail_if_fabric_deployment_disabled(self) -> None: + """ + ### Summary + Handle the following cases: + - switch migration mode is ``inconsistent`` + - switch migration mode is ``migration`` + - fabric is in read-only mode (IS_READ_ONLY is True) + - fabric is in freeze mode (Deployment Disable) - mode = self.switch_details.maintenance_mode + ### Raises + - ``ValueError`` if any of the above cases are true + """ + method_name = inspect.stack()[0][3] + for ip_address, value in self.have.items(): + fabric_name = value.get("fabric_name") + mode = value.get("mode") + serial_number = value.get("serial_number") + fabric_deployment_disabled = value.get("fabric_deployment_disabled") + fabric_freeze_mode = value.get("fabric_freeze_mode") + fabric_read_only = value.get("fabric_read_only") + + additional_info = "Additional info: " + additional_info += f"hosting_fabric: {fabric_name}, " + additional_info += "fabric_deployment_disabled: " + additional_info += f"{fabric_deployment_disabled}, " + additional_info += "fabric_freeze_mode: " + additional_info += f"{fabric_freeze_mode}, " + additional_info += "fabric_read_only: " + additional_info += f"{fabric_read_only}, " + additional_info += f"maintenance_mode: {mode}. " if mode == "inconsistent": msg = f"{self.class_name}.{method_name}: " msg += "Switch maintenance mode state differs from the " msg += "controller's maintenance mode state for switch " - msg += f"with ip_address {ip_address}. This is typically " - msg += "resolved by initiating a switch Deploy Config on " - msg += "the controller." + msg += f"with ip_address {ip_address}, " + msg += f"serial_number {serial_number}. " + msg += "This is typically resolved by initiating a switch " + msg += "Deploy Config on the controller. " + msg += additional_info raise ValueError(msg) if mode == "migration": msg = f"{self.class_name}.{method_name}: " msg += "Switch maintenance mode is in migration state for the " - msg += f"switch with ip_address {ip_address}. " + msg += "switch with " + msg += f"ip_address {ip_address}, " + msg += f"serial_number {serial_number}. " msg += "This indicates that the switch configuration is not " msg += "compatible with the switch role in the hosting " msg += "fabric. The issue might be resolved by initiating a " msg += "fabric Recalculate & Deploy on the controller. " - msg += "Failing that, the switch configuration might need to be " - msg += "manually modified to match the switch role in the " - msg += "hosting fabric." + msg += "Failing that, the switch configuration might need to " + msg += "be manually modified to match the switch role in the " + msg += "hosting fabric. " + msg += additional_info + raise ValueError(msg) + + if fabric_read_only is True: + msg = f"{self.class_name}.{method_name}: " + msg += "The hosting fabric is in read-only mode for the " + msg += f"switch with ip_address {ip_address}, " + msg += f"serial_number {serial_number}. " + msg += "The issue can be resolved for LAN_Classic fabrics by " + msg += "unchecking 'Fabric Monitor Mode' in the fabric " + msg += "settings on the controller. " + msg += additional_info raise ValueError(msg) - self.have[ip_address] = {} - self.have[ip_address].update({"fabric_name": fabric_name}) - self.have[ip_address].update({"mode": mode}) - self.have[ip_address].update({"serial_number": serial_number}) + if fabric_freeze_mode is True: + msg = f"{self.class_name}.{method_name}: " + msg += "The hosting fabric is in " + msg += "'Deployment Disable' state for the switch with " + msg += f"ip_address {ip_address}, " + msg += f"serial_number {serial_number}. " + msg += "Review the 'Deployment Enable / Deployment Disable' " + msg += "setting on the controller at: " + msg += "Fabric Controller > Overview > " + msg += "Topology > > Actions > More, and change " + msg += "the setting to 'Deployment Enable'. " + msg += additional_info + raise ValueError(msg) def get_need(self): """ @@ -1028,6 +1050,8 @@ def commit(self): except ValueError as error: raise ValueError(error) from error + self.bail_if_fabric_deployment_disabled() + self.get_need() try: @@ -1045,9 +1069,6 @@ def send_need(self) -> None: """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - msg = f"{self.class_name}.{method_name}: entered. " - msg += f"self.need: {json_pretty(self.need)}" - self.log.debug(msg) if len(self.need) == 0: msg = f"{self.class_name}.{method_name}: " @@ -1106,33 +1127,41 @@ def get_have(self): Build self.have, a dict containing the current mode of all switches. ### Raises - - ``ValueError`` if self.ansible_module is not set - - ``ValueError`` if SwitchDetails() raises ``ControllerResponseError`` - - ``ValueError`` if SwitchDetails() raises ``ValueError`` + - ``ValueError`` if MaintenanceModeInfo() raises ``ValueError`` ### self.have structure Have is a dict, keyed on switch_ip, where each element is a dict with the following structure: - ``fabric_name``: The name of the switch's hosting fabric. - - ``freeze_mode``: The current state of the switch's hosting fabric. - If freeze_mode is True, configuration changes cannot be made to the - fabric or the switches within the fabric. + - ``fabric_freeze_mode``: The current ``freezeMode`` state of the switch's + hosting fabric. If ``freeze_mode`` is True, configuration changes cannot + be made to the fabric or the switches within the fabric. + - ``fabric_read_only``: The current ``IS_READ_ONLY`` state of the switch's + hosting fabric. If ``fabric_read_only`` is True, configuration changes cannot + be made to the fabric or the switches within the fabric. - ``mode``: The current maintenance mode of the switch. - - ``role``: The role of the switch in the hosting fabric. + Possible values include: , ``inconsistent``, ``maintenance``, + ``migration``, ``normal``. + - ``role``: The role of the switch in the hosting fabric, e.g. + ``spine``, ``leaf``, ``border_gateway``, etc. - ``serial_number``: The serial number of the switch. ```json { "192.169.1.2": { - deployment_disabled: true + fabric_deployment_disabled: true + fabric_freeze_mode: true, fabric_name: "MyFabric", + fabric_read_only: true mode: "maintenance", role: "spine", serial_number: "FCI1234567" }, "192.169.1.3": { - deployment_disabled: false + fabric_deployment_disabled: false + fabric_freeze_mode: false, fabric_name: "YourFabric", + fabric_read_only: false mode: "normal", role: "leaf", serial_number: "FCH2345678" @@ -1146,59 +1175,14 @@ def get_have(self): msg += f"ansible_module must be set before calling {method_name}" raise ValueError(msg) - self.switch_details.rest_send = RestSend(self.ansible_module) - self.fabric_details.rest_send = RestSend(self.ansible_module) - - try: - self.switch_details.refresh() - except (ControllerResponseError, ValueError) as error: - raise ValueError(error) from error - - try: - self.fabric_details.refresh() - except (ControllerResponseError, ValueError) as error: - raise ValueError(error) from error - - self.have = {} - # self.config has already been validated - for switch in self.config.get("switches"): - ip_address = switch.get("ip_address") - self.switch_details.filter = ip_address - - try: - serial_number = self.switch_details.serial_number - except ValueError as error: - raise ValueError(error) from error - - if serial_number is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"Switch with ip_address {ip_address} " - msg += "does not exist on the controller." - raise ValueError(msg) - - fabric_name = self.switch_details.fabric_name - freeze_mode = self.switch_details.freeze_mode - mode = self.switch_details.maintenance_mode - role = self.switch_details.switch_role - - try: - self.fabric_details.filter = fabric_name - except ValueError as error: - raise ValueError(error) from error - fabric_read_only = self.fabric_details.is_read_only - - self.have[ip_address] = {} - self.have[ip_address].update({"fabric_name": fabric_name}) - if freeze_mode is True or fabric_read_only is True: - self.have[ip_address].update({"deployment_disabled": True}) - else: - self.have[ip_address].update({"deployment_disabled": False}) - self.have[ip_address].update({"mode": mode}) - if role is not None: - self.have[ip_address].update({"role": role}) - else: - self.have[ip_address].update({"role": "na"}) - self.have[ip_address].update({"serial_number": serial_number}) + instance = MaintenanceModeInfo(self.ansible_module.params) + instance.rest_send = RestSend(self.ansible_module) + instance.results = self.results + instance.config = [ + item["ip_address"] for item in self.config.get("switches", {}) + ] + instance.refresh() + self.have = instance.info def commit(self) -> None: """ From 6c51d453a63d110c456cf316b49b8641d25ce041 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 29 May 2024 15:20:34 -1000 Subject: [PATCH 095/230] RestSend() v2. New class, more... 1. rest_send_v2.py - RestSend(): New class that leverages dependency injection to remove all dependencies on AnsibleModule. 2. rest_send_v2.py - RestSend().save_settings() new method to save current setting of check_mode and timeout. 3. rest_send_v2.py - RestSend().restore_settings() new method to restore saved setting of check_mode and timeout. 4. dcnm_sender.py: Sender() - injected into RestSend(). Sender() uses dcnm_send(), and hence, AnsibleModule, but hides these from RestSend(). In the future, RestSend() could use a different Sender() that, say, uses the Requests module. 5. SwitchDetails(): Modify to use rest_send_v2.py 6. MaintenanceMode(): Modify to use rest_send_v2.py --- plugins/module_utils/common/dcnm_sender.py | 233 ++++++ .../module_utils/common/maintenance_mode.py | 24 +- plugins/module_utils/common/rest_send_v2.py | 761 ++++++++++++++++++ plugins/module_utils/common/switch_details.py | 66 +- plugins/modules/dcnm_maintenance_mode.py | 74 +- 5 files changed, 1098 insertions(+), 60 deletions(-) create mode 100644 plugins/module_utils/common/dcnm_sender.py create mode 100644 plugins/module_utils/common/rest_send_v2.py diff --git a/plugins/module_utils/common/dcnm_sender.py b/plugins/module_utils/common/dcnm_sender.py new file mode 100644 index 000000000..5bb038265 --- /dev/null +++ b/plugins/module_utils/common/dcnm_sender.py @@ -0,0 +1,233 @@ +# +# 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 ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import \ + dcnm_send + + +class Sender: + """ + ### Summary + An injected dependency for ``RestSend`` which implements the + ``sender`` interface using dcnm_send. + + ### Raises + - ``ValueError`` if ``ansible_module`` is not set. + ### Usage + ``ansible_module`` is an instance of ``AnsibleModule``. + + ```python + sender = Sender() + sender.ansible_module = ansible_module + rest_send = RestSend() + rest_send.sender = sender + # etc... + # See rest_send_v2.py for RestSend() usage. + ``` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + self.params = None + self.properties = {} + self.properties["ansible_module"] = None + self.properties["path"] = None + self.properties["payload"] = None + self.properties["response"] = None + self.properties["verb"] = None + self._valid_verbs = {"GET", "POST", "PUT", "DELETE"} + + msg = "ENTERED Sender(): " + self.log.debug(msg) + + def _verify_commit_parameters(self): + """ + ### Summary + Verify that required parameters are set prior to calling ``commit()`` + + ### Raises + - ``ValueError`` if ``verb`` is not set + - ``ValueError`` if ``path`` is not set + """ + if self.ansible_module is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "ansible_module must be set before calling commit()." + raise ValueError(msg) + if self.path is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "path must be set before calling commit()." + raise ValueError(msg) + if self.verb is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "verb must be set before calling commit()." + raise ValueError(msg) + + def commit(self): + """ + Send the REST request to the controller + + ### Raises + - AnsibleModule.fail_json() if the response is not a dict + ### Properties read + - ``verb``: HTTP verb e.g. GET, POST, PUT, DELETE + - ``path``: HTTP path e.g. http://controller_ip/path/to/endpoint + - ``payload`` Optional HTTP payload + + ## Properties written + - ``response``: raw response from the controller + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + self._verify_commit_parameters() + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"Calling dcnm_send: verb {self.verb}, path {self.path}" + if self.payload is None: + self.log.debug(msg) + response = dcnm_send(self.ansible_module, self.verb, self.path) + else: + msg += ", 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 = copy.deepcopy(response) + + @property + def ansible_module(self): + """ + An AnsibleModule instance. + + ### Raises + - ``TypeError`` if value is not an instance of AnsibleModule. + """ + return self.properties["ansible_module"] + + @ansible_module.setter + def ansible_module(self, value): + method_name = inspect.stack()[0][3] + try: + self.params = value.params + except AttributeError as error: + msg = f"{self.class_name}.{method_name}: " + msg += "instance.ansible_module must be an instance of AnsibleModule. " + msg += f"Got type {type(value).__name__}, value {value}. " + msg += f"Error detail: {error}." + raise TypeError(msg) from error + self.properties["ansible_module"] = value + + @property + def path(self): + """ + Endpoint path for the REST request. + + ### Raises + None + + ### Example + ``/appcenter/cisco/ndfc/api/v1/...etc...`` + """ + return self.properties.get("path") + + @path.setter + def path(self, value): + self.properties["path"] = value + + @property + def payload(self): + """ + Return the payload to send to the controller + + ### Raises + - ``TypeError`` if value is not a ``dict``. + """ + return self.properties["payload"] + + @payload.setter + def payload(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 type {type(value).__name__}, " + msg += f"value {value}." + raise TypeError(msg) + self.properties["payload"] = value + + @property + def response(self): + """ + ### Summary + The response from the controller. + + ### Raises + - ``TypeError`` if value is not a ``dict``. + + - getter: Return a copy of ``response`` + - setter: Set ``response`` + """ + 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 type {type(value).__name__}, " + msg += f"value {value}." + raise TypeError(msg) + self.properties["response"] = value + + @property + def verb(self): + """ + Verb for the REST request. + + ### Raises + - ``ValueError`` if value is not a valid verb. + + ### Valid verbs + ``GET``, ``POST``, ``PUT``, ``DELETE`` + """ + return self.properties.get("verb") + + @verb.setter + def verb(self, value): + method_name = inspect.stack()[0][3] + if value not in self._valid_verbs: + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be one of {sorted(self._valid_verbs)}. " + msg += f"Got {value}." + raise ValueError(msg) + self.properties["verb"] = value diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 2a35066fe..8efcf48f7 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -29,7 +29,6 @@ ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ SwitchDetails -# Used in MaintenanceModeInfo from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ FabricDetailsByName @@ -703,17 +702,30 @@ class MaintenanceModeInfo: ``` ### Usage + - Where: + - ``params`` is ``AnsibleModule.params`` + - ``config`` is per the above example. + - ``sender`` is an instance of a Sender() class. + See ``dcnm_sender.py`` for usage. + ```python - instance = MaintenanceModeInfo(AnsibleModule.params) + ansible_module = AnsibleModule() + # + params = AnsibleModule.params + instance = MaintenanceModeInfo(params) + + sender = Sender() + sender.ansible_module = ansible_module + rest_send = RestSend() + rest_send.sender = sender try: instance.config = config - instance.rest_send = RestSend(ansible_module) + instance.rest_send = rest_send instance.results = Results() instance.refresh() except (TypeError, ValueError) as error: handle_error(error) deployment_disabled = instance.deployment_disabled - deployment_disabled = instance.deployment_disabled fabric_freeze_mode = instance.fabric_freeze_mode fabric_name = instance.fabric_name fabric_read_only = instance.fabric_read_only @@ -728,7 +740,7 @@ def __init__(self, params): self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.action = "maintenance_mode_have" + self.action = "maintenance_mode_info" self.params = params self.conversions = ConversionUtils() @@ -844,7 +856,7 @@ def refresh(self): raise ValueError(error) from error info = {} - # self.config has already been validated + # Populate info dict for ip_address in self.config: self.switch_details.filter = ip_address diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py new file mode 100644 index 000000000..0baa60b93 --- /dev/null +++ b/plugins/module_utils/common/rest_send_v2.py @@ -0,0 +1,761 @@ +# +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import copy +import inspect +import json +import logging +import re +from time import sleep + +# Using only for its failed_result property +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results + + +class RestSend: + """ + ### Summary + Send REST requests to the controller with retries, and handle responses. + + ### Usage + Below we are using a Sender() class that requires an instance of + AnsibleModule, and uses dcnm_send() to send requests to the controller. + See dcnm_sender.py for details about implementing Sender() classes. + + ```python + sender = Sender() # class that implements the sender interface + sender.ansible_module = ansible_module + + rest_send = RestSend() + rest_send.unit_test = True # optional, use in unit tests for speed + rest_send.sender = sender + rest_send.path = "/rest/top-down/fabrics" + rest_send.verb = "GET" + rest_send.payload = my_payload # optional + rest_send.save_settings() # save current check_mode and timeout + rest_send.timeout = 300 # optional + rest_send.check_mode = True + # Do things with rest_send... + rest_send.commit() + rest_send.restore_settings() # restore check_mode and timeout + rest_send.commit() + + # list of responses from the controller for this session + response = rest_send.response + # dict containing the current controller response + response_current = rest_send.response_current + # list of results from the controller for this session + result = rest_send.result + # dict containing the current controller result + result_current = rest_send.result_current + ``` + """ + + def __init__(self, params): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + self.params = params + msg = "ENTERED RestSend(): " + msg += f"params: {self.params}" + self.log.debug(msg) + + self.properties = {} + self.properties["check_mode"] = False + self.properties["path"] = None + self.properties["payload"] = None + self.properties["response"] = [] + self.properties["response_current"] = {} + self.properties["result"] = [] + self.properties["result_current"] = {} + self.properties["send_interval"] = 5 + self.properties["sender"] = None + self.properties["timeout"] = 300 + self.properties["unit_test"] = False + self.properties["verb"] = None + + # See save_settings() and restore_settings() + self.saved_timeout = None + self.saved_check_mode = None + + self._valid_verbs = {"GET", "POST", "PUT", "DELETE"} + + self.check_mode = self.params.get("check_mode", False) + self.state = self.params.get("state") + + msg = "ENTERED RestSend(): " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + + def _verify_commit_parameters(self): + """ + ### Summary + Verify that required parameters are set prior to calling ``commit()`` + + ### Raises + - ``ValueError`` if: + - ``path`` is not set + - ``sender`` is not set + - ``verb`` is not set + """ + if self.path is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "path must be set before calling commit()." + raise ValueError(msg) + if self.sender is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "sender must be set before calling commit()." + raise ValueError(msg) + if self.verb is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "verb must be set before calling commit()." + raise ValueError(msg) + + def restore_settings(self): + """ + ### Summary + Restore ``check_mode`` and ``timeout`` to their saved values. + + ### Raises + None + + ### See also + - ``save_settings()`` + + ### Discussion + This is useful when a task needs to temporarily set ``check_mode`` + to False, (or change the timeout value) and then restore them to + their original values. + + - ``check_mode`` is not restored if ``save_setting()`` has not + previously been called. + - ``timeout`` is not restored if ``save_setting()`` has not + previously been called. + """ + if self.saved_check_mode is not None: + self.check_mode = self.saved_check_mode + if self.saved_timeout is not None: + self.timeout = self.saved_timeout + + def save_settings(self): + """ + Save the current values of ``check_mode`` and ``timeout`` for later + restoration. + + ### Raises + None + + ### See also + - ``restore_settings()`` + + + - ``check_mode`` is not saved if it has not yet been initialized. + - ``timeout`` is not save if it has not yet been initialized. + """ + if self.check_mode is not None: + self.saved_check_mode = self.check_mode + if self.timeout is not None: + self.saved_timeout = self.timeout + + def commit(self): + """ + Send the REST request to the controller + """ + msg = f"{self.class_name}.commit: " + msg += f"check_mode: {self.check_mode}." + self.log.debug(msg) + if self.check_mode is True: + self.commit_check_mode() + else: + self.commit_normal_mode() + + def commit_check_mode(self): + """ + ### Summary + Simulate a controller request for check_mode. + + ### Raises + None + + ### Properties read: + - ``verb``: HTTP verb e.g. DELETE, GET, POST, PUT + - ``path``: HTTP path e.g. http://controller_ip/path/to/endpoint + - ``payload``: Optional HTTP payload + + ### Properties written: + - ``response_current``: raw simulated response + - ``result_current``: result from self._handle_response() method + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"verb {self.verb}, path {self.path}." + self.log.debug(msg) + + self._verify_commit_parameters() + + self.response_current = {} + self.response_current["RETURN_CODE"] = 200 + self.response_current["METHOD"] = self.verb + self.response_current["REQUEST_PATH"] = self.path + self.response_current["MESSAGE"] = "OK" + self.response_current["CHECK_MODE"] = True + self.response_current["DATA"] = "[simulated-check-mode-response:Success]" + self.result_current = self._handle_response( + copy.deepcopy(self.response_current) + ) + + self.response = copy.deepcopy(self.response_current) + self.result = copy.deepcopy(self.result_current) + + def commit_normal_mode(self): + """ + Call dcnm_send() with retries until successful response or timeout is exceeded. + + ### Raises + - AnsibleModule.fail_json() if the response is not a dict + ### Properties read + - ``send_interval``: interval between retries (set in ImageUpgradeCommon) + - ``timeout``: timeout in seconds (set in ImageUpgradeCommon) + - ``verb``: HTTP verb e.g. GET, POST, PUT, DELETE + - ``path``: HTTP path e.g. http://controller_ip/path/to/endpoint + - ``payload`` Optional HTTP payload + + ## Properties written + - ``response``: raw response from the controller + - ``result``: result from self._handle_response() method + """ + method_name = inspect.stack()[0][3] + 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}, unit_test: {self.unit_test}." + self.log.debug(msg) + + self.sender.path = self.path + self.sender.verb = self.verb + if self.payload is not None: + self.sender.payload = self.payload + while timeout > 0 and success is False: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"Calling sender.commit(): verb {self.verb}, path {self.path}" + + self.sender.commit() + + self.response_current = self.sender.response + self.result_current = self._handle_response(self.response_current) + + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += f"result_current: {json.dumps(self.result_current, indent=4, sort_keys=True)}." + self.log.debug(msg) + + success = self.result_current["success"] + if success is False and self.unit_test is False: + sleep(self.send_interval) + timeout -= self.send_interval + + self.response_current = self._strip_invalid_json_from_response_data( + self.response_current + ) + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "response_current: " + msg += f"{json.dumps(self.response_current, indent=4, sort_keys=True)}." + self.log.debug(msg) + + self.response = copy.deepcopy(self.response_current) + self.result = copy.deepcopy(self.result_current) + + def _strip_invalid_json_from_response_data(self, response): + """ + Strip "Invalid JSON response:" from response["DATA"] if present + + This just clutters up the output and is not useful to the user. + """ + if "DATA" not in response: + return response + if not isinstance(response["DATA"], str): + return response + response["DATA"] = re.sub(r"Invalid JSON response:\s*", "", response["DATA"]) + return response + + def _handle_response(self, response): + """ + ### Summary + Call the appropriate handler for response based on verb + + ### Raises + - ``ValueError`` if verb is not a valid verb + + ### Valid verbs + - GET, POST, PUT, DELETE + """ + 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}." + raise ValueError(msg) + + def _handle_get_response(self, response): + """ + ### Summary + Handle GET responses from the controller. + + ### Caller + ``self._handle_response()`` + + ### 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): + """ + ### Summary + Handle POST, PUT responses from the controller. + + ### Caller + ``self.self._handle_response()`` + + + ### 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 check_mode(self): + """ + ### Summary + Determines if dcnm_send should be called. + + ### Raises + - ``TypeError`` if value is not a ``bool`` + + ### Default + ``False`` + + - If ``False``, dcnm_send is called. Real controller responses + are returned by RestSend() + - If ``True``, dcnm_send is not called. Simulated controller + responses are returned by RestSend() + + ### Discussion + We want to be able to read data from the controller for read-only + operations (i.e. to set check_mode to False temporarily, even when + the user has set check_mode to True). For example, SwitchIssuDetails + is a read-only operation, and we want to be able to read this data to + provide a real controller response to stage, validate, and upgrade + tasks. + """ + return self.properties.get("check_mode") + + @check_mode.setter + def check_mode(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be a bool(). Got {value}." + raise TypeError(msg) + self.properties["check_mode"] = value + + @property + def failed_result(self): + """ + Return a result for a failed task with no changes + """ + return Results().failed_result + + @property + def path(self): + """ + Endpoint path for the REST request. + + ### Raises + None + + ### Example + ``/appcenter/cisco/ndfc/api/v1/...etc...`` + """ + return self.properties.get("path") + + @path.setter + def path(self, value): + self.properties["path"] = value + + @property + def payload(self): + """ + Return the payload to send to the controller + + ### Raises + None + """ + return self.properties["payload"] + + @payload.setter + def payload(self, value): + self.properties["payload"] = value + + @property + def response_current(self): + """ + ### Summary + Return the current response from the controller + as a ``dict``. ``commit()`` must be called first. + + ### Raises + - setter: ``TypeError`` if value is not a ``dict`` + + ### getter + Return a copy of ``response_current`` + + ### setter + Set ``response_current`` + """ + 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 type {type(value).__name__}, " + msg += f"Value: {value}." + raise TypeError(msg) + self.properties["response_current"] = value + + @property + def response(self): + """ + ### Summary + The aggregated list of responses from the controller. + + ``commit()`` must be called first. + + ### Raises + - setter: ``TypeError`` if value is not a ``dict`` + + ### getter + Return a copy of ``response`` + + ### setter + Append value to ``response`` + """ + 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 type {type(value).__name__}, " + msg += f"Value: {value}." + raise TypeError(msg) + self.properties["response"].append(value) + + @property + def result(self): + """ + ### Summary + The aggregated list of results from the controller. + + ``commit()`` must be called first. + + ### Raises + - setter: ``TypeError`` if value is not a ``dict`` + + ### getter + Return a copy of ``result`` + + ### setter + Append value to ``result`` + """ + 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}." + raise TypeError(msg) + self.properties["result"].append(value) + + @property + def result_current(self): + """ + ### Summary + The current result from the controller + + ``commit()`` must be called first. + + This is a dict containing the current result. + + ### Raises + - setter: ``TypeError`` if value is not a ``dict`` + + ### getter + Return a copy of ``current_result`` + + ### setter + Set ``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}." + raise TypeError(msg) + self.properties["result_current"] = value + + @property + def send_interval(self): + """ + ### Summary + Send interval, in seconds, for retrying responses from the controller. + + ### Valid values + ``int`` + ### Default + ``5`` + + ### Raises + - setter: ``TypeError`` if value is not an ``int`` + + ### getter + Returns ``send_interval`` + + ### setter + Sets ``send_interval`` + """ + 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}." + raise TypeError(msg) + self.properties["send_interval"] = value + + @property + def sender(self): + """ + A class implementing functionality to send requests to the controller. + + The class must implement the following: + + 1. Class().class_name: str: property + - Returns the name of the class + - The class name must be "Sender" + 2. Class().verb: str: property setter + - Set the HTTP verb to use in the request. + - One of {"GET", "POST", "PUT", "DELETE"} + 3. Class().path: str: property setter + - Set the path to the controller endpoint. + 4. Class().payload: dict: property + - Set the payload to send to the controller. + - Must be Optional + 5. Class().commit(): method + - Initiate the request to the controller. + 6. Class().response: dict: property + - Return the response from the controller. + + ### Raises + - ``TypeError`` if value is not an instance of ``Sender`` + """ + return self.properties.get("sender") + + @sender.setter + def sender(self, value): + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "Sender" + msg = f"ZZZ: {self.class_name}.{method_name}: " + msg += f"Entered with value: {value}." + self.log.debug(msg) + + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) + self.properties["sender"] = value + + @property + def timeout(self): + """ + ### Summary + Timeout, in seconds, for retrieving responses from the controller. + + ### Raises + - setter: ``TypeError`` if value is not an ``int`` + + ### Valid values + ``int`` + + ### Default + ``300`` + + ### getter + Returns ``timeout`` + + ### setter + Sets ``timeout`` + """ + 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}." + raise TypeError(msg) + self.properties["timeout"] = value + + @property + def unit_test(self): + """ + ### Summary + Is RestSend being called from a unit test. + Set this to True in unit tests to speed the test up. + + ### Raises + - setter: ``TypeError`` if value is not a ``bool`` + + ### Default + ``False`` + + ### getter + Returns ``unit_test`` + + ### setter + Sets ``unit_test`` + """ + 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}." + raise TypeError(msg) + self.properties["unit_test"] = value + + @property + def verb(self): + """ + Verb for the REST request. + + ### Raises + - setter: ``ValueError`` if value is not a valid verb. + + ### Valid verbs + ``GET``, ``POST``, ``PUT``, ``DELETE`` + """ + return self.properties.get("verb") + + @verb.setter + def verb(self, value): + method_name = inspect.stack()[0][3] + if value not in self._valid_verbs: + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be one of {sorted(self._valid_verbs)}. " + msg += f"Got {value}." + raise ValueError(msg) + self.properties["verb"] = value diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 2b16acc91..8b00ec0fb 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -56,6 +56,7 @@ def __init__(self): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED common.SwitchDetails()") + self.action = "switch_details" self.conversions = ConversionUtils() self.ep_all_switches = EpAllSwitches() self.path = self.ep_all_switches.path @@ -69,7 +70,7 @@ def _init_properties(self): self.properties["info"] = {} self.properties["params"] = None - def validate_commit_parameters(self): + def validate_commit_parameters(self) -> None: """ Validate that mandatory parameters are set before calling refresh(). @@ -89,47 +90,78 @@ def validate_commit_parameters(self): msg += f"{self.class_name}.refresh()." raise ValueError(msg) - def refresh(self): + def send_request(self) -> None: """ - Refresh switch_details with current switch details from - the controller. + ### Summary + Send the request to the controller. ### Raises - - ``ControllerResponseError`` if the controller response is not 200. - - ``ValueError`` if mandatory parameters are not set. + None """ - method_name = inspect.stack()[0][3] - - try: - self.validate_commit_parameters() - except ValueError as error: - raise ValueError(error) from error - + # Send request + self.rest_send.save_settings() + self.rest_send.timeout = 1 # Regardless of ansible_module.check_mode, we need to get the # switch details. So, set check_mode to False. self.rest_send.check_mode = False self.rest_send.verb = self.verb self.rest_send.path = self.path self.rest_send.commit() + self.rest_send.restore_settings() + + def update_results(self) -> None: + """ + ### Summary + Update and register the results. + ### Raises + - ``ControllerResponseError`` if the controller response is not 200. + """ + method_name = inspect.stack()[0][3] + # Update and register results + self.results.action = self.action self.results.response_current = self.rest_send.response_current self.results.response = self.rest_send.response_current self.results.result_current = self.rest_send.result_current self.results.result = self.rest_send.result_current + # SwitchDetails never changes the controller state + self.results.changed = False - if self.results.response_current.get("RETURN_CODE") == 200: + if self.results.response_current["RETURN_CODE"] == 200: self.results.failed = False else: self.results.failed = True - # SwitchDetails never changes the controller state - self.results.changed = False - if self.results.response_current["RETURN_CODE"] != 200: + self.results.register_task_result() + + if self.results.failed is True: msg = f"{self.class_name}.{method_name}: " msg += "Unable to retrieve switch information from the controller. " msg += f"Got response {self.results.response_current}" raise ControllerResponseError(msg) + def refresh(self): + """ + Refresh switch_details with current switch details from + the controller. + + ### Raises + - ``ControllerResponseError`` if the controller response is not 200. + - ``ValueError`` if mandatory parameters are not set. + """ + + try: + self.validate_commit_parameters() + except ValueError as error: + raise ValueError(error) from error + + self.send_request() + + try: + self.update_results() + except ControllerResponseError as error: + raise ControllerResponseError(error) from error + data = self.results.response_current.get("DATA") self.properties["info"] = {} for switch in data: diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index c3544d08d..c3220cc95 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -137,10 +137,11 @@ ParamsMergeDefaults from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate_v2 import \ ParamsValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.plugins.module_utils.common.dcnm_sender import Sender from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ SwitchDetails from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ @@ -221,7 +222,7 @@ def commit(self): if self.params["state"] == "query": self._build_params_spec_for_query_state() - def _build_params_spec_for_merged_state(self) -> dict: + def _build_params_spec_for_merged_state(self) -> None: """ Build the parameter specifications for ``merged`` state. """ @@ -774,27 +775,24 @@ def get_want(self) -> None: self.want = instance.want except (TypeError, ValueError) as error: raise ValueError(error) from error - # Exit if there's nothing to do - if len(self.want) == 0: - self.ansible_module.exit_json(**self.results.ok_result) @property - def ansible_module(self): + def rest_send(self): """ - getter: return an instance of AnsibleModule - setter: set an instance of AnsibleModule + getter: return an instance of RestSend + setter: set an instance of RestSend """ - return self._properties["ansible_module"] + return self._properties["rest_send"] - @ansible_module.setter - def ansible_module(self, value): + @rest_send.setter + def rest_send(self, value): method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if not isinstance(value, AnsibleModule): + if not isinstance(value, RestSend): msg = f"{self.class_name}.{method_name}: " - msg += "expected AnsibleModule instance. " + msg += "expected RestSend instance. " msg += f"got {type(value).__name__}." raise ValueError(msg) - self._properties["ansible_module"] = value + self._properties["rest_send"] = value class Merged(Common): @@ -821,7 +819,6 @@ def __init__(self, params): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.fabric_details = FabricDetailsByName(self.params) - self.rest_send = None msg = f"ENTERED Merged.{method_name}: " msg += f"state: {self.state}, " @@ -880,13 +877,9 @@ def get_have(self): ``` """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if self.ansible_module is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"ansible_module must be set before calling {method_name}" - raise ValueError(msg) - instance = MaintenanceModeInfo(self.ansible_module.params) - instance.rest_send = RestSend(self.ansible_module) + instance = MaintenanceModeInfo(self.params) + instance.rest_send = self.rest_send instance.results = self.results instance.config = [ item["ip_address"] for item in self.config.get("switches", {}) @@ -894,7 +887,7 @@ def get_have(self): instance.refresh() self.have = instance.info - def bail_if_fabric_deployment_disabled(self) -> None: + def fabric_deployment_disabled(self) -> None: """ ### Summary Handle the following cases: @@ -982,7 +975,7 @@ def get_need(self): Build self.need for merged state. ### Raises - None + - ``ValueError`` if the switch is not found on the controller. ### self.need structure ```json @@ -1030,27 +1023,29 @@ def commit(self): Commit the merged state request ### Raises - - ``ValueError`` if get_want() raises ``ValueError`` - - ``ValueError`` if get_have() raises ``ValueError`` - - ``ValueError`` if send_need() raises ``ValueError`` + - ``ValueError`` if: + - ``get_want()`` raises ``ValueError`` + - ``get_have()`` raises ``ValueError`` + - ``send_need()`` raises ``ValueError`` """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable msg = f"{self.class_name}.{method_name}: entered" self.log.debug(msg) - self.rest_send = RestSend(self.ansible_module) - try: self.get_want() except ValueError as error: raise ValueError(error) from error + # Return if there's nothing to do + if len(self.want) == 0: + return try: self.get_have() except ValueError as error: raise ValueError(error) from error - self.bail_if_fabric_deployment_disabled() + self.fabric_deployment_disabled() self.get_need() @@ -1170,13 +1165,9 @@ def get_have(self): ``` """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if self.ansible_module is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"ansible_module must be set before calling {method_name}" - raise ValueError(msg) - instance = MaintenanceModeInfo(self.ansible_module.params) - instance.rest_send = RestSend(self.ansible_module) + instance = MaintenanceModeInfo(self.params) + instance.rest_send = self.rest_send instance.results = self.results instance.config = [ item["ip_address"] for item in self.config.get("switches", {}) @@ -1198,6 +1189,9 @@ def commit(self) -> None: self.get_want() except ValueError as error: raise ValueError(error) from error + # Return if there's nothing to do + if len(self.want) == 0: + return try: self.get_have() @@ -1255,10 +1249,16 @@ def main(): ansible_module.fail_json(msg) ansible_module.params["check_mode"] = ansible_module.check_mode + + sender = Sender() + sender.ansible_module = ansible_module + rest_send = RestSend(ansible_module.params) + rest_send.sender = sender + if ansible_module.params["state"] == "merged": try: task = Merged(ansible_module.params) - task.ansible_module = ansible_module + task.rest_send = rest_send task.commit() except ValueError as error: ansible_module.fail_json(f"{error}", **task.results.failed_result) @@ -1266,7 +1266,7 @@ def main(): elif ansible_module.params["state"] == "query": try: task = Query(ansible_module.params) - task.ansible_module = ansible_module + task.rest_send = rest_send task.commit() except ValueError as error: ansible_module.fail_json(f"{error}", **task.results.failed_result) From 3143e44561df18b89d7b8bdf611ea3205ee7559e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 29 May 2024 16:42:32 -1000 Subject: [PATCH 096/230] Remove unused imports After the previous commit, SwitchDetails() and FabricDetails() are no longer needed in dcnm_maintenance_mode.py, since they've been moved to MaintenanceMode() and MaintenanceModeInfo(). --- plugins/modules/dcnm_maintenance_mode.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index c3220cc95..c391bf8cc 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -128,6 +128,8 @@ from os import environ from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.common.dcnm_sender import \ + Sender from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import ( MaintenanceMode, MaintenanceModeInfo) @@ -141,11 +143,6 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.common.dcnm_sender import Sender -from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ - SwitchDetails -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ - FabricDetailsByName def json_pretty(msg): @@ -731,9 +728,6 @@ def __init__(self, params): self.results.state = self.state self.results.check_mode = self.check_mode - self.switch_details = SwitchDetails() - self.switch_details.results = self.results - msg = f"ENTERED Common().{method_name}: " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" @@ -818,7 +812,6 @@ def __init__(self, params): raise ValueError(msg) from error self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.fabric_details = FabricDetailsByName(self.params) msg = f"ENTERED Merged.{method_name}: " msg += f"state: {self.state}, " @@ -1109,7 +1102,6 @@ def __init__(self, params): raise ValueError(msg) from error self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.fabric_details = FabricDetailsByName(self.params) msg = "ENTERED Query(): " msg += f"state: {self.state}, " From bf7439cf16650bcb1d842f679b86d1be022d05cb Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 30 May 2024 08:13:38 -1000 Subject: [PATCH 097/230] Abstract response handling Abstract response handling by defining a "response handler" interface. Leverage this abstraction in RestSend() version 2. 1. module_utils/common/response_handler.py: Implementation of the response handler interface. See the docstring for ResponseHandler() in this file for interface details. 2. module_utils/common/rest_send_v2.py: Leverage the response handler interface and remove concrete local response handling methods. 3. dcnm_maintenance_mode.py: Modifiy RestSend() instantiation to include injection of the ResponseHandler() class. --- .../module_utils/common/response_handler.py | 149 +++++++++---- plugins/module_utils/common/rest_send_v2.py | 209 ++++++++---------- plugins/modules/dcnm_maintenance_mode.py | 3 + 3 files changed, 203 insertions(+), 158 deletions(-) diff --git a/plugins/module_utils/common/response_handler.py b/plugins/module_utils/common/response_handler.py index 1953de773..c96f3dbf7 100644 --- a/plugins/module_utils/common/response_handler.py +++ b/plugins/module_utils/common/response_handler.py @@ -25,26 +25,65 @@ class ResponseHandler: """ - - Parse response from the controller and set self.result - based on the response. - - Usage: + ### Summary: + Implement the response handler interface for injection into RestSend(). + + ### Raises: + - ``TypeError`` if: + - ``response`` is not a dict. + - ``ValueError`` if: + - ``response`` is missing any fields required by the handler + to calculate the result. + - Required fields: + - ``RETURN_CODE`` + - ``MESSAGE`` + - ``verb`` is not valid. + - ``response`` is not set prior to calling ``commit()``. + - ``verb`` is not set prior to calling ``commit()``. + + ### Interface specification: + - setter property: ``response`` + - Accepts a dict containing the controller response. + - Raises ``TypeError`` if: + - ``response`` is not a dict. + - Raises ``ValueError`` if: + - ``response`` is missing any fields required by the handler + to calculate the result, for example ``RETURN_CODE`` and + ``MESSAGE``. + - getter property: ``result`` + - Returns a dict containing the calculated result based on the + controller response and the request verb. + - setter property: ``verb`` + - Accepts a string containing the request verb. + - Valid verb: One of "DELETE", "GET", "POST", "PUT". + - Raises ``ValueError`` if verb is not valid. + - method: ``commit()`` + - Parse ``response`` and set ``result``. + - Raise ``ValueError`` if: + - ``response`` is not set. + - ``verb`` is not set. + + ### Usage example ```python # import and instantiate the class from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import ResponseHandler response_handler = ResponseHandler() - # Set the response from the controller - response_handler.response = controller_response + try: + # Set the response from the controller + response_handler.response = controller_response - # Set the request verb - response_handler.verb = "GET" + # Set the request verb + response_handler.verb = "GET" - # Call commit to parse the response - response_handler.commit() + # Call commit to parse the response + response_handler.commit() - # Access the result - result = response_handler.result + # Access the result + result = response_handler.result + except (TypeError, ValueError) as error: + handle_error(error) ``` - NOTES: @@ -53,6 +92,7 @@ class ResponseHandler: def __init__(self): self.class_name = self.__class__.__name__ + method_name = inspect.stack()[0][3] self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -63,10 +103,13 @@ def __init__(self): self.return_codes_success = {200, 404} self.valid_verbs = {"DELETE", "GET", "POST", "PUT"} + msg = f"ENTERED common.{self.class_name}.{method_name}" + self.log.debug(msg) + def _handle_response(self) -> None: """ - - Call the appropriate handler for response based on verb - - Raise ``ValueError`` if verb is unknown + ### Summary + Call the appropriate handler for response based on verb """ if self.verb == "GET": self._get_response() @@ -75,17 +118,18 @@ def _handle_response(self) -> None: def _get_response(self) -> None: """ - - Handle controller responses to GET requests and set self.result - with the following: + ### Summary + Handle GET responses from the controller and set self.result. + - self.result is a dict containing: - found: - False, if response: - - MESSAGE == "Not found" and - - RETURN_CODE == 404 + - MESSAGE == "Not found" and + - RETURN_CODE == 404 - True otherwise - success: - False if response: - - RETURN_CODE != 200 or - - MESSAGE != "OK" + - RETURN_CODE != 200 or + - MESSAGE != "OK" - True otherwise """ result = {} @@ -108,8 +152,10 @@ def _get_response(self) -> None: def _post_put_delete_response(self) -> None: """ - - Handle POST, PUT, DELETE responses from the controller - and set self.result with the following + ### Summary + Handle POST, PUT, DELETE responses from the controller and set + self.result. + - self.result is a dict containing: - changed: - True if changes were made by the controller - ERROR key is not present @@ -138,10 +184,14 @@ def _post_put_delete_response(self) -> None: def commit(self): """ - - Parse the response from the controller and set self.result - based on the response. - - Raise ``ValueError`` if response is not set - - Raise ``ValueError`` if verb is not set + ### Summary + Parse the response from the controller and set self.result + based on the response. + + ### Raises + - ``ValueError`` if: + - ``response`` is not set. + - ``verb`` is not set. """ method_name = inspect.stack()[0][3] msg = f"{self.class_name}.{method_name}: " @@ -162,13 +212,26 @@ def commit(self): @property def response(self): """ - - getter: Return response. - - setter: Set response. - - setter: Raise ``ValueError`` if response is not a dict. - - setter: Raise ``ValueError`` if MESSAGE key is missing - in response. - - setter: Raise ``ValueError`` if RETURN_CODE key is missing - in response. + ### Summary + The controller response. + + ### Raises + - setter: ``TypeError`` if: + - ``response`` is not a dict. + - setter: ``ValueError`` if: + - ``response`` is missing any fields required by the handler + to calculate the result. + - Required fields: + - ``RETURN_CODE`` + - ``MESSAGE`` + + ### getter + Return the response. Used internally to pass the response + between methods. + + ### setter + Set response. External interface to set the response from the + controller. """ return self._properties.get("response", None) @@ -179,7 +242,7 @@ def response(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"{self.class_name}.{method_name} must be a dict. " msg += f"Got {value}." - raise ValueError(msg) + raise TypeError(msg) if value.get("MESSAGE", None) is None: msg = f"{self.class_name}.{method_name}: " msg += "response must have a MESSAGE key. " @@ -197,7 +260,7 @@ def result(self): """ - getter: Return result. - setter: Set result. - - setter: Raise ``ValueError`` if result is not a dict. + - setter: Raise ``TypeError`` if result is not a dict. """ return self._properties.get("result", None) @@ -208,15 +271,25 @@ def result(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"{self.class_name}.{method_name} must be a dict. " msg += f"Got {value}." - raise ValueError(msg) + raise TypeError(msg) self._properties["result"] = value @property def verb(self): """ - - getter: Return request verb. - - setter: Set request verb. - - setter: Raise ``ValueError`` if request verb is invalid. + ### Summary + The request verb. + + ### Raises + - setter: ``ValueError`` if: + - ``verb`` is not valid. + - Valid verbs: "DELETE", "GET", "POST", "PUT". + + ### getter + Internal interface that returns the request verb. + + ### setter + External interface to set the request verb. """ return self._properties.get("verb", None) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 0baa60b93..ff95ba66e 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -85,6 +85,7 @@ def __init__(self, params): self.properties["payload"] = None self.properties["response"] = [] self.properties["response_current"] = {} + self.properties["response_handler"] = None self.properties["result"] = [] self.properties["result_current"] = {} self.properties["send_interval"] = 5 @@ -115,6 +116,7 @@ def _verify_commit_parameters(self): ### Raises - ``ValueError`` if: - ``path`` is not set + - ``response_handler`` is not set - ``sender`` is not set - ``verb`` is not set """ @@ -122,6 +124,10 @@ def _verify_commit_parameters(self): msg = f"{self.class_name}._verify_commit_parameters: " msg += "path must be set before calling commit()." raise ValueError(msg) + if self.response_handler is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "response_handler must be set before calling commit()." + raise ValueError(msg) if self.sender is None: msg = f"{self.class_name}._verify_commit_parameters: " msg += "sender must be set before calling commit()." @@ -168,9 +174,9 @@ def save_settings(self): ### See also - ``restore_settings()`` - + ### NOTES - ``check_mode`` is not saved if it has not yet been initialized. - - ``timeout`` is not save if it has not yet been initialized. + - ``timeout`` is not saved if it has not yet been initialized. """ if self.check_mode is not None: self.saved_check_mode = self.check_mode @@ -216,17 +222,22 @@ def commit_check_mode(self): self._verify_commit_parameters() - self.response_current = {} - self.response_current["RETURN_CODE"] = 200 - self.response_current["METHOD"] = self.verb - self.response_current["REQUEST_PATH"] = self.path - self.response_current["MESSAGE"] = "OK" - self.response_current["CHECK_MODE"] = True - self.response_current["DATA"] = "[simulated-check-mode-response:Success]" - self.result_current = self._handle_response( - copy.deepcopy(self.response_current) - ) + response_current = {} + response_current["RETURN_CODE"] = 200 + response_current["METHOD"] = self.verb + response_current["REQUEST_PATH"] = self.path + response_current["MESSAGE"] = "OK" + response_current["CHECK_MODE"] = True + response_current["DATA"] = "[simulated-check-mode-response:Success]" + self.response_current = response_current + try: + self.response_handler.response = self.response_current + self.response_handler.verb = self.verb + self.response_handler.commit() + self.result_current = self.response_handler.result + except (TypeError, ValueError) as error: + raise ValueError(error) from error self.response = copy.deepcopy(self.response_current) self.result = copy.deepcopy(self.result_current) @@ -271,9 +282,16 @@ def commit_normal_mode(self): msg += f"Calling sender.commit(): verb {self.verb}, path {self.path}" self.sender.commit() - self.response_current = self.sender.response - self.result_current = self._handle_response(self.response_current) + + # Handle controller response and derive result + try: + self.response_handler.response = self.response_current + self.response_handler.verb = self.verb + self.response_handler.commit() + self.result_current = self.response_handler.result + except ValueError as error: + raise ValueError(error) from error msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " @@ -297,11 +315,14 @@ def commit_normal_mode(self): self.response = copy.deepcopy(self.response_current) self.result = copy.deepcopy(self.result_current) - def _strip_invalid_json_from_response_data(self, response): + @staticmethod + def _strip_invalid_json_from_response_data(response: dict) -> dict: """ + ### Summary Strip "Invalid JSON response:" from response["DATA"] if present - This just clutters up the output and is not useful to the user. + This string in the response clutters up the output and is not + useful to the user. """ if "DATA" not in response: return response @@ -310,103 +331,11 @@ def _strip_invalid_json_from_response_data(self, response): response["DATA"] = re.sub(r"Invalid JSON response:\s*", "", response["DATA"]) return response - def _handle_response(self, response): - """ - ### Summary - Call the appropriate handler for response based on verb - - ### Raises - - ``ValueError`` if verb is not a valid verb - - ### Valid verbs - - GET, POST, PUT, DELETE - """ - 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}." - raise ValueError(msg) - - def _handle_get_response(self, response): - """ - ### Summary - Handle GET responses from the controller. - - ### Caller - ``self._handle_response()`` - - ### 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): - """ - ### Summary - Handle POST, PUT responses from the controller. - - ### Caller - ``self.self._handle_response()`` - - - ### 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 check_mode(self): """ ### Summary - Determines if dcnm_send should be called. + Determines if changes should be made on the controller. ### Raises - ``TypeError`` if value is not a ``bool`` @@ -414,18 +343,19 @@ def check_mode(self): ### Default ``False`` - - If ``False``, dcnm_send is called. Real controller responses - are returned by RestSend() - - If ``True``, dcnm_send is not called. Simulated controller - responses are returned by RestSend() + - If ``False``, write operations, if any, are made on the controller. + - If ``True``, write operations are not made on the controller. + Instead, controller responses for write operations are simulated + to be successful (200 response code) and these simulated responses + are returned by RestSend(). Read operations are not affected + and are sent to the controller and real responses are returned. ### Discussion We want to be able to read data from the controller for read-only operations (i.e. to set check_mode to False temporarily, even when - the user has set check_mode to True). For example, SwitchIssuDetails + the user has set check_mode to True). For example, SwitchDetails is a read-only operation, and we want to be able to read this data to - provide a real controller response to stage, validate, and upgrade - tasks. + provide a real controller response to the user. """ return self.properties.get("check_mode") @@ -535,6 +465,47 @@ def response(self, value): raise TypeError(msg) self.properties["response"].append(value) + @property + def response_handler(self): + """ + ### Summary + A class that implements the response handler interface. This + handles responses from the controller and returns results. + + ### Raises + - ``TypeError`` if: + - ``value`` is not an instance of ``ResponseHandler`` + + ### getter + Return a the ``response_handler`` instance. + + ### setter + Set the ``response_handler`` instance. + + ### NOTES + - See module_utils/common/response_handler.py for details about + implementing a ``ResponseHandler`` class. + """ + return self.properties.get("response_handler") + + @response_handler.setter + def response_handler(self, value): + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "ResponseHandler" + + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) + self.properties["response_handler"] = value + @property def result(self): """ @@ -544,7 +515,8 @@ def result(self): ``commit()`` must be called first. ### Raises - - setter: ``TypeError`` if value is not a ``dict`` + - setter: ``TypeError`` if: + - value is not a ``dict``. ### getter Return a copy of ``result`` @@ -659,9 +631,6 @@ def sender(self, value): method_name = inspect.stack()[0][3] _class_have = None _class_need = "Sender" - msg = f"ZZZ: {self.class_name}.{method_name}: " - msg += f"Entered with value: {value}." - self.log.debug(msg) msg = f"{self.class_name}.{method_name}: " msg += f"value must be an instance of {_class_need}. " diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index c391bf8cc..f0ac62bd9 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -139,6 +139,8 @@ ParamsMergeDefaults from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate_v2 import \ ParamsValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ @@ -1245,6 +1247,7 @@ def main(): sender = Sender() sender.ansible_module = ansible_module rest_send = RestSend(ansible_module.params) + rest_send.response_handler = ResponseHandler() rest_send.sender = sender if ansible_module.params["state"] == "merged": From 4ef99a79697dbf174b71da5f96b3654501d9c7ea Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 30 May 2024 08:21:14 -1000 Subject: [PATCH 098/230] ResultHandler(): Update unit tests 1. Update unit tests to expect TypeError when input to result.setter and response.setter is not a dict. --- tests/unit/module_utils/common/test_response_handler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/module_utils/common/test_response_handler.py b/tests/unit/module_utils/common/test_response_handler.py index 0b2964b8c..be4df011d 100644 --- a/tests/unit/module_utils/common/test_response_handler.py +++ b/tests/unit/module_utils/common/test_response_handler.py @@ -84,7 +84,7 @@ def test_response_handler_00030(response_handler) -> None: - response.setter Summary - - Verify ``ValueError`` is raised when response is not a dict. + - Verify ``TypeError`` is raised when response is not a dict. """ with does_not_raise(): @@ -92,7 +92,7 @@ def test_response_handler_00030(response_handler) -> None: match = r"ResponseHandler\.response:\s+" match += r"ResponseHandler\.response must be a dict\.\s+" match += r"Got INVALID\." - with pytest.raises(ValueError, match=match): + with pytest.raises(TypeError, match=match): instance.response = "INVALID" @@ -415,7 +415,7 @@ def test_response_handler_00080(response_handler) -> None: - result.setter Summary - - Verify ``ValueError`` is raised when result is not a dict. + - Verify ``TypeError`` is raised when result is not a dict. """ with does_not_raise(): @@ -423,5 +423,5 @@ def test_response_handler_00080(response_handler) -> None: match = r"ResponseHandler\.result:\s+" match += r"ResponseHandler\.result must be a dict\.\s+" match += r"Got INVALID\." - with pytest.raises(ValueError, match=match): + with pytest.raises(TypeError, match=match): instance.result = "INVALID" From 73969c2b0aef6635e7688f2d597f0506fa0a80a8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 30 May 2024 08:48:48 -1000 Subject: [PATCH 099/230] RestSend() v2: Update usage sections of docstring --- plugins/module_utils/common/rest_send_v2.py | 33 +++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index ff95ba66e..34596a382 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -33,20 +33,36 @@ class RestSend: """ ### Summary - Send REST requests to the controller with retries, and handle responses. - - ### Usage - Below we are using a Sender() class that requires an instance of - AnsibleModule, and uses dcnm_send() to send requests to the controller. - See dcnm_sender.py for details about implementing Sender() classes. - + - Send REST requests to the controller with retries. + - Accepts a ``Sender()`` class that implements the sender interface. + - The sender interface is defined in + ``module_utils/common/dcnm_sender.py`` + - Accepts a ``ResponseHandler()`` class that implements the response + handler interface. + - The response handler interface is defined in + ``module_utils/common/response_handler.py`` + + ### Usage discussion + - A Sender() class is used in the usage example below that requires an + instance of ``AnsibleModule``, and uses ``dcnm_send()`` to send + requests to the controller. + - See ``module_utils/common/dcnm_sender.py`` for details about + implementing ``Sender()`` classes. + - A ResponseHandler() class is used in the usage example below that + abstracts controller response handling. It accepts a controller + response dict and returns a result dict. + - See ``module_utils/common/response_handler.py`` for details + about implementing ``ResponseHandler()`` classes. + + ### Usage example ```python sender = Sender() # class that implements the sender interface sender.ansible_module = ansible_module rest_send = RestSend() - rest_send.unit_test = True # optional, use in unit tests for speed rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + rest_send.unit_test = True # optional, use in unit tests for speed rest_send.path = "/rest/top-down/fabrics" rest_send.verb = "GET" rest_send.payload = my_payload # optional @@ -56,7 +72,6 @@ class RestSend: # Do things with rest_send... rest_send.commit() rest_send.restore_settings() # restore check_mode and timeout - rest_send.commit() # list of responses from the controller for this session response = rest_send.response From d597bcba184b9c59db286cc259687f27944c004f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 30 May 2024 15:14:15 -1000 Subject: [PATCH 100/230] Hardening and docstring updates module_utils/common/dcnm_send.py: - Update class docstring Raises section to include all exceptions that should be caught. - Update class docstring Usage section to include appropriate try-except block. - Update commit() docstring Raises section to remove AnsibleModule.fail_json() and add all cases where exceptions might be reaised. rest_send_v2.py: - commit_normal_mode(): - Update docstring Raises section. - Add try-except block around _verify_commit_parameters() - Add try-except block around sender.commit() --- plugins/module_utils/common/dcnm_sender.py | 25 ++++++++++++++++----- plugins/module_utils/common/rest_send_v2.py | 17 ++++++++++---- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/plugins/module_utils/common/dcnm_sender.py b/plugins/module_utils/common/dcnm_sender.py index 5bb038265..12cb25396 100644 --- a/plugins/module_utils/common/dcnm_sender.py +++ b/plugins/module_utils/common/dcnm_sender.py @@ -34,15 +34,26 @@ class Sender: ``sender`` interface using dcnm_send. ### Raises - - ``ValueError`` if ``ansible_module`` is not set. + - ``ValueError`` if: + - ``ansible_module`` is not set. + - ``path`` is not set. + - ``verb`` is not set. + - ``TypeError`` if: + - ``ansible_module`` is not an instance of AnsibleModule. + - ``payload`` is not a ``dict``. + - ``response`` is not a ``dict``. + ### Usage ``ansible_module`` is an instance of ``AnsibleModule``. ```python sender = Sender() - sender.ansible_module = ansible_module - rest_send = RestSend() - rest_send.sender = sender + try: + sender.ansible_module = ansible_module + rest_send = RestSend() + rest_send.sender = sender + except (TypeError, ValueError) as error: + handle_error(error) # etc... # See rest_send_v2.py for RestSend() usage. ``` @@ -92,7 +103,11 @@ def commit(self): Send the REST request to the controller ### Raises - - AnsibleModule.fail_json() if the response is not a dict + - ``ValueError`` if: + - ``ansible_module`` is not set. + - ``path`` is not set. + - ``verb`` is not set. + ### Properties read - ``verb``: HTTP verb e.g. GET, POST, PUT, DELETE - ``path``: HTTP path e.g. http://controller_ip/path/to/endpoint diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 34596a382..572e40d0b 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -261,7 +261,9 @@ def commit_normal_mode(self): Call dcnm_send() with retries until successful response or timeout is exceeded. ### Raises - - AnsibleModule.fail_json() if the response is not a dict + - ``ValueError`` if: + - HandleResponse() raises ``ValueError`` + - Sender().commit() raises ``ValueError`` ### Properties read - ``send_interval``: interval between retries (set in ImageUpgradeCommon) - ``timeout``: timeout in seconds (set in ImageUpgradeCommon) @@ -276,7 +278,11 @@ def commit_normal_mode(self): method_name = inspect.stack()[0][3] caller = inspect.stack()[1][3] - self._verify_commit_parameters() + try: + self._verify_commit_parameters() + except ValueError as error: + raise ValueError(error) from error + try: timeout = self.timeout except AttributeError: @@ -296,9 +302,12 @@ def commit_normal_mode(self): msg += f"caller: {caller}. " msg += f"Calling sender.commit(): verb {self.verb}, path {self.path}" - self.sender.commit() - self.response_current = self.sender.response + try: + self.sender.commit() + except ValueError as error: + raise ValueError(error) from error + self.response_current = self.sender.response # Handle controller response and derive result try: self.response_handler.response = self.response_current From 6ab07024edeb734c72a2f35e560c50d7d9304a8c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 31 May 2024 11:29:00 -1000 Subject: [PATCH 101/230] MockSender(): mock the Sender() class 1. MockSender(): mock the Sender() class to return simulated responses. We added a gen property to set the generator 2. test_maintenance_mode.py - Update to use MockSender() rather than mocking dcnm_send() 3. test_maintenance_mode.py - Update to import RestSend() version 2 since this is what MaintenanceMode() is using. 4. test_maintenance_mode.py - renumber test case 00120 to 00220. 5. MaintenanceMode(): Change a few error messages for consistency. --- .../module_utils/common/maintenance_mode.py | 19 +-- .../unit/module_utils/common/common_utils.py | 85 ++++++++++++ .../fixtures/responses_ConfigDeploy.json | 2 +- .../fixtures/responses_MaintenanceMode.json | 2 +- .../common/test_maintenance_mode.py | 129 +++++++++++++++--- 5 files changed, 204 insertions(+), 33 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 8efcf48f7..855c8fc6e 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -185,7 +185,7 @@ def verify_config_parameters(self, value) -> None: self.verify_ip_address(item) self.verify_mode(item) self.verify_serial_number(item) - except ValueError as error: + except (TypeError, ValueError) as error: raise ValueError(error) from error def verify_deploy(self, item) -> None: @@ -196,17 +196,18 @@ def verify_deploy(self, item) -> None: ### Raises - ``ValueError`` if: - ``deploy`` is not present. + - ``TypeError`` if: - `deploy`` is not a boolean. """ method_name = inspect.stack()[0][3] if item.get("deploy", None) is None: msg = f"{self.class_name}.{method_name}: " - msg += "deploy must be present in config." + msg += "config is missing mandatory key: deploy." raise ValueError(msg) if not isinstance(item.get("deploy", None), bool): msg = f"{self.class_name}.{method_name}: " msg += "deploy must be a boolean." - raise ValueError(msg) + raise TypeError(msg) def verify_fabric_name(self, item) -> None: """ @@ -221,7 +222,7 @@ def verify_fabric_name(self, item) -> None: method_name = inspect.stack()[0][3] if item.get("fabric_name", None) is None: msg = f"{self.class_name}.{method_name}: " - msg += "fabric_name must be present in config." + msg += "config is missing mandatory key: fabric_name." raise ValueError(msg) try: self.conversion.validate_fabric_name(item.get("fabric_name", None)) @@ -240,7 +241,7 @@ def verify_ip_address(self, item) -> None: method_name = inspect.stack()[0][3] if item.get("ip_address", None) is None: msg = f"{self.class_name}.{method_name}: " - msg += "ip_address must be present in config." + msg += "config is missing mandatory key: ip_address." raise ValueError(msg) def verify_mode(self, item) -> None: @@ -256,7 +257,7 @@ def verify_mode(self, item) -> None: method_name = inspect.stack()[0][3] if item.get("mode", None) is None: msg = f"{self.class_name}.{method_name}: " - msg += "mode is mandatory, but is missing from the config." + msg += "config is missing mandatory key: mode." raise ValueError(msg) if item.get("mode", None) not in self.valid_modes: msg = f"{self.class_name}.{method_name}: " @@ -276,7 +277,7 @@ def verify_serial_number(self, item) -> None: method_name = inspect.stack()[0][3] if item.get("serial_number", None) is None: msg = f"{self.class_name}.{method_name}: " - msg += "serial_number must be present in config." + msg += "config is missing mandatory key: serial_number." raise ValueError(msg) def verify_commit_parameters(self) -> None: @@ -324,7 +325,7 @@ def commit(self) -> None: """ try: self.verify_commit_parameters() - except ValueError as error: + except (TypeError, ValueError) as error: raise ValueError(error) from error try: @@ -578,7 +579,7 @@ def config(self) -> list: def config(self, value): try: self.verify_config_parameters(value) - except ValueError as error: + except (TypeError, ValueError) as error: raise ValueError(error) from error self._properties["config"] = value diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index a24239de5..2c25dfc09 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -85,6 +85,91 @@ def public_method_for_pylint(self) -> Any: """ +class MockSender: + """ + Mock the Sender class + + ### Usage + Typically, ``def responses()`` would yield a file reader with a + key into a json file. + + For example + ``` + def responses(): + yield responses_maintenance_mode(key) + yield responses_config_deploy(key) + ``` + + Below we are yielding dictionaries directly for simplicity. + + ```python + def responses(): + yield {"key1": "value1"} + yield {"key2": "value2"} + + sender = MockSender() + sender.gen = ResponseGenerator(responses()) + + rest_send = RestSend() + rest_send.sender = sender + # rest of test case... + """ + + def __init__(self): + self.class_name = "Sender" + self.properties = {} + self.properties["gen"] = None + + def commit(self): + """ + do nothing + """ + + @property + def gen(self): + """ + - getter: Return the ``ResponseGenerator()`` instance. + - setter: Set the ``ResponseGenerator()`` instance that provides + simulated responses. + """ + return self.properties["gen"] + + @gen.setter + def gen(self, value): + self.properties["gen"] = value + + @property + def response(self): + """ + return the simulated response + """ + return self.gen.next + + @response.setter + def response(self, *args, **kwargs): + pass + + @property + def path(self): + """ + do nothing + """ + + @path.setter + def path(self, *args, **kwargs): + pass + + @property + def verb(self): + """ + do nothing + """ + + @verb.setter + def verb(self, *args, **kwargs): + pass + + class MockAnsibleModule: """ Mock the AnsibleModule class diff --git a/tests/unit/module_utils/common/fixtures/responses_ConfigDeploy.json b/tests/unit/module_utils/common/fixtures/responses_ConfigDeploy.json index 415500b08..e147169ca 100644 --- a/tests/unit/module_utils/common/fixtures/responses_ConfigDeploy.json +++ b/tests/unit/module_utils/common/fixtures/responses_ConfigDeploy.json @@ -1,5 +1,5 @@ { - "test_maintenance_mode_00120a": { + "test_maintenance_mode_00220a": { "DATA": { "status": "Configuration deployment completed." }, diff --git a/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json b/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json index ab09b5e92..116ddd228 100644 --- a/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json +++ b/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json @@ -1,5 +1,5 @@ { - "test_maintenance_mode_00120a": { + "test_maintenance_mode_00220a": { "DATA": { "status": "Success" }, diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 8681446f6..4be7fe3d6 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -29,6 +29,7 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import copy import inspect import pytest @@ -40,14 +41,15 @@ ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import \ MaintenanceMode -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - maintenance_mode_fixture, params, responses_config_deploy, - responses_maintenance_mode) + MockSender, ResponseGenerator, does_not_raise, maintenance_mode_fixture, + params, responses_config_deploy, responses_maintenance_mode) FABRIC_NAME = "VXLAN_Fabric" CONFIG = [ @@ -128,6 +130,7 @@ def test_maintenance_mode_00030(maintenance_mode) -> None: Classes and Methods - MaintenanceMode() - __init__() + - verify_commit_parameters() - commit() Summary @@ -148,7 +151,7 @@ def test_maintenance_mode_00030(maintenance_mode) -> None: """ with does_not_raise(): instance = maintenance_mode - instance.rest_send = RestSend(MockAnsibleModule()) + instance.rest_send = RestSend({}) instance.results = Results() match = r"MaintenanceMode\.verify_commit_parameters: " @@ -163,6 +166,7 @@ def test_maintenance_mode_00040(maintenance_mode) -> None: Classes and Methods - MaintenanceMode() - __init__() + - verify_commit_parameters() - commit() Summary @@ -198,6 +202,7 @@ def test_maintenance_mode_00050(maintenance_mode) -> None: Classes and Methods - MaintenanceMode() - __init__() + - verify_commit_parameters() - commit() Summary @@ -218,7 +223,7 @@ def test_maintenance_mode_00050(maintenance_mode) -> None: """ with does_not_raise(): instance = maintenance_mode - instance.rest_send = RestSend(MockAnsibleModule) + instance.rest_send = RestSend({}) instance.config = CONFIG match = r"MaintenanceMode\.verify_commit_parameters: " @@ -236,7 +241,7 @@ def test_maintenance_mode_00050(maintenance_mode) -> None: (ValueError, ValueError, "Bad value"), ], ) -def test_maintenance_mode_00100( +def test_maintenance_mode_00200( monkeypatch, maintenance_mode, mock_exception, expected_exception, mock_message ) -> None: """ @@ -272,7 +277,7 @@ def mock_change_system_mode(*args, **kwargs): with does_not_raise(): instance = maintenance_mode instance.config = CONFIG - instance.rest_send = RestSend(MockAnsibleModule) + instance.rest_send = RestSend({}) instance.results = Results() monkeypatch.setattr(instance, "change_system_mode", mock_change_system_mode) @@ -287,7 +292,7 @@ def mock_change_system_mode(*args, **kwargs): (ValueError, ValueError, "Bad value"), ], ) -def test_maintenance_mode_00110( +def test_maintenance_mode_00210( monkeypatch, maintenance_mode, mock_exception, expected_exception, mock_message ) -> None: """ @@ -326,7 +331,7 @@ def mock_deploy_switches(*args, **kwargs): with does_not_raise(): instance = maintenance_mode instance.config = CONFIG - instance.rest_send = RestSend(MockAnsibleModule) + instance.rest_send = RestSend({}) instance.results = Results() monkeypatch.setattr(instance, "change_system_mode", mock_change_system_mode) @@ -335,7 +340,7 @@ def mock_deploy_switches(*args, **kwargs): instance.commit() -def test_maintenance_mode_00120(monkeypatch, maintenance_mode) -> None: +def test_maintenance_mode_00220(maintenance_mode) -> None: """ Classes and Methods - MaintenanceMode() @@ -351,7 +356,7 @@ def test_maintenance_mode_00120(monkeypatch, maintenance_mode) -> None: Code Flow - Setup - MaintenanceMode() is instantiated - - dcnm_send() is patched to return the mocked controller responses + - Sender() is mocked to return expected responses - Required attributes are set - MaintenanceMode().commit() is called - responses_MaintenanceMode contains a dict with: @@ -369,29 +374,27 @@ def test_maintenance_mode_00120(monkeypatch, maintenance_mode) -> None: method_name = inspect.stack()[0][3] key = f"{method_name}a" - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" - def responses(): yield responses_maintenance_mode(key) yield responses_config_deploy(key) - gen = ResponseGenerator(responses()) + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) - def mock_dcnm_send(*args, **kwargs): - item = gen.next - return item + PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." + PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" with does_not_raise(): + rest_send = RestSend({"state": "merged", "check_mode": False}) + rest_send.sender = mock_sender + rest_send.response_handler = ResponseHandler() instance = maintenance_mode - instance.rest_send = RestSend(MockAnsibleModule()) + instance.rest_send = rest_send instance.rest_send.unit_test = True instance.rest_send.timeout = 1 instance.results = Results() instance.config = CONFIG - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) - with does_not_raise(): instance.commit() @@ -432,3 +435,85 @@ def mock_dcnm_send(*args, **kwargs): assert instance.results.result[1].get("changed", None) is True assert instance.results.result[1].get("success", None) is True + + +def test_maintenance_mode_00300(maintenance_mode) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - verify_config_parameters() + - config.setter + + Summary + - Verify MaintenanceMode().verify_config_parameters() raises + - ``TypeError`` if: + - value is not a list + - Verify MaintenanceMode().config.setter re-raises: + - ``TypeError`` as ``ValueError`` + + Code Flow - Setup + - MaintenanceMode() is instantiated + - config is set to a non-list value + + Code Flow - Test + - MaintenanceMode().config.setter is accessed with non-list + + Expected Result + - verify_config_parameters() raises ``TypeError``. + - config.setter re-raises as ``ValueError``. + - Exception message matches expected. + """ + with does_not_raise(): + instance = maintenance_mode + match = r"MaintenanceMode\.verify_config_parameters:\s+" + match += r"MaintenanceMode\.config must be a list\.\s+" + match += r"Got type: str\." + with pytest.raises(ValueError, match=match): + instance.config = "NOT_A_LIST" + + +@pytest.mark.parametrize( + "remove_param", + # ["deploy", "fabric_name", "ip_address", "mode", "serial_number"], + [("deploy"), ("fabric_name"), ("ip_address"), ("mode"), ("serial_number")], +) +def test_maintenance_mode_00310(maintenance_mode, remove_param) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - verify_config_parameters() + - config.setter + + Summary + - Verify MaintenanceMode().verify_config_parameters() raises + - ``ValueError`` if: + - deploy is missing from config + - fabric_name is missing from config + - ip_address is missing from config + - mode is missing from config + - serial_number is missing from config + + + Code Flow - Setup + - MaintenanceMode() is instantiated + + Code Flow - Test + - MaintenanceMode().config is set to a dict with all of the above + keys present, except that each key, in turn, is removed. + + Expected Result + - ``ValueError`` is raised + - Exception message matches expected + """ + + with does_not_raise(): + instance = maintenance_mode + + config = copy.deepcopy(CONFIG[0]) + del config[remove_param] + match = rf"MaintenanceMode\.verify_{remove_param}:\s+" + match += rf"config is missing mandatory key: {remove_param}\." + with pytest.raises(ValueError, match=match): + instance.config = [config] From 2c64941432a53a36f47d547f7b8697781887f13e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 31 May 2024 14:44:07 -1000 Subject: [PATCH 102/230] MaintenanceMode(): add/update unit tests. 1. MaintenanceMode().deploy: Update error message. 2. test_maintenance_mode_00220: - update to test normal mode as well. 3. test_maintenance_mode_00400 - Verify MaintenanceMode().verify_config_parameters() re-raises - ``ValueError`` if: - ``deploy`` raises ``TypeError`` 4. test_maintenance_mode_00500: - Verify MaintenanceMode().verify_config_parameters() re-raises - ``ValueError`` if: - ``fabric_name`` raises ``ValueError`` due to being an invalid value. 5. test_maintenance_mode_00600: - Verify MaintenanceMode().verify_config_parameters() re-raises - ``ValueError`` if: - ``mode`` raises ``ValueError`` due to being an invalid value. --- .../module_utils/common/maintenance_mode.py | 4 +- .../common/test_maintenance_mode.py | 180 +++++++++++++++++- 2 files changed, 177 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 855c8fc6e..e1cec9c4f 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -206,7 +206,9 @@ def verify_deploy(self, item) -> None: raise ValueError(msg) if not isinstance(item.get("deploy", None), bool): msg = f"{self.class_name}.{method_name}: " - msg += "deploy must be a boolean." + msg += "Expected boolean for deploy. " + msg += f"Got type {type(item).__name__}, " + msg += f"value {item.get('deploy', None)}." raise TypeError(msg) def verify_fabric_name(self, item) -> None: diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 4be7fe3d6..00e593157 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -340,7 +340,14 @@ def mock_deploy_switches(*args, **kwargs): instance.commit() -def test_maintenance_mode_00220(maintenance_mode) -> None: +@pytest.mark.parametrize( + "mode", + [ + ("maintenance"), + ("normal"), + ], +) +def test_maintenance_mode_00220(maintenance_mode, mode) -> None: """ Classes and Methods - MaintenanceMode() @@ -381,8 +388,8 @@ def responses(): mock_sender = MockSender() mock_sender.gen = ResponseGenerator(responses()) - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + config = copy.deepcopy(CONFIG[0]) + config["mode"] = mode with does_not_raise(): rest_send = RestSend({"state": "merged", "check_mode": False}) @@ -393,7 +400,7 @@ def responses(): instance.rest_send.unit_test = True instance.rest_send.timeout = 1 instance.results = Results() - instance.config = CONFIG + instance.config = [config] with does_not_raise(): instance.commit() @@ -404,7 +411,7 @@ def responses(): assert isinstance(instance.results.result, list) assert instance.results.diff[0].get("fabric_name", None) == FABRIC_NAME assert instance.results.diff[0].get("ip_address", None) == "192.168.1.2" - assert instance.results.diff[0].get("maintenance_mode", None) == "maintenance" + assert instance.results.diff[0].get("maintenance_mode", None) == mode assert instance.results.diff[0].get("sequence_number", None) == 1 assert instance.results.diff[0].get("serial_number", None) == "FDO22180ASJ" @@ -475,7 +482,6 @@ def test_maintenance_mode_00300(maintenance_mode) -> None: @pytest.mark.parametrize( "remove_param", - # ["deploy", "fabric_name", "ip_address", "mode", "serial_number"], [("deploy"), ("fabric_name"), ("ip_address"), ("mode"), ("serial_number")], ) def test_maintenance_mode_00310(maintenance_mode, remove_param) -> None: @@ -517,3 +523,165 @@ def test_maintenance_mode_00310(maintenance_mode, remove_param) -> None: match += rf"config is missing mandatory key: {remove_param}\." with pytest.raises(ValueError, match=match): instance.config = [config] + + +@pytest.mark.parametrize( + "param, raises", + [ + (False, None), + (True, None), + (10, ValueError), + ("FOO", ValueError), + (["FOO"], ValueError), + ({"FOO": "BAR"}, ValueError), + ], +) +def test_maintenance_mode_00400(maintenance_mode, param, raises) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - verify_config_parameters() + - config.setter + + Summary + - Verify MaintenanceMode().verify_config_parameters() re-raises + - ``ValueError`` if: + - ``deploy`` raises ``TypeError`` + + Code Flow - Setup + - MaintenanceMode() is instantiated + + Code Flow - Test + - MaintenanceMode().config is set to a dict. + - The dict is updated with deploy set to valid and invalid + values of ``deploy`` + + Expected Result + - ``ValueError`` is raised when deploy is not a boolean + - Exception message matches expected + - Exception is not raised when deploy is a boolean + """ + + with does_not_raise(): + instance = maintenance_mode + + config = copy.deepcopy(CONFIG[0]) + config["deploy"] = param + match = r"MaintenanceMode\.verify_deploy:\s+" + match += r"Expected boolean for deploy\.\s+" + match += r"Got type\s+" + if raises: + with pytest.raises(raises, match=match): + instance.config = [config] + else: + instance.config = [config] + assert instance.config[0]["deploy"] == param + + +@pytest.mark.parametrize( + "param, raises", + [ + ("MyFabric", None), + ("MyFabric_123", None), + ("10MyFabric", ValueError), + ("_MyFabric", ValueError), + ("MyFabric&BadFabric", ValueError), + ], +) +def test_maintenance_mode_00500(maintenance_mode, param, raises) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - verify_config_parameters() + - config.setter + + Summary + - Verify MaintenanceMode().verify_config_parameters() re-raises + - ``ValueError`` if: + - ``fabric_name`` raises ``ValueError`` due to being an + invalid value. + + Code Flow - Setup + - MaintenanceMode() is instantiated + + Code Flow - Test + - MaintenanceMode().config is set to a dict. + - The dict is updated with fabric_name set to valid and invalid + values of ``fabric_name`` + + Expected Result + - ``ValueError`` is raised when fabric_name is not a valid value + - Exception message matches expected + - Exception is not raised when fabric_name is a valid value + """ + + with does_not_raise(): + instance = maintenance_mode + + config = copy.deepcopy(CONFIG[0]) + config["fabric_name"] = param + match = r"ConversionUtils\.validate_fabric_name:\s+" + match += rf"Invalid fabric name: {param}\.\s+" + match += r"Fabric name must start with a letter A-Z or a-z and contain\s+" + match += r"only the characters in:" + if raises: + with pytest.raises(raises, match=match): + instance.config = [config] + else: + instance.config = [config] + assert instance.config[0]["fabric_name"] == param + + +@pytest.mark.parametrize( + "param, raises", + [ + ("maintenance", None), + ("normal", None), + (10, ValueError), + (["192.168.1.2"], ValueError), + ({"ip_address": "192.168.1.2"}, ValueError), + ], +) +def test_maintenance_mode_00600(maintenance_mode, param, raises) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - verify_config_parameters() + - config.setter + + Summary + - Verify MaintenanceMode().verify_config_parameters() re-raises + - ``ValueError`` if: + - ``mode`` raises ``ValueError`` due to being an + invalid value. + + Code Flow - Setup + - MaintenanceMode() is instantiated + + Code Flow - Test + - MaintenanceMode().config is set to a dict. + - The dict is updated with mode set to valid and invalid + values of ``mode`` + + Expected Result + - ``ValueError`` is raised when mode is not a valid value + - Exception message matches expected + - Exception is not raised when mode is a valid value + """ + + with does_not_raise(): + instance = maintenance_mode + + config = copy.deepcopy(CONFIG[0]) + config["mode"] = param + match = r"MaintenanceMode\.verify_mode:\s+" + match += r"mode must be one of\s+" + if raises: + with pytest.raises(raises, match=match): + instance.config = [config] + else: + instance.config = [config] + assert instance.config[0]["mode"] == param From 8bfdaf47bf6685a36d7662dbb4af7c9b009abeb7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 1 Jun 2024 10:08:23 -1000 Subject: [PATCH 103/230] Log() version 2. Simplify usage. 1. Log(): new version 2 class. This simplifies usage within our main module files to two lines: log = Log() log.commit() 2. Log(): Add a 'develop' property to enable exceptions from the logging system itself. By default, this is disabled (False). 3. Log(): Ensure that the logging config file does not specify any logging handers that emit to console, stderr, stdout (since these latter two could be redirected to the console). 4. Log(): Update docstring with usage examples and an example logging config file. 5. dcnm_maintenance_mode.py: Use the Log() version 2 class. --- plugins/module_utils/common/log_v2.py | 352 +++++++++++++++++++++++ plugins/modules/dcnm_maintenance_mode.py | 26 +- 2 files changed, 357 insertions(+), 21 deletions(-) create mode 100644 plugins/module_utils/common/log_v2.py diff --git a/plugins/module_utils/common/log_v2.py b/plugins/module_utils/common/log_v2.py new file mode 100644 index 000000000..0b69b1d5a --- /dev/null +++ b/plugins/module_utils/common/log_v2.py @@ -0,0 +1,352 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import json +import logging +from logging.config import dictConfig +from os import environ + + +class Log: + """ + ### Summary + Create the base dcnm logging object. + + ### Raises + - ``ValueError`` if: + - An error is encountered reading the logging config file. + - An error is encountered parsing the logging config file. + + ### Usage + + By default, Log() does the following: + + 1. Reads the environment variable ``NDFC_LOGGING_CONFIG`` to determine + the path to the logging config file. If the environment variable is + not set, then logging is disabled. + 2. Sets ``develop`` to False. This disables exceptions raised by the + logging module itself. + + Hence, the simplest usage for Log() is: + + - Set the environment variable ``NDFC_LOGGING_CONFIG`` to the + path of the logging config file. ``bash`` shell is used in the + example below. + + ```bash + export NDFC_LOGGING_CONFIG="/path/to/logging_config.json" + ``` + + - Instantiate a Log() object instance and call ``commit()`` on the instance: + + ```python + from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log + try: + log = Log() + log.commit() + except ValueError as error: + # handle error + ``` + + To later disable logging, unset the environment variable. + ``bash`` shell is used in the example below. + + ```bash + unset NDFC_LOGGING_CONFIG + ``` + + To enable exceptions from the logging module (not recommended, unless needed for + development), set ``develop`` to True: + + ```python + from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log + try: + log = Log() + log.develop = True + log.commit() + except ValueError as error: + # handle error + ``` + + To directly set the path to the logging config file, overriding the + ``NDFC_LOGGING_CONFIG`` environment variable, set the ``config`` + property prior to calling ``commit()``: + + ```python + from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log + try: + log = Log() + log.config = "/path/to/logging_config.json" + log.commit() + except ValueError as error: + # handle error + ``` + + At this point, a base/parent logger is created for which all other + loggers throughout the dcnm collection will be children. + This allows for a single logging config to be used for all modules in the + collection, and allows for the logging config to be specified in a + single place external to the code. + + ### Example module code using the Log() object + + In the main() function of a module. + ```python + from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log + + def main(): + try: + log = Log() + log.commit() + except ValueError as error: + ansible_module.fail_json(msg=str(error)) + + task = AnsibleTask() + ``` + + In the AnsibleTask() class (or any other classes running in the + main() function's call stack i.e. classes instantiated in either + main() or in AnsibleTask()). + + ```python + class AnsibleTask: + def __init__(self): + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + def some_method(self): + self.log.debug("This is a debug message.") + ``` + + ### Logging Config File + The logging config file MUST conform to ``logging.config.dictConfig`` + from Python's standard library and MUST NOT contain any handlers or + that log to stdout or stderr. The logging config file MUST only + contain handlers that log to files. + + An example logging config file is shown below: + + ```json + { + "version": 1, + "formatters": { + "standard": { + "class": "logging.Formatter", + "format": "%(asctime)s - %(levelname)s - [%(name)s.%(funcName)s.%(lineno)d] %(message)s" + } + }, + "handlers": { + "file": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "standard", + "level": "DEBUG", + "filename": "/tmp/dcnm.log", + "mode": "a", + "encoding": "utf-8", + "maxBytes": 50000000, + "backupCount": 4 + } + }, + "loggers": { + "dcnm": { + "handlers": [ + "file" + ], + "level": "DEBUG", + "propagate": false + } + }, + "root": { + "level": "INFO", + "handlers": [ + "file" + ] + } + } + ``` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + # Disable exceptions raised by the logging module. + # Set this to True during development to catch logging errors. + logging.raiseExceptions = False + + self._build_properties() + + def _build_properties(self) -> None: + self.properties = {} + self.properties["config"] = environ.get("NDFC_LOGGING_CONFIG", None) + + def disable_logging(self): + """ + ### Summary + - Disable logging by removing all handlers from the base logger. + + ### Raises + None + """ + logger = logging.getLogger() + for handler in logger.handlers.copy(): + try: + logger.removeHandler(handler) + except ValueError: # if handler already removed + pass + logger.addHandler(logging.NullHandler()) + logger.propagate = False + + def enable_logging(self): + """ + ### Summary + - Enable logging by reading the logging config file and configuring + the base logger instance. + ### Raises + - ``ValueError`` if: + - An error is encountered reading the logging config file. + """ + if str(self.config).strip() == "": + return + + try: + with open(self.config, "r", encoding="utf-8") as file: + try: + logging_config = json.load(file) + except json.JSONDecodeError as error: + msg = f"error parsing logging config from {self.config}. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error + except IOError as error: + msg = f"error reading logging config from {self.config}. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error + + try: + self.validate_logging_config(logging_config) + except ValueError as error: + raise ValueError(str(error)) from error + + try: + dictConfig(logging_config) + except (RuntimeError, TypeError, ValueError) as error: + msg = "logging.config.dictConfig: " + msg += f"Unable to configure logging from {self.config}. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error + + def validate_logging_config(self, logging_config: dict) -> None: + """ + ### Summary + - Validate the logging config file. + - Ensure that the logging config file does not contain any handlers + that log to console, stdout, or stderr. + + ### Raises + - ``ValueError`` if: + - The logging config file contains a handler that logs to + console, stdout, or stderr. + + ### Usage + ```python + log = Log() + log.config = "/path/to/logging_config.json" + log.commit() + ``` + """ + for handler in logging_config.get("handlers", {}): + if handler in ["console", "stderr", "stdout"]: + msg = f"logging config file {self.config} contains a handler " + msg += "that logs to console, stdout, or stderr. This will " + msg += "break Ansible module execution. Remove these handlers " + msg += "from the logging config file and try again. " + msg += f"Handler: {handler}" + raise ValueError(msg) + + def commit(self): + """ + ### Summary + - If ``config`` is None, disable logging. + - If ``config`` is a JSON file conformant with + ``logging.config.dictConfig``, read the file and configure the + base logger instance from the file's contents. + + ### Raises + - ``ValueError`` if: + - An error is encountered reading the logging config file. + + ### Notes + 1. If self.config is None, then logging is disabled. + 2. If self.config is a path to a JSON file, then the file is read + and logging is configured from the file. + + ### Usage + ```python + log = Log() + log.config = "/path/to/logging_config.json" + log.commit() + ``` + """ + if self.config is None: + self.disable_logging() + else: + self.enable_logging() + + @property + def config(self): + """ + ### Summary + Path to a JSON file from which logging config is read. + JSON file must conform to ``logging.config.dictConfig`` from Python's + standard library. + + ### Default + If the environment variable ``NDFC_LOGGING_CONFIG`` is set, then + the value of that variable is used. Otherwise, None. + + The environment variable can be overridden by directly setting + ``config`` to one of the following prior to calling ``commit()``: + + 1. None. Logging will be disabled. + 2. Path to a JSON file from which logging config is read. + Must conform to ``logging.config.dictConfig`` from Python's + standard library. + """ + return self.properties["config"] + + @config.setter + def config(self, value): + self.properties["config"] = value + + @property + def develop(self): + """ + ### Summary + Disable or enable exceptions raised by the logging module. + + ### Default + False + + ### Valid Values + - ``True``: Exceptions will be raised by the logging module. + - ``False``: Exceptions will not be raised by the logging module. + """ + return self.properties["develop"] + + @develop.setter + def develop(self, value): + logging.raiseExceptions = value diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index f0ac62bd9..a5b01f1b6 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -125,12 +125,12 @@ import inspect import json import logging -from os import environ from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.common.dcnm_sender import \ Sender -from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import \ + Log from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import ( MaintenanceMode, MaintenanceModeInfo) from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ @@ -1219,28 +1219,12 @@ def main(): ansible_module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True ) - log = Log(ansible_module) - - # Create the base/parent logger for the dcnm collection. - # Set the following environment variable to enable logging: - # - NDFC_LOGGING_CONFIG= - # logging_config.json must be must be conformant with logging.config.dictConfig - # and must not log to the console. - # For an example logging_config.json configuration, see: - # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json - config_file = environ.get("NDFC_LOGGING_CONFIG", None) - if config_file is not None: - log.config = config_file + # Logging setup try: + log = Log() log.commit() - except json.decoder.JSONDecodeError as error: - msg = f"Invalid logging configuration file: {log.config}. " - msg += f"Error detail: {error}" - ansible_module.fail_json(msg) except ValueError as error: - msg = f"Invalid logging configuration file: {log.config}. " - msg += f"Error detail: {error}" - ansible_module.fail_json(msg) + ansible_module.fail_json(str(error)) ansible_module.params["check_mode"] = ansible_module.check_mode From 642ed0bcab18527afa507f5fde5707bbed60346f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 1 Jun 2024 17:00:35 -1000 Subject: [PATCH 104/230] Log() v2: 96% unit test coverage --- plugins/module_utils/common/log_v2.py | 8 + tests/unit/module_utils/common/test_log_v2.py | 372 ++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 tests/unit/module_utils/common/test_log_v2.py diff --git a/plugins/module_utils/common/log_v2.py b/plugins/module_utils/common/log_v2.py index 0b69b1d5a..7abd0a70a 100644 --- a/plugins/module_utils/common/log_v2.py +++ b/plugins/module_utils/common/log_v2.py @@ -18,6 +18,7 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import inspect import json import logging from logging.config import dictConfig @@ -193,6 +194,7 @@ def __init__(self): def _build_properties(self) -> None: self.properties = {} self.properties["config"] = environ.get("NDFC_LOGGING_CONFIG", None) + self.properties["develop"] = False def disable_logging(self): """ @@ -349,4 +351,10 @@ def develop(self): @develop.setter def develop(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: Expected boolean for develop. " + msg += f"Got: type {type(value).__name__} for value {value}." + raise TypeError(msg) + self.properties["develop"] = value logging.raiseExceptions = value diff --git a/tests/unit/module_utils/common/test_log_v2.py b/tests/unit/module_utils/common/test_log_v2.py new file mode 100644 index 000000000..925d80d51 --- /dev/null +++ b/tests/unit/module_utils/common/test_log_v2.py @@ -0,0 +1,372 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import inspect +import json +import logging +from os import environ + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import \ + Log +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + does_not_raise + + +def logging_config(logging_config_file) -> dict: + """ + ### Summary + Return a logging configuration conformant with dictConfig. + """ + return { + "version": 1, + "formatters": { + "standard": { + "class": "logging.Formatter", + "format": "%(asctime)s - %(levelname)s - [%(name)s.%(funcName)s.%(lineno)d] %(message)s", + } + }, + "handlers": { + "file": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "standard", + "level": "DEBUG", + "filename": logging_config_file, + "mode": "a", + "encoding": "utf-8", + "maxBytes": 500000, + "backupCount": 4, + } + }, + "loggers": { + "dcnm": {"handlers": ["file"], "level": "DEBUG", "propagate": False} + }, + "root": {"level": "INFO", "handlers": ["file"]}, + } + + +def test_log_v2_00010(tmp_path) -> None: + """ + ### Methods + - Log().commit() + + ### Test + - Happy path. + - log. logs to the logfile. + - The log message contains the calling method's name. + """ + method_name = inspect.stack()[0][3] + log_dir = tmp_path / "log_dir" + log_dir.mkdir() + config_file = log_dir / "logging_config.json" + log_file = log_dir / "dcnm.log" + config = logging_config(str(log_file)) + with open(config_file, "w", encoding="UTF-8") as fp: + json.dump(config, fp) + + environ["NDFC_LOGGING_CONFIG"] = str(config_file) + + with does_not_raise(): + instance = Log() + instance.commit() + + info_msg = "foo" + debug_msg = "bing" + warning_msg = "bar" + critical_msg = "baz" + log = logging.getLogger("dcnm.test_logger") + log.info(info_msg) + log.debug(debug_msg) + log.warning(warning_msg) + log.critical(critical_msg) + assert logging.getLevelName(log.getEffectiveLevel()) == "DEBUG" + assert info_msg in log_file.read_text(encoding="UTF-8") + assert debug_msg in log_file.read_text(encoding="UTF-8") + assert warning_msg in log_file.read_text(encoding="UTF-8") + assert critical_msg in log_file.read_text(encoding="UTF-8") + # test that the log message includes the method name + assert method_name in log_file.read_text(encoding="UTF-8") + + +def test_log_v2_00100(tmp_path) -> None: + """ + ### Methods + - Log().commit() + + ### Test + - Nothing is logged when NDFC_LOGGING_CONFIG is not set + """ + log_dir = tmp_path / "log_dir" + log_dir.mkdir() + config_file = log_dir / "logging_config.json" + log_file = log_dir / "dcnm.log" + config = logging_config(str(log_file)) + with open(config_file, "w", encoding="UTF-8") as fp: + json.dump(config, fp) + + with does_not_raise(): + instance = Log() + instance.commit() + + info_msg = "foo" + debug_msg = "bing" + warning_msg = "bar" + critical_msg = "baz" + log = logging.getLogger("dcnm.test_logger") + log.info(info_msg) + log.debug(debug_msg) + log.warning(warning_msg) + log.critical(critical_msg) + # test that nothing was logged (file was not created) + with pytest.raises(FileNotFoundError): + log_file.read_text(encoding="UTF-8") + + +@pytest.mark.parametrize("env_var", [(""), (" ")]) +def test_log_v2_00110(tmp_path, env_var) -> None: + """ + ### Methods + - Log().commit() + + ### Test + - Nothing is logged when NDFC_LOGGING_CONFIG is set to an + an empty string. + """ + log_dir = tmp_path / "log_dir" + log_dir.mkdir() + config_file = log_dir / "logging_config.json" + log_file = log_dir / "dcnm.log" + config = logging_config(str(log_file)) + with open(config_file, "w", encoding="UTF-8") as fp: + json.dump(config, fp) + + environ["NDFC_LOGGING_CONFIG"] = env_var + + with does_not_raise(): + instance = Log() + instance.commit() + + info_msg = "foo" + debug_msg = "bing" + warning_msg = "bar" + critical_msg = "baz" + log = logging.getLogger("dcnm.test_logger") + log.info(info_msg) + log.debug(debug_msg) + log.warning(warning_msg) + log.critical(critical_msg) + # test that nothing was logged (file was not created) + with pytest.raises(FileNotFoundError): + log_file.read_text(encoding="UTF-8") + + +def test_log_v2_00120(tmp_path) -> None: + """ + ### Methods + - Log().commit() + + ### Test Setup + - NDFC_LOGGING_CONFIG is set to a file that exists, + which would normally enable logging. + - Log().config is set to None, which overrides NDFC_LOGGING_CONFIG. + + ### Test + - Nothing is logged becase Log().config overrides NDFC_LOGGING_CONFIG. + """ + log_dir = tmp_path / "log_dir" + log_dir.mkdir() + config_file = log_dir / "logging_config.json" + log_file = log_dir / "dcnm.log" + config = logging_config(str(log_file)) + with open(config_file, "w", encoding="UTF-8") as fp: + json.dump(config, fp) + + environ["NDFC_LOGGING_CONFIG"] = str(config_file) + + with does_not_raise(): + instance = Log() + instance.config = None + instance.commit() + + info_msg = "foo" + debug_msg = "bing" + warning_msg = "bar" + critical_msg = "baz" + log = logging.getLogger("dcnm.test_logger") + log.info(info_msg) + log.debug(debug_msg) + log.warning(warning_msg) + log.critical(critical_msg) + # test that nothing was logged (file was not created) + with pytest.raises(FileNotFoundError): + log_file.read_text(encoding="UTF-8") + + +def test_log_v2_00200() -> None: + """ + ### Methods + - Log().commit() + + ### Test + - ``ValueError`` is raised if logging config file does not exist. + """ + config_file = "DOES_NOT_EXIST.json" + environ["NDFC_LOGGING_CONFIG"] = config_file + + with does_not_raise(): + instance = Log() + + match = rf"error reading logging config from {config_file}\.\s+" + match += r"Error detail:\s+\[Errno 2\]\s+No such file or directory:\s+" + match += rf"\'{config_file}\'" + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_log_v2_00210(tmp_path) -> None: + """ + ### Methods + - Log().commit() + + ### Test + - ``ValueError`` is raised if logging config file contains invalid JSON. + """ + log_dir = tmp_path / "log_dir" + log_dir.mkdir() + config_file = log_dir / "logging_config.json" + with open(config_file, "w", encoding="UTF-8") as fp: + json.dump({"BAD": "JSON"}, fp) + + environ["NDFC_LOGGING_CONFIG"] = str(config_file) + + with does_not_raise(): + instance = Log() + + match = r"logging\.config\.dictConfig:\s+" + match += rf"Unable to configure logging from {config_file}\.\s+" + match += "Error detail: dictionary doesn't specify a version" + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_log_v2_00220(tmp_path) -> None: + """ + ### Methods + - Log().commit() + + ### Test + - ``ValueError`` is raised if logging config file does not contain JSON. + """ + log_dir = tmp_path / "log_dir" + log_dir.mkdir() + config_file = log_dir / "logging_config.json" + with open(config_file, "w", encoding="UTF-8") as fp: + fp.write("NOT JSON") + + environ["NDFC_LOGGING_CONFIG"] = str(config_file) + + with does_not_raise(): + instance = Log() + + match = rf"error parsing logging config from {config_file}\.\s+" + match += r"Error detail: Expecting value: line 1 column 1 \(char 0\)" + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_log_v2_00230(tmp_path) -> None: + """ + ### Methods + - Log().commit() + + ### Test + - ``ValueError`` is raised if logging config file contains a + handler that emits to console. + """ + log_dir = tmp_path / "log_dir" + log_dir.mkdir() + config_file = log_dir / "logging_config.json" + log_file = log_dir / "dcnm.log" + config = logging_config(str(log_file)) + config["handlers"]["console"] = { + "class": "logging.StreamHandler", + "formatter": "standard", + "level": "DEBUG", + "stream": "ext://sys.stdout", + } + with open(config_file, "w", encoding="UTF-8") as fp: + json.dump(config, fp) + + environ["NDFC_LOGGING_CONFIG"] = str(config_file) + + with does_not_raise(): + instance = Log() + + match = rf"logging config file .*{config_file}\s+" + match += r"contains a handler that logs to console, stdout, or stderr\.\s+" + match += r"This will break Ansible module execution\.\s+" + match += r"Remove these handlers from the logging config file and\s+" + match += r"try again. Handler: console" + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_log_v2_00300() -> None: + """ + ### Methods + - Log().develop (setter) + + ### Test + - ``TypeError`` is raised if develop is set to a non-bool. + """ + with does_not_raise(): + instance = Log() + + match = r"Log\.develop:\s+" + match += r"Expected boolean for develop\.\s+" + match += r"Got: type str for value FOO\." + with pytest.raises(TypeError, match=match): + instance.develop = "FOO" + + +@pytest.mark.parametrize("develop", [(True), (False)]) +def test_log_v2_00310(develop) -> None: + """ + ### Methods + - Log().develop (setter) + + ### Test + - develop is set correctly if passed a bool. + - No exceptions are raised. + """ + with does_not_raise(): + instance = Log() + instance.develop = develop + assert instance.develop == develop From 00f776139f628bfaea8a71c53ebc22b9042c6fed Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 2 Jun 2024 10:01:56 -1000 Subject: [PATCH 105/230] Log() v2: Error handling improvements. 1. Log(): Update error messages for consistency. 2. Log().validate_logging_config(): Simply logic to raise exception if the logging config contains any handlers not in self.valid_handlers. 3. Log().validate_logging_config(): Raise ValueError if no handlers are found in the logging config file. --- plugins/module_utils/common/log_v2.py | 43 ++++++--- tests/unit/module_utils/common/test_log_v2.py | 90 ++++++++++++++++--- 2 files changed, 112 insertions(+), 21 deletions(-) diff --git a/plugins/module_utils/common/log_v2.py b/plugins/module_utils/common/log_v2.py index 7abd0a70a..ec5adfacc 100644 --- a/plugins/module_utils/common/log_v2.py +++ b/plugins/module_utils/common/log_v2.py @@ -32,8 +32,13 @@ class Log: ### Raises - ``ValueError`` if: - - An error is encountered reading the logging config file. - - An error is encountered parsing the logging config file. + - An error is encountered reading the logging config file. + - An error is encountered parsing the logging config file. + - An invalid handler is found in the logging config file. + - Valid handlers are listed in self.valid_handlers, + which currently contains: "file". + - No formatters are found in the logging config file that + are associated with the configured handlers. ### Usage @@ -189,6 +194,9 @@ def __init__(self): # Set this to True during development to catch logging errors. logging.raiseExceptions = False + self.valid_handlers = set() + self.valid_handlers.add("file") + self._build_properties() def _build_properties(self) -> None: @@ -260,8 +268,10 @@ def validate_logging_config(self, logging_config: dict) -> None: ### Raises - ``ValueError`` if: - - The logging config file contains a handler that logs to - console, stdout, or stderr. + - The logging config file contains no handlers. + - The logging config file contains a handler other than + the handlers listed in self.valid_handlers (see class + docstring). ### Usage ```python @@ -270,14 +280,25 @@ def validate_logging_config(self, logging_config: dict) -> None: log.commit() ``` """ + if len(logging_config.get("handlers", {})) == 0: + msg = "logging.config.dictConfig: " + msg += "No file handlers found. " + msg += "Add a file handler to the logging config file " + msg += f"and try again: {self.config}" + raise ValueError(msg) + bad_handlers = [] for handler in logging_config.get("handlers", {}): - if handler in ["console", "stderr", "stdout"]: - msg = f"logging config file {self.config} contains a handler " - msg += "that logs to console, stdout, or stderr. This will " - msg += "break Ansible module execution. Remove these handlers " - msg += "from the logging config file and try again. " - msg += f"Handler: {handler}" - raise ValueError(msg) + if handler not in self.valid_handlers: + msg = "logging.config.dictConfig: " + msg += "handlers found that may interrupt Ansible module " + msg += "execution. " + msg += "Remove these handlers from the logging config file " + msg += "and try again. " + bad_handlers.append(handler) + if len(bad_handlers) > 0: + msg += f"Handlers: {','.join(bad_handlers)}. " + msg += f"Logging config file: {self.config}." + raise ValueError(msg) def commit(self): """ diff --git a/tests/unit/module_utils/common/test_log_v2.py b/tests/unit/module_utils/common/test_log_v2.py index 925d80d51..120203855 100644 --- a/tests/unit/module_utils/common/test_log_v2.py +++ b/tests/unit/module_utils/common/test_log_v2.py @@ -43,7 +43,7 @@ def logging_config(logging_config_file) -> dict: """ ### Summary - Return a logging configuration conformant with dictConfig. + Return a logging configuration conformant with logging.config.dictConfig. """ return { "version": 1, @@ -269,9 +269,10 @@ def test_log_v2_00210(tmp_path) -> None: with does_not_raise(): instance = Log() - match = r"logging\.config\.dictConfig:\s+" - match += rf"Unable to configure logging from {config_file}\.\s+" - match += "Error detail: dictionary doesn't specify a version" + match = r"logging.config.dictConfig:\s+" + match += r"No file handlers found\.\s+" + match += r"Add a file handler to the logging config file\s+" + match += rf"and try again: {config_file}" with pytest.raises(ValueError, match=match): instance.commit() @@ -307,8 +308,8 @@ def test_log_v2_00230(tmp_path) -> None: - Log().commit() ### Test - - ``ValueError`` is raised if logging config file contains a - handler that emits to console. + - ``ValueError`` is raised if logging config file contains + handler(s) that emit to non-file destinations. """ log_dir = tmp_path / "log_dir" log_dir.mkdir() @@ -329,11 +330,80 @@ def test_log_v2_00230(tmp_path) -> None: with does_not_raise(): instance = Log() - match = rf"logging config file .*{config_file}\s+" - match += r"contains a handler that logs to console, stdout, or stderr\.\s+" - match += r"This will break Ansible module execution\.\s+" + match = r"logging.config.dictConfig:\s+" + match += r"handlers found that may interrupt Ansible module\s+" + match += r"execution\.\s+" match += r"Remove these handlers from the logging config file and\s+" - match += r"try again. Handler: console" + match += r"try again\.\s+" + match += r"Handlers:\s+.*\.\s+" + match += r"Logging config file:\s+.*\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_log_v2_00240(tmp_path) -> None: + """ + ### Methods + - Log().commit() + + ### Test + - ``ValueError`` is raised if logging config file does not + contain any handlers. + + ### NOTES: + - test_log_v2_00210, raises the same error message in the case where + the logging config file contains JSON that is not conformant with + dictConfig. + """ + log_dir = tmp_path / "log_dir" + log_dir.mkdir() + config_file = log_dir / "logging_config.json" + log_file = log_dir / "dcnm.log" + config = logging_config(str(log_file)) + del config["handlers"] + with open(config_file, "w", encoding="UTF-8") as fp: + json.dump(config, fp) + + environ["NDFC_LOGGING_CONFIG"] = str(config_file) + + with does_not_raise(): + instance = Log() + + match = r"logging.config.dictConfig:\s+" + match += r"No file handlers found\.\s+" + match += r"Add a file handler to the logging config file\s+" + match += rf"and try again: {config_file}" + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_log_v2_00250(tmp_path) -> None: + """ + ### Methods + - Log().commit() + + ### Test + - ``ValueError`` is raised if logging config file does not + contain any formatters or contains formatters that are not + associated with handlers. + """ + log_dir = tmp_path / "log_dir" + log_dir.mkdir() + config_file = log_dir / "logging_config.json" + log_file = log_dir / "dcnm.log" + config = logging_config(str(log_file)) + del config["formatters"] + with open(config_file, "w", encoding="UTF-8") as fp: + json.dump(config, fp) + + environ["NDFC_LOGGING_CONFIG"] = str(config_file) + + with does_not_raise(): + instance = Log() + + match = r"logging.config.dictConfig:\s+" + match += r"Unable to configure logging from\s+.*\.\s+" + match += r"Error detail: Unable to configure handler.*" with pytest.raises(ValueError, match=match): instance.commit() From ce0ad9189a6e9ca81c42fba56dd4fdc5253b7540 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 2 Jun 2024 11:09:52 -1000 Subject: [PATCH 106/230] MaintenanceMode(): Add unit test test_maintenance_mode_00700: - Verify MaintenanceMode().change_system_mode() raises ``ValueError`` when ``EpMaintenanceModeEnable`` or ``EpMaintenanceModeDisable`` raise any of: - ``TypeError`` - ``ValueError`` --- .../common/test_maintenance_mode.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 00e593157..283114e28 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -685,3 +685,92 @@ def test_maintenance_mode_00600(maintenance_mode, param, raises) -> None: else: instance.config = [config] assert instance.config[0]["mode"] == param + + +@pytest.mark.parametrize( + "endpoint_instance, mock_exception, expected_exception, mock_message", + [ + ("ep_maintenance_mode_disable", TypeError, ValueError, "Bad type"), + ("ep_maintenance_mode_disable", ValueError, ValueError, "Bad value"), + ("ep_maintenance_mode_enable", TypeError, ValueError, "Bad type"), + ("ep_maintenance_mode_enable", ValueError, ValueError, "Bad value"), + ], +) +def test_maintenance_mode_00700( + monkeypatch, + maintenance_mode, + endpoint_instance, + mock_exception, + expected_exception, + mock_message, +) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - commit() + + Summary + - Verify MaintenanceMode().change_system_mode() raises ``ValueError`` + when ``EpMaintenanceModeEnable`` or ``EpMaintenanceModeDisable`` raise + any of: + - ``TypeError`` + - ``ValueError`` + + + Code Flow - Setup + - MaintenanceMode() is instantiated + - Required attributes are set + - EpMaintenanceModeEnable() is mocked to raise each of the above exceptions + + Code Flow - Test + - MaintenanceMode().commit() is called for each exception + + Expected Result + - ``ValueError`` is raised + - Exception message matches expected + """ + + class MockEndpoint: + """ + Mock Ep*() class + """ + + def __init__(self): + self._fabric_name = None + self._serial_number = None + + @property + def fabric_name(self): + """ + Mock fabric_name getter/setter + """ + return self._fabric_name + + @fabric_name.setter + def fabric_name(self, value): + raise mock_exception(mock_message) + + @property + def serial_number(self): + """ + Mock serial_number getter/setter + """ + return self._serial_number + + @serial_number.setter + def serial_number(self, value): + self._serial_number = value + + with does_not_raise(): + instance = maintenance_mode + config = copy.deepcopy(CONFIG[0]) + if endpoint_instance == "ep_maintenance_mode_disable": + config["mode"] = "normal" + instance.config = [config] + instance.rest_send = RestSend({}) + instance.results = Results() + + monkeypatch.setattr(instance, endpoint_instance, MockEndpoint()) + with pytest.raises(expected_exception, match=mock_message): + instance.commit() From 36a4bae85ee34b3f09c30c4b34277e301158e036 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 2 Jun 2024 12:50:17 -1000 Subject: [PATCH 107/230] MaintenanceMode: Add unit test test_maintenance_mode_00230: - Verify commit() unsuccessful case: - RETURN_CODE == 500. - commit raises ``ValueError`` when change_system_mode() raises ``ControllerResponseError``. - Controller response contains expected structure and values. --- .../fixtures/responses_MaintenanceMode.json | 10 ++ .../common/test_maintenance_mode.py | 94 +++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json b/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json index 116ddd228..20da8f3d6 100644 --- a/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json +++ b/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json @@ -8,5 +8,15 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_Fabric/switches/FDO22180ASJ/maintenance-mode", "RETURN_CODE": 200, "sequence_number": 1 + }, + "test_maintenance_mode_00230a": { + "DATA": { + "status": "Failure" + }, + "MESSAGE": "Internal Server Error", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_Fabric/switches/FDO22180ASJ/maintenance-mode", + "RETURN_CODE": 500, + "sequence_number": 1 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 283114e28..3d31e148f 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -444,6 +444,100 @@ def responses(): assert instance.results.result[1].get("success", None) is True +@pytest.mark.parametrize( + "mode", + [ + ("maintenance"), + ("normal"), + ], +) +def test_maintenance_mode_00230(maintenance_mode, mode) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - commit() + - change_system_mode() + - deploy_switches() + + Summary + - Verify commit() unsuccessful case: + - RETURN_CODE == 500. + - commit raises ``ValueError`` when change_system_mode() raises + ``ControllerResponseError``. + - Controller response contains expected structure and values. + + Code Flow - Setup + - MaintenanceMode() is instantiated + - Sender() is mocked to return expected responses + - Required attributes are set + - MaintenanceMode().commit() is called + - responses_MaintenanceMode contains a dict with: + - RETURN_CODE == 500 + - DATA == {"status": "Failure"} + + Code Flow - Test + - ``MaintenanceMode().commit()`` is called + - ``change_system_mode()`` raises ``ControllerResponseError`` + - ``commit()`` raises ``ValueError`` + + Expected Result + - ``commit()`` raises ``ValueError`` + - instance.response_data returns expected data + - MaintenanceMode()._properties are updated + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_maintenance_mode(key) + # yield responses_config_deploy(key) + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + + config = copy.deepcopy(CONFIG[0]) + config["mode"] = mode + + with does_not_raise(): + rest_send = RestSend({"state": "merged", "check_mode": False}) + rest_send.sender = mock_sender + rest_send.response_handler = ResponseHandler() + instance = maintenance_mode + instance.rest_send = rest_send + instance.rest_send.unit_test = True + instance.rest_send.timeout = 1 + instance.results = Results() + instance.config = [config] + + match = r"MaintenanceMode\.change_system_mode:\s+" + match += r"Unable to change system mode on switch:\s+" + match += rf"fabric_name {config['fabric_name']},\s+" + match += rf"ip_address {config['ip_address']},\s+" + match += rf"serial_number {config['serial_number']}\.\s+" + match += r"Got response\s+.*" + with pytest.raises(ValueError, match=match): + instance.commit() + + assert isinstance(instance.results.diff, list) + assert isinstance(instance.results.metadata, list) + assert isinstance(instance.results.response, list) + assert isinstance(instance.results.result, list) + assert len(instance.results.diff[0]) == 1 + + assert instance.results.metadata[0].get("action", None) == "maintenance_mode" + assert instance.results.metadata[0].get("sequence_number", None) == 1 + assert instance.results.metadata[0].get("state", None) == "merged" + + assert instance.results.response[0].get("DATA", {}).get("status") == "Failure" + assert instance.results.response[0].get("MESSAGE", None) == "Internal Server Error" + assert instance.results.response[0].get("RETURN_CODE", None) == 500 + assert instance.results.response[0].get("METHOD", None) == "POST" + + assert instance.results.result[0].get("changed", None) is False + assert instance.results.result[0].get("success", None) is False + + def test_maintenance_mode_00300(maintenance_mode) -> None: """ Classes and Methods From 8f2492fc4bb0320637f0b31db038cb5259e07d7f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 2 Jun 2024 13:31:57 -1000 Subject: [PATCH 108/230] dcnm_maintenance_mode: Hardening 1. Merged().send_need(): Wrap MaintenanceMode() in try-except block. 2. Merged().get_have(): Wrap MaintenanceModeInfo() in try-except block. 3. Query().get_have(): Wrap MaintenanceModeInfo() in try-except block. 4. MaintenanceMode(): Update class docstring. --- .../module_utils/common/maintenance_mode.py | 5 +- plugins/modules/dcnm_maintenance_mode.py | 54 +++++++++++-------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index e1cec9c4f..51392c229 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -51,9 +51,8 @@ class MaintenanceMode: - ``commit`` if config, rest_send, or results are not set. - ``commit`` if ``EpMaintenanceModeEnable`` or ``EpMaintenanceModeDisable`` raise ``ValueError``. - - - ``ControllerResponseError`` in the following methods: - - ``commit`` if controller response != 200. + - ``commit`` if either ``chance_system_mode()`` or + ``deploy_switches()`` raise ``ControllerResponseError``. - ``TypeError`` in the following properties: - ``rest_send`` if value is not an instance of RestSend. diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index a5b01f1b6..183702631 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -873,13 +873,19 @@ def get_have(self): """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - instance = MaintenanceModeInfo(self.params) - instance.rest_send = self.rest_send - instance.results = self.results - instance.config = [ - item["ip_address"] for item in self.config.get("switches", {}) - ] - instance.refresh() + try: + instance = MaintenanceModeInfo(self.params) + instance.rest_send = self.rest_send + instance.results = self.results + instance.config = [ + item["ip_address"] for item in self.config.get("switches", {}) + ] + instance.refresh() + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error while retrieving switch info. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error self.have = instance.info def fabric_deployment_disabled(self) -> None: @@ -1055,7 +1061,8 @@ def send_need(self) -> None: Build and send the payload to modify maintenance mode. ### Raises - - ``ValueError`` if MaintenanceMode() raises ``ValueError`` + - ``ValueError`` if MaintenanceMode() raises either + ``TypeError`` or ``ValueError`` """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable @@ -1066,16 +1073,13 @@ def send_need(self) -> None: self.log.debug(msg) return - instance = MaintenanceMode(self.params) - instance.rest_send = self.rest_send - instance.results = self.results try: + instance = MaintenanceMode(self.params) + instance.rest_send = self.rest_send + instance.results = self.results instance.config = self.need - except ValueError as error: - raise ValueError(error) from error - try: instance.commit() - except ValueError as error: + except (TypeError, ValueError) as error: raise ValueError(error) from error @@ -1160,13 +1164,19 @@ def get_have(self): """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - instance = MaintenanceModeInfo(self.params) - instance.rest_send = self.rest_send - instance.results = self.results - instance.config = [ - item["ip_address"] for item in self.config.get("switches", {}) - ] - instance.refresh() + try: + instance = MaintenanceModeInfo(self.params) + instance.rest_send = self.rest_send + instance.results = self.results + instance.config = [ + item["ip_address"] for item in self.config.get("switches", {}) + ] + instance.refresh() + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error while retrieving switch info. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error self.have = instance.info def commit(self) -> None: From f98b0994aa8abe83bfd0126b675a9bbf4806e81f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 2 Jun 2024 13:39:12 -1000 Subject: [PATCH 109/230] Log() v2: Fix class docstring issues. 1. Log() v2: Add TypeError to class docstring Raises section. 2. Log() v2: Fix indentation in class docstring. Most lines were indented one extra space. --- plugins/module_utils/common/log_v2.py | 206 +++++++++++++------------- 1 file changed, 104 insertions(+), 102 deletions(-) diff --git a/plugins/module_utils/common/log_v2.py b/plugins/module_utils/common/log_v2.py index ec5adfacc..5fd8212db 100644 --- a/plugins/module_utils/common/log_v2.py +++ b/plugins/module_utils/common/log_v2.py @@ -27,11 +27,11 @@ class Log: """ - ### Summary - Create the base dcnm logging object. + ### Summary + Create the base dcnm logging object. - ### Raises - - ``ValueError`` if: + ### Raises + - ``ValueError`` if: - An error is encountered reading the logging config file. - An error is encountered parsing the logging config file. - An invalid handler is found in the logging config file. @@ -39,105 +39,107 @@ class Log: which currently contains: "file". - No formatters are found in the logging config file that are associated with the configured handlers. + - ``TypeError`` if: + - ``develop`` is not a boolean. - ### Usage - - By default, Log() does the following: - - 1. Reads the environment variable ``NDFC_LOGGING_CONFIG`` to determine - the path to the logging config file. If the environment variable is - not set, then logging is disabled. - 2. Sets ``develop`` to False. This disables exceptions raised by the - logging module itself. - - Hence, the simplest usage for Log() is: - - - Set the environment variable ``NDFC_LOGGING_CONFIG`` to the - path of the logging config file. ``bash`` shell is used in the - example below. - - ```bash - export NDFC_LOGGING_CONFIG="/path/to/logging_config.json" - ``` - - - Instantiate a Log() object instance and call ``commit()`` on the instance: - - ```python - from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log - try: - log = Log() - log.commit() - except ValueError as error: - # handle error - ``` - - To later disable logging, unset the environment variable. - ``bash`` shell is used in the example below. - - ```bash - unset NDFC_LOGGING_CONFIG - ``` - - To enable exceptions from the logging module (not recommended, unless needed for - development), set ``develop`` to True: - - ```python - from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log - try: - log = Log() - log.develop = True - log.commit() - except ValueError as error: - # handle error - ``` - - To directly set the path to the logging config file, overriding the - ``NDFC_LOGGING_CONFIG`` environment variable, set the ``config`` - property prior to calling ``commit()``: - - ```python - from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log - try: - log = Log() - log.config = "/path/to/logging_config.json" - log.commit() - except ValueError as error: - # handle error - ``` - - At this point, a base/parent logger is created for which all other - loggers throughout the dcnm collection will be children. - This allows for a single logging config to be used for all modules in the - collection, and allows for the logging config to be specified in a - single place external to the code. - - ### Example module code using the Log() object - - In the main() function of a module. - ```python - from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log - - def main(): - try: - log = Log() - log.commit() - except ValueError as error: - ansible_module.fail_json(msg=str(error)) - - task = AnsibleTask() - ``` - - In the AnsibleTask() class (or any other classes running in the - main() function's call stack i.e. classes instantiated in either - main() or in AnsibleTask()). - - ```python - class AnsibleTask: - def __init__(self): - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - def some_method(self): - self.log.debug("This is a debug message.") + ### Usage + + By default, Log() does the following: + + 1. Reads the environment variable ``NDFC_LOGGING_CONFIG`` to determine + the path to the logging config file. If the environment variable is + not set, then logging is disabled. + 2. Sets ``develop`` to False. This disables exceptions raised by the + logging module itself. + + Hence, the simplest usage for Log() is: + + - Set the environment variable ``NDFC_LOGGING_CONFIG`` to the + path of the logging config file. ``bash`` shell is used in the + example below. + + ```bash + export NDFC_LOGGING_CONFIG="/path/to/logging_config.json" + ``` + + - Instantiate a Log() object instance and call ``commit()`` on the instance: + + ```python + from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log + try: + log = Log() + log.commit() + except ValueError as error: + # handle error + ``` + + To later disable logging, unset the environment variable. + ``bash`` shell is used in the example below. + + ```bash + unset NDFC_LOGGING_CONFIG + ``` + + To enable exceptions from the logging module (not recommended, unless needed for + development), set ``develop`` to True: + + ```python + from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log + try: + log = Log() + log.develop = True + log.commit() + except ValueError as error: + # handle error + ``` + + To directly set the path to the logging config file, overriding the + ``NDFC_LOGGING_CONFIG`` environment variable, set the ``config`` + property prior to calling ``commit()``: + + ```python + from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log + try: + log = Log() + log.config = "/path/to/logging_config.json" + log.commit() + except ValueError as error: + # handle error + ``` + + At this point, a base/parent logger is created for which all other + loggers throughout the dcnm collection will be children. + This allows for a single logging config to be used for all modules in the + collection, and allows for the logging config to be specified in a + single place external to the code. + + ### Example module code using the Log() object + + In the main() function of a module. + ```python + from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log + + def main(): + try: + log = Log() + log.commit() + except ValueError as error: + ansible_module.fail_json(msg=str(error)) + + task = AnsibleTask() + ``` + + In the AnsibleTask() class (or any other classes running in the + main() function's call stack i.e. classes instantiated in either + main() or in AnsibleTask()). + + ```python + class AnsibleTask: + def __init__(self): + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + def some_method(self): + self.log.debug("This is a debug message.") ``` ### Logging Config File From b879c756cb55c6a4a4dd58e7f21e833958d41600 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 3 Jun 2024 09:28:01 -1000 Subject: [PATCH 110/230] Fix Results() update to remove duplicate result and response The following classes were incorrectly updating Results() with duplicated entries for result and response keys. FabricDetails() SwitchDetails() --- plugins/module_utils/common/switch_details.py | 2 -- plugins/module_utils/fabric/fabric_details.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 8b00ec0fb..8b06e6f73 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -121,9 +121,7 @@ def update_results(self) -> None: # Update and register results self.results.action = self.action self.results.response_current = self.rest_send.response_current - self.results.response = self.rest_send.response_current self.results.result_current = self.rest_send.result_current - self.results.result = self.rest_send.result_current # SwitchDetails never changes the controller state self.results.changed = False diff --git a/plugins/module_utils/fabric/fabric_details.py b/plugins/module_utils/fabric/fabric_details.py index f7cfc6007..aa2d7fa2b 100644 --- a/plugins/module_utils/fabric/fabric_details.py +++ b/plugins/module_utils/fabric/fabric_details.py @@ -62,9 +62,7 @@ def _update_results(self): details. """ self.results.response_current = self.rest_send.response_current - self.results.response = self.rest_send.response_current self.results.result_current = self.rest_send.result_current - self.results.result = self.rest_send.result_current if self.results.response_current.get("RETURN_CODE") == 200: self.results.failed = False else: From 83101d886b17ab84f7e00ac84a45b0faa20204a2 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 3 Jun 2024 09:59:05 -1000 Subject: [PATCH 111/230] FabricDetails(): Backout last commit (only for FabricDetails) FabricDetails() has dependent code that does not like the change made in the last commit. I'll copy FabricDetails() to module_utils/common and make the changes in the copied version. The existing code within dcnm_fabric can remain as-is and we can modify it to use the new FabricDetails() from module_utils/common later. --- plugins/module_utils/fabric/fabric_details.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/module_utils/fabric/fabric_details.py b/plugins/module_utils/fabric/fabric_details.py index aa2d7fa2b..f7cfc6007 100644 --- a/plugins/module_utils/fabric/fabric_details.py +++ b/plugins/module_utils/fabric/fabric_details.py @@ -62,7 +62,9 @@ def _update_results(self): details. """ self.results.response_current = self.rest_send.response_current + self.results.response = self.rest_send.response_current self.results.result_current = self.rest_send.result_current + self.results.result = self.rest_send.result_current if self.results.response_current.get("RETURN_CODE") == 200: self.results.failed = False else: From 8bfacca877ed92c88e37ae73406d7c1627a03ca9 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 3 Jun 2024 11:15:06 -1000 Subject: [PATCH 112/230] Results().did_anything_change(): return False if state == query Results().did_anything_change(): Update conditional. BACKGROUND: Previously, Results().did_anything_change() returned False only when self.action == "query". This is wrong. It's worked up until now because existing modules set Results().action to "query". However, action is intended to be a freeform string that describes what action was taken, so could be any string depending on what future modules set it to. The conditional should have been: if self.state == "query" CHANGES: Modified the conditional to be: if self.action == "query" or self.state == "query" TODO: We should remove self.action from the conditional after testing that existing modules still work correctly after it's been removed. For now, we can leave self.action in the conditional, since it's unlikely that self.action will be set specifically to "query" for future modules that make changes to the controller. --- plugins/module_utils/common/results.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/common/results.py b/plugins/module_utils/common/results.py index ed4c23e1b..8b5594fe2 100644 --- a/plugins/module_utils/common/results.py +++ b/plugins/module_utils/common/results.py @@ -218,16 +218,14 @@ def did_anything_change(self) -> bool: """ msg = f"{self.class_name}.did_anything_change(): ENTERED: " msg += f"self.action: {self.action}, " + msg += f"self.state: {self.state}, " msg += f"self.result_current: {self.result_current}, " msg += f"self.diff: {self.diff}" self.log.debug(msg) if self.check_mode is True: return False - if self.action == "query": - msg = f"{self.class_name}.did_anything_change(): " - msg += f"self.action: {self.action}" - self.log.debug(msg) + if self.action == "query" or self.state == "query": return False if self.result_current.get("changed", None) is True: return True From 3777b7a48921ea37350dcea50eeb0815a4670bf0 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 3 Jun 2024 14:26:14 -1000 Subject: [PATCH 113/230] Results(): raise TypeError instead of ValueError 1. Results(): modify all properties to raise TypeError instead of ValueError when they are passed unexpected types. 2. tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_common.py: Modify test cases to assert for TypeError instead of ValueError --- plugins/module_utils/common/results.py | 247 +++++++++++------- .../test_image_policy_common.py | 50 ++-- 2 files changed, 177 insertions(+), 120 deletions(-) diff --git a/plugins/module_utils/common/results.py b/plugins/module_utils/common/results.py index 8b5594fe2..79505e26d 100644 --- a/plugins/module_utils/common/results.py +++ b/plugins/module_utils/common/results.py @@ -27,42 +27,50 @@ class Results: """ + ### Summary Collect results across tasks. + ### Raises + - ``TypeError``: if properties are not of the correct type. + + ### Description Provides a mechanism to collect results across tasks. The task classes must support this Results class. Specifically, they must implement the following: - 1. Accept an instantiation of Results + 1. Accept an instantiation of`` Results()`` - Typically a class property is used for this - 2. Populate the Results instance with the results of the task - - Typically done by transferring RestSend's responses to the - Results instance - 3. Register the results of the task with Results, using: - - Results.register_task_result() + 2. Populate the ``Results`` instance with the results of the task + - Typically done by transferring ``RestSend()``'s responses to the + ``Results`` instance + 3. Register the results of the task with ``Results``, using: + - ``Results.register_task_result()`` - Typically done after the task is complete - Results should be instantiated in the main Ansible Task class and passed - to all other task classes. The task classes should populate the Results - instance with the results of the task and then register the results with - Results.register_task_result(). This may be done within a separate class - (as in the example below, where FabricDelete() class is called from the - TaskDelete() class. The Results instance can then be used to build the - final result, by calling Results.build_final_result(). + ``Results`` should be instantiated in the main Ansible Task class and + passed to all other task classes. The task classes should populate the + ``Results`` instance with the results of the task and then register the + results with ``Results.register_task_result()``. - Example Usage: + This may be done within a separate class (as in the example below, where + the ``FabricDelete()`` class is called from the ``TaskDelete()`` class. + The ``Results`` instance can then be used to build the final result, by + calling ``Results.build_final_result()``. + ### Example Usage We assume an Ansible module structure as follows: - TaskCommon() : Common methods used by the various ansible state classes. - TaskDelete(TaskCommon) : Implements the delete state - TaskMerge(TaskCommon) : Implements the merge state - TaskQuery(TaskCommon) : Implements the query state - etc... + - ``TaskCommon()`` : Common methods used by the various ansible + state classes. + - ``TaskDelete(TaskCommon)`` : Implements the delete state + - ``TaskMerge(TaskCommon)`` : Implements the merge state + - ``TaskQuery(TaskCommon)`` : Implements the query state + - etc... - In TaskCommon, Results is instantiated and, hence, is inherited by all + In TaskCommon, ``Results`` is instantiated and, hence, is inherited by all state classes.: + ```python class TaskCommon: def __init__(self): self.results = Results() @@ -77,12 +85,13 @@ def results(self): @results.setter def results(self, value): self.properties["results"] = value - + ``` In each of the state classes (TaskDelete, TaskMerge, TaskQuery, etc...) a class is instantiated (in the example below, FabricDelete) that supports collecting results for the Results instance: + ```python class TaskDelete(TaskCommon): def __init__(self, ansible_module): super().__init__(ansible_module) @@ -98,18 +107,19 @@ def commit(self): # results.register_task_result() is called within the # commit() method of the FabricDelete class. self.fabric_delete.commit() - + ``` Finally, within the main() method of the Ansible module, the final result is built by calling Results.build_final_result(): + ```python if ansible_module.params["state"] == "deleted": task = TaskDelete(ansible_module) task.commit() elif ansible_module.params["state"] == "merged": task = TaskDelete(ansible_module) task.commit() - etc... + # etc, for other states... # Build the final result task.results.build_final_result() @@ -118,49 +128,56 @@ def commit(self): if True in task.results.failed: ansible_module.fail_json(**task.results.final_result) ansible_module.exit_json(**task.results.final_result) + ``` + results.final_result will be a dict with the following structure - # results.final_result will be a dict with the following structure - + ```json { "changed": True, # or False "failed": True, # or False "diff": { - [], + [{"diff1": "diff"}, {"diff2": "diff"}, {"etc...": "diff"}], } "response": { - [], + [{"response1": "response"}, {"response2": "response"}, {"etc...": "response"}], } "result": { - [], + [{"result1": "result"}, {"result2": "result"}, {"etc...": "result"}], } "metadata": { - [], + [{"metadata1": "metadata"}, {"metadata2": "metadata"}, {"etc...": "metadata"}], } } + ``` diff, response, and result dicts are per the Ansible DCNM Collection standard output. An example of a result dict would be (sequence_number is added by Results): + ```json { "found": true, - "sequence_number": 0, + "sequence_number": 1, "success": true } + ``` An example of a metadata dict would be (sequence_number is added by Results): + + ```json { "action": "merge", "check_mode": false, "state": "merged", - "sequence_number": 0 + "sequence_number": 1 } + ``` - sequence_number indicates the order in which the task was registered with Results. - It provides a way to correlate the diff, response, result, and metadata across all - tasks. + ``sequence_number`` indicates the order in which the task was registered + with ``Results``. It provides a way to correlate the diff, response, + result, and metadata across all tasks. """ def __init__(self): @@ -241,8 +258,10 @@ def did_anything_change(self) -> bool: def register_task_result(self): """ + ### Summary Register a task's result. + ### Description 1. Append result_current, response_current, diff_current and metadata_current their respective lists (result, response, diff, and metadata) @@ -299,7 +318,11 @@ def register_task_result(self): def build_final_result(self): """ - Build the final result. This consists of the following: + ### Summary + Build the final result. + + ### Description + The final result consists of the following: ```json { "changed": True, # or False @@ -365,7 +388,11 @@ def ok_result(self) -> Dict[str, Any]: @property def action(self): """ + ### Summary Added to results to indicate the action that was taken + + ### Raises + - ``TypeError``: if value is not a string """ return self.properties["action"] @@ -376,7 +403,7 @@ def action(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"instance.{method_name} must be a string. " msg += f"Got {value}." - raise ValueError(msg) + raise TypeError(msg) msg = f"{self.class_name}.{method_name}: " msg += f"value: {value}" self.log.debug(msg) @@ -385,9 +412,17 @@ def action(self, value): @property def changed(self) -> set: """ - bool = whether we changed anything + ### Summary + - A ``set()`` containing boolean values indicating whether + anything changed. + - The setter adds a boolean value to the set. + - The getter returns the set. - raise ValueError if value is not a bool + ### Raises + - setter: ``TypeError``: if value is not a bool + + ### Returns + - A set() of Boolean values indicating whether any tasks changed """ return self.properties["changed"] @@ -397,13 +432,18 @@ def changed(self, value): if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " msg += f"instance.changed must be a bool. Got {value}" - raise ValueError(msg) + raise TypeError(msg) self.properties["changed"].add(value) @property def check_mode(self): """ - check_mode + ### Summary + - A boolean indicating whether Ansible check_mode is enabled. + - ``True`` if check_mode is enabled, ``False`` otherwise. + + ### Raises + - ``TypeError``: if value is not a bool """ return self.properties["check_mode"] @@ -414,15 +454,19 @@ def check_mode(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"instance.{method_name} must be a bool. " msg += f"Got {value}." - raise ValueError(msg) + raise TypeError(msg) self.properties["check_mode"] = value @property def diff(self): """ - List of dicts representing the changes made + ### Summary + - A list of dicts representing the changes made. + - The setter appends a dict to the list. + - The getter returns the list. - raise ValueError if value is not a dict + ### Raises + - setter: ``TypeError``: if value is not a dict """ return self.properties["diff"] @@ -432,16 +476,19 @@ def diff(self, value): if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " msg += f"instance.diff must be a dict. Got {value}" - raise ValueError(msg) + raise TypeError(msg) value["sequence_number"] = self.task_sequence_number self.properties["diff"].append(copy.deepcopy(value)) @property def diff_current(self): """ + ### Summary - getter: Return the current diff - setter: Set the current diff - - setter: raise ``ValueError`` if value is not a dict + + ### Raises + - setter: ``TypeError`` if value is not a dict. """ value = self.properties.get("diff_current") value["sequence_number"] = self.task_sequence_number @@ -454,18 +501,19 @@ def diff_current(self, value): msg = f"{self.class_name}.{method_name}: " msg += "instance.diff_current must be a dict. " msg += f"Got {value}." - raise ValueError(msg) + raise TypeError(msg) self.properties["diff_current"] = value @property def failed(self) -> set: """ - A set() of Boolean values indicating whether any tasks failed - - If the set contains True, at least one task failed - If the set contains only False all tasks succeeded + ### Summary + - A set() of Boolean values indicating whether any tasks failed + - If the set contains True, at least one task failed. + - If the set contains only False all tasks succeeded. - raise ValueError if value is not a bool + ### Raises + - ``TypeError`` if value is not a bool. """ return self.properties["failed"] @@ -478,18 +526,19 @@ def failed(self, value): self.properties["failed"].add(True) msg = f"{self.class_name}.{method_name}: " msg += f"instance.failed must be a bool. Got {value}" - raise ValueError(msg) + raise TypeError(msg) self.properties["failed"].add(value) @property def metadata(self): """ - List of dicts representing the metadata (if any) - for each diff. + ### Summary + - List of dicts representing the metadata (if any) for each diff. + - getter: Return the metadata. + - setter: Append value to the metadata list. - - getter: Return the metadata - - setter: Append value to the metadata list - - setter: raise ``ValueError`` if value is not a dict + ### Raises + - setter: ``TypeError`` if value is not a dict. """ return self.properties["metadata"] @@ -499,15 +548,19 @@ def metadata(self, value): if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " msg += f"instance.metadata must be a dict. Got {value}" - raise ValueError(msg) + raise TypeError(msg) value["sequence_number"] = self.task_sequence_number self.properties["metadata"].append(copy.deepcopy(value)) @property def metadata_current(self): """ + ### Summary - getter: Return the current metadata which is comprised of the properties action, check_mode, and state. + + ### Raises + None """ value = {} value["action"] = self.action @@ -519,14 +572,14 @@ def metadata_current(self): @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. + ### Summary + - Return a ``dict`` containing the current response from the controller. + ``instance.commit()`` must be called first. + - getter: Return the current response. + - setter: Set the current response. - - getter: Return the current response - - setter: Set the current response - - setter: raise ``ValueError`` if value is not a dict + ### Raises + - setter: ``TypeError`` if value is not a dict. """ value = self.properties.get("response_current") value["sequence_number"] = self.task_sequence_number @@ -539,20 +592,20 @@ def response_current(self, value): msg = f"{self.class_name}.{method_name}: " msg += "instance.response_current must be a dict. " msg += f"Got {value}." - raise ValueError(msg) + raise TypeError(msg) self.properties["response_current"] = value @property def response(self): """ - Return the aggregated POST response from the controller - instance.commit() must be called first. + ### Summary + - A ``list`` of ``dict``, where each ``dict`` contains a response + from the controller. + - getter: Return the response list. + - setter: Append ``dict`` to the response list. - This is a list of responses from the controller. - - - getter: Return the response list - - setter: Append value to the response list - - setter: raise ``ValueError`` if value is not a dict + ### Raises + - setter: ``TypeError``: if value is not a dict. """ return self.properties.get("response") @@ -563,18 +616,22 @@ def response(self, value): msg = f"{self.class_name}.{method_name}: " msg += "instance.response must be a dict. " msg += f"Got {value}." - raise ValueError(msg) + raise TypeError(msg) value["sequence_number"] = self.task_sequence_number self.properties["response"].append(copy.deepcopy(value)) @property def response_data(self): """ + ### Summary - getter: Return the contents of the DATA key within ``current_response``. - setter: set ``response_data`` to the value passed in which should be the contents of the DATA key within ``current_response``. + + ### Raises + None """ return self.properties.get("response_data") @@ -585,14 +642,13 @@ def response_data(self, value): @property def result(self): """ - Return the aggregated result from the controller - instance.commit() must be called first. + ### Summary + - A ``list`` of ``dict``, where each ``dict`` contains a result. + - getter: Return the result list. + - setter: Append ``dict`` to the result list. - This is a list of results from the controller. - - - getter: Return the result list - - setter: Append value to the result list - - setter: raise ``ValueError`` if value is not a dict + ### Raises + - setter: ``TypeError`` if value is not a dict """ return self.properties.get("result") @@ -603,21 +659,20 @@ def result(self, value): msg = f"{self.class_name}.{method_name}: " msg += "instance.result must be a dict. " msg += f"Got {value}." - raise ValueError(msg) + raise TypeError(msg) value["sequence_number"] = self.task_sequence_number self.properties["result"].append(copy.deepcopy(value)) @property def result_current(self): """ - Return the current result from the controller - instance.commit() must be called first. - - This is a dict containing the current result. + ### Summary + - The current result. + - getter: Return the current result. + - setter: Set the current result. - - getter: Return the current result - - setter: Set the current result - - setter: raise ``ValueError`` if value is not a dict + ### Raises + - setter: ``TypeError`` if value is not a dict """ value = self.properties.get("result_current") value["sequence_number"] = self.task_sequence_number @@ -630,17 +685,19 @@ def result_current(self, value): msg = f"{self.class_name}.{method_name}: " msg += "instance.result_current must be a dict. " msg += f"Got {value}." - raise ValueError(msg) + raise TypeError(msg) self.properties["result_current"] = value @property def state(self): """ - The Ansible state + ### Summary + - The Ansible state + - getter: Return the state. + - setter: Set the state. - - getter: Return the state - - setter: Set the state - - setter: raise ``ValueError`` if value is not a string + ### Raises + - setter: ``TypeError`` if value is not a string """ return self.properties["state"] @@ -651,5 +708,5 @@ def state(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"instance.{method_name} must be a string. " msg += f"Got {value}." - raise ValueError(msg) + raise TypeError(msg) self.properties["state"] = value diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_common.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_common.py index d67f81ba3..f517e3533 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_common.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_common.py @@ -378,8 +378,8 @@ def test_image_policy_common_00050(image_policy_common, arg, return_value) -> No [ (True, does_not_raise(), True), (False, does_not_raise(), True), - (None, pytest.raises(ValueError, match=MATCH_00060), False), - ("FOO", pytest.raises(ValueError, match=MATCH_00060), False), + (None, pytest.raises(TypeError, match=MATCH_00060), False), + ("FOO", pytest.raises(TypeError, match=MATCH_00060), False), ], ) def test_image_policy_common_00060(image_policy_common, arg, expected, flag) -> None: @@ -422,8 +422,8 @@ def test_image_policy_common_00060(image_policy_common, arg, expected, flag) -> does_not_raise(), True, ), - (None, None, pytest.raises(ValueError, match=MATCH_00070), False), - ("FOO", None, pytest.raises(ValueError, match=MATCH_00070), False), + (None, None, pytest.raises(TypeError, match=MATCH_00070), False), + ("FOO", None, pytest.raises(TypeError, match=MATCH_00070), False), ], ) def test_image_policy_common_00070( @@ -463,8 +463,8 @@ def test_image_policy_common_00070( [ (True, does_not_raise(), True), (False, does_not_raise(), True), - (None, pytest.raises(ValueError, match=MATCH_00080), False), - ("FOO", pytest.raises(ValueError, match=MATCH_00080), False), + (None, pytest.raises(TypeError, match=MATCH_00080), False), + ("FOO", pytest.raises(TypeError, match=MATCH_00080), False), ], ) def test_image_policy_common_00080(image_policy_common, arg, expected, flag) -> None: @@ -524,8 +524,8 @@ def test_image_policy_common_00090(image_policy_common) -> None: [ ({}, {"sequence_number": 0}, does_not_raise(), True), ({"foo": "bar"}, {"foo": "bar", "sequence_number": 0}, does_not_raise(), True), - (None, None, pytest.raises(ValueError, match=MATCH_00100), False), - ("FOO", None, pytest.raises(ValueError, match=MATCH_00100), False), + (None, None, pytest.raises(TypeError, match=MATCH_00100), False), + ("FOO", None, pytest.raises(TypeError, match=MATCH_00100), False), ], ) def test_image_policy_common_00100( @@ -539,12 +539,12 @@ def test_image_policy_common_00100( Summary Verify that instance.results.response_current returns expected values and - raises ValueError appropriately. + raises TypeError appropriately. Test - instance.results.response_current returns expected values - - ValueError is raised when unexpected values are passed - - ValueError is not raised when expected values are passed + - TypeError is raised when unexpected values are passed + - TypeError is not raised when expected values are passed """ with does_not_raise(): instance = image_policy_common @@ -570,8 +570,8 @@ def test_image_policy_common_00100( does_not_raise(), True, ), - (None, None, pytest.raises(ValueError, match=MATCH_00110), False), - ("FOO", None, pytest.raises(ValueError, match=MATCH_00110), False), + (None, None, pytest.raises(TypeError, match=MATCH_00110), False), + ("FOO", None, pytest.raises(TypeError, match=MATCH_00110), False), ], ) def test_image_policy_common_00110( @@ -585,12 +585,12 @@ def test_image_policy_common_00110( Summary Verify that instance.results.response returns expected values and - raises ValueError appropriately. + raises TypeError appropriately. Test - instance.results.response returns expected value - - ValueError is raised when unexpected values are passed - - ValueError is not raised when expected values are passed + - TypeError is raised when unexpected values are passed + - TypeError is not raised when expected values are passed """ with does_not_raise(): instance = image_policy_common @@ -652,8 +652,8 @@ def test_image_policy_common_00120(image_policy_common, arg, return_value) -> No does_not_raise(), True, ), - (None, None, pytest.raises(ValueError, match=MATCH_00130), False), - ("FOO", None, pytest.raises(ValueError, match=MATCH_00130), False), + (None, None, pytest.raises(TypeError, match=MATCH_00130), False), + ("FOO", None, pytest.raises(TypeError, match=MATCH_00130), False), ], ) def test_image_policy_common_00130( @@ -667,12 +667,12 @@ def test_image_policy_common_00130( Summary Verify that instance.results.result returns expected values and - raises ValueError appropriately. + raises TypeError appropriately. Test - instance.results.result returns expected values - - ValueError is raised when unexpected values are passed - - ValueError is not raised when expected values are passed + - TypeError is raised when unexpected values are passed + - TypeError is not raised when expected values are passed """ with does_not_raise(): instance = image_policy_common @@ -693,8 +693,8 @@ def test_image_policy_common_00130( [ ({}, {"sequence_number": 0}, does_not_raise(), True), ({"foo": "bar"}, {"foo": "bar", "sequence_number": 0}, does_not_raise(), True), - (None, None, pytest.raises(ValueError, match=MATCH_00140), False), - ("FOO", None, pytest.raises(ValueError, match=MATCH_00140), False), + (None, None, pytest.raises(TypeError, match=MATCH_00140), False), + ("FOO", None, pytest.raises(TypeError, match=MATCH_00140), False), ], ) def test_image_policy_common_00140( @@ -712,8 +712,8 @@ def test_image_policy_common_00140( Test - instance.results.result_current returns expected values - - ValueError is raised when unexpected values are passed - - ValueError is not raised when expected values are passed + - TypeError is raised when unexpected values are passed + - TypeError is not raised when expected values are passed """ with does_not_raise(): instance = image_policy_common From eb2b40619760e493b85d1ded247bfae29987e203 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 3 Jun 2024 15:01:00 -1000 Subject: [PATCH 114/230] MaintenanceMode: use RestSend() v2, more... 1. test_maintenance_mode.py: Modify to reflect changes to MaintenanceMode() 2. dcnm_maintenance_mode.py: Query(): Modify the Results() update - Add a custom response. - Change action to "maintenance_mode_info" 3. FabricDetails() v2. New class in module_utils/fabric to eventually replace FabricDetails() v1. 4. SwichDetails(): Update docstrings. Rename validate_commit_parameters() to validate_refresh_parameters() 5. SwichDetails(): Wrap RestSend() and Results() in try-except blocks. 6. SwichDetails(): Update rest_send and results properties to raise TypeError if not passed instances of RestSend() and Results(), respectively. 7. RestSend() v2: Update docstrings. 8. MaintenanceMode(): Use RestSend() v2 9. MaintenanceModeInfo(): Use RestSend() v2 --- .../module_utils/common/maintenance_mode.py | 35 +- plugins/module_utils/common/rest_send_v2.py | 91 ++- plugins/module_utils/common/switch_details.py | 154 +++- .../module_utils/fabric/fabric_details_v2.py | 711 ++++++++++++++++++ plugins/modules/dcnm_maintenance_mode.py | 10 +- .../common/test_maintenance_mode.py | 4 +- 6 files changed, 931 insertions(+), 74 deletions(-) create mode 100644 plugins/module_utils/fabric/fabric_details_v2.py diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 51392c229..9df591373 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -29,7 +29,7 @@ ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ SwitchDetails -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ FabricDetailsByName @@ -346,6 +346,7 @@ def change_system_mode(self) -> None: - ``ValueError`` if: - ``fabric_name`` is invalid. - endpoint cannot be resolved. + - ``Results()`` raises an exception. - ``TypeError`` if: - ``serial_number`` is not a string. """ @@ -390,14 +391,19 @@ def change_system_mode(self) -> None: } # register result - self.results.action = self.action - self.results.check_mode = self.check_mode - self.results.state = self.state - self.results.response_current = copy.deepcopy( - self.rest_send.response_current - ) - self.results.result_current = copy.deepcopy(self.rest_send.result_current) - self.results.register_task_result() + try: + self.results.action = "change_sytem_mode" + self.results.check_mode = self.check_mode + self.results.state = self.state + self.results.response_current = copy.deepcopy( + self.rest_send.response_current + ) + self.results.result_current = copy.deepcopy( + self.rest_send.result_current + ) + self.results.register_task_result() + except (TypeError, ValueError) as error: + raise ValueError(error) from error if self.results.response_current["RETURN_CODE"] != 200: msg = f"{self.class_name}.{method_name}: " @@ -841,11 +847,14 @@ def refresh(self): self.verify_refresh_parameters() - self.switch_details.rest_send = self.rest_send - self.fabric_details.rest_send = self.rest_send + try: + self.switch_details.rest_send = self.rest_send + self.fabric_details.rest_send = self.rest_send - self.switch_details.results = self.results - self.fabric_details.results = self.results + self.switch_details.results = self.results + self.fabric_details.results = self.results + except TypeError as error: + raise ValueError(error) from error try: self.switch_details.refresh() diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 572e40d0b..1c80a2d57 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -42,6 +42,28 @@ class RestSend: - The response handler interface is defined in ``module_utils/common/response_handler.py`` + ### Raises + - ``ValueError`` if: + - self._verify_commit_parameters() raises + ``ValueError`` + - ResponseHandler() raises ``TypeError`` or ``ValueError`` + - Sender().commit() raises ``ValueError`` + - ``verb`` is not a valid verb (GET, POST, PUT, DELETE) + - ``TypeError`` if: + - ``check_mode`` is not a ``bool`` + - ``path`` is not a ``str`` + - ``payload`` is not a ``dict`` + - ``response`` is not a ``dict`` + - ``response_current`` is not a ``dict`` + - ``response_handler`` is not an instance of + ``ResponseHandler()`` + - ``result`` is not a ``dict`` + - ``result_current`` is not a ``dict`` + - ``send_interval`` is not an ``int`` + - ``sender`` is not an instance of ``Sender()`` + - ``timeout`` is not an ``int`` + - ``unit_test`` is not a ``bool`` + ### Usage discussion - A Sender() class is used in the usage example below that requires an instance of ``AnsibleModule``, and uses ``dcnm_send()`` to send @@ -59,19 +81,22 @@ class RestSend: sender = Sender() # class that implements the sender interface sender.ansible_module = ansible_module - rest_send = RestSend() - rest_send.sender = sender - rest_send.response_handler = ResponseHandler() - rest_send.unit_test = True # optional, use in unit tests for speed - rest_send.path = "/rest/top-down/fabrics" - rest_send.verb = "GET" - rest_send.payload = my_payload # optional - rest_send.save_settings() # save current check_mode and timeout - rest_send.timeout = 300 # optional - rest_send.check_mode = True - # Do things with rest_send... - rest_send.commit() - rest_send.restore_settings() # restore check_mode and timeout + try: + rest_send = RestSend() + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + rest_send.unit_test = True # optional, use in unit tests for speed + rest_send.path = "/rest/top-down/fabrics" + rest_send.verb = "GET" + rest_send.payload = my_payload # optional + rest_send.save_settings() # save current check_mode and timeout + rest_send.timeout = 300 # optional + rest_send.check_mode = True + # Do things with rest_send... + rest_send.commit() + rest_send.restore_settings() # restore check_mode and timeout + except (TypeError, ValueError) as error: + # Handle error # list of responses from the controller for this session response = rest_send.response @@ -200,7 +225,31 @@ def save_settings(self): def commit(self): """ + ### Summary Send the REST request to the controller + + ### Raises + - ``ValueError`` if: + - RestSend()._verify_commit_parameters() raises + ``ValueError`` + - ResponseHandler() raises ``TypeError`` or ``ValueError`` + - Sender().commit() raises ``ValueError`` + - ``verb`` is not a valid verb (GET, POST, PUT, DELETE) + - ``TypeError`` if: + - ``check_mode`` is not a ``bool`` + - ``path`` is not a ``str`` + - ``payload`` is not a ``dict`` + - ``response`` is not a ``dict`` + - ``response_current`` is not a ``dict`` + - ``response_handler`` is not an instance of + ``ResponseHandler()`` + - ``result`` is not a ``dict`` + - ``result_current`` is not a ``dict`` + - ``send_interval`` is not an ``int`` + - ``sender`` is not an instance of ``Sender()`` + - ``timeout`` is not an ``int`` + - ``unit_test`` is not a ``bool`` + """ msg = f"{self.class_name}.commit: " msg += f"check_mode: {self.check_mode}." @@ -216,7 +265,13 @@ def commit_check_mode(self): Simulate a controller request for check_mode. ### Raises - None + - ``ValueError`` if: + - ResponseHandler() raises ``TypeError`` or ``ValueError`` + - self.response_current raises ``TypeError`` + - self.result_current raises ``TypeError`` + - self.response raises ``TypeError`` + - self.result raises ``TypeError`` + ### Properties read: - ``verb``: HTTP verb e.g. DELETE, GET, POST, PUT @@ -244,17 +299,17 @@ def commit_check_mode(self): response_current["MESSAGE"] = "OK" response_current["CHECK_MODE"] = True response_current["DATA"] = "[simulated-check-mode-response:Success]" - self.response_current = response_current try: + self.response_current = response_current self.response_handler.response = self.response_current self.response_handler.verb = self.verb self.response_handler.commit() self.result_current = self.response_handler.result + self.response = copy.deepcopy(self.response_current) + self.result = copy.deepcopy(self.result_current) except (TypeError, ValueError) as error: raise ValueError(error) from error - self.response = copy.deepcopy(self.response_current) - self.result = copy.deepcopy(self.result_current) def commit_normal_mode(self): """ @@ -314,7 +369,7 @@ def commit_normal_mode(self): self.response_handler.verb = self.verb self.response_handler.commit() self.result_current = self.response_handler.result - except ValueError as error: + except (TypeError, ValueError) as error: raise ValueError(error) from error msg = f"{self.class_name}.{method_name}: " diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 8b06e6f73..9151915e2 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -34,20 +34,29 @@ class SwitchDetails: Retrieve switch details from the controller and provide property accessors for the switch attributes. + ### Raises + - ``ControllerResponseError`` if: + - The controller RETURN_CODE is not 200. + - ``ValueError`` if: + - Mandatory parameters are not set. + - There was an error configuring RestSend() e.g. invalid + property values, etc. + ### Usage ```python - instance = SwitchDetails() - instance.results = Results() - instance.rest_send = RestSend(ansible_module) - instance.refresh() + try: + instance = SwitchDetails() + instance.results = Results() + instance.rest_send = RestSend(ansible_module) + instance.refresh() + except (ControllerResponseError, ValueError) as error: + # Handle error instance.filter = "10.1.1.1" fabric_name = instance.fabric_name serial_number = instance.serial_number etc... ``` - ### Endpoint - ``/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches`` """ def __init__(self): @@ -70,8 +79,9 @@ def _init_properties(self): self.properties["info"] = {} self.properties["params"] = None - def validate_commit_parameters(self) -> None: + def validate_refresh_parameters(self) -> None: """ + ### Summary Validate that mandatory parameters are set before calling refresh(). ### Raises @@ -96,18 +106,22 @@ def send_request(self) -> None: Send the request to the controller. ### Raises - None + - ``ValueError`` if the RestSend object raises + ``TypeError`` or ``ValueError``. """ # Send request - self.rest_send.save_settings() - self.rest_send.timeout = 1 - # Regardless of ansible_module.check_mode, we need to get the - # switch details. So, set check_mode to False. - self.rest_send.check_mode = False - self.rest_send.verb = self.verb - self.rest_send.path = self.path - self.rest_send.commit() - self.rest_send.restore_settings() + try: + self.rest_send.save_settings() + self.rest_send.timeout = 1 + # Regardless of ansible_module.check_mode, we need to get the + # switch details. So, set check_mode to False. + self.rest_send.check_mode = False + self.rest_send.verb = self.verb + self.rest_send.path = self.path + self.rest_send.commit() + self.rest_send.restore_settings() + except (TypeError, ValueError) as error: + raise ValueError(error) from error def update_results(self) -> None: """ @@ -115,22 +129,27 @@ def update_results(self) -> None: Update and register the results. ### Raises - - ``ControllerResponseError`` if the controller response is not 200. + - ``ControllerResponseError`` if: + - The controller RETURN_CODE is not 200. + - ``ValueError`` if: + - ``Results()`` raises ``TypeError``. """ method_name = inspect.stack()[0][3] # Update and register results - self.results.action = self.action - self.results.response_current = self.rest_send.response_current - self.results.result_current = self.rest_send.result_current - # SwitchDetails never changes the controller state - self.results.changed = False - - if self.results.response_current["RETURN_CODE"] == 200: - self.results.failed = False - else: - self.results.failed = True - - self.results.register_task_result() + try: + self.results.action = self.action + self.results.response_current = self.rest_send.response_current + self.results.result_current = self.rest_send.result_current + # SwitchDetails never changes the controller state + self.results.changed = False + + if self.results.response_current["RETURN_CODE"] == 200: + self.results.failed = False + else: + self.results.failed = True + self.results.register_task_result() + except TypeError as error: + raise ValueError(error) from error if self.results.failed is True: msg = f"{self.class_name}.{method_name}: " @@ -144,16 +163,29 @@ def refresh(self): the controller. ### Raises - - ``ControllerResponseError`` if the controller response is not 200. - - ``ValueError`` if mandatory parameters are not set. + - ``ControllerResponseError`` if: + - The controller RETURN_CODE is not 200. + - ``ValueError`` if + - Mandatory parameters are not set. + - There was an error configuring RestSend() e.g. + invalid property values, etc. """ - + method_name = inspect.stack()[0][3] try: - self.validate_commit_parameters() + self.validate_refresh_parameters() except ValueError as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Mandatory parameters need review. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error - self.send_request() + try: + self.send_request() + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error sending request to the controller. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error try: self.update_results() @@ -416,23 +448,69 @@ def release(self): @property def rest_send(self): """ - An instance of the ``RestSend`` class. + ### Summary + An instance of the RestSend class. + + ### Raises + - setter: ``TypeError`` if the value is not an instance of RestSend. + + ### getter + Return an instance of the RestSend class. + + ### setter + Set an instance of the RestSend class. """ return self.properties["rest_send"] @rest_send.setter def rest_send(self, value): + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "RestSend" + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) self.properties["rest_send"] = value @property def results(self): """ - An instance of the ``Results`` class. + ### Summary + An instance of the Results class. + + ### Raises + - setter: ``TypeError`` if the value is not an instance of Results. + + ### getter + Return an instance of the Results class. + + ### setter + Set an instance of the Results class. """ return self.properties["results"] @results.setter def results(self, value): + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "Results" + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) self.properties["results"] = value @property diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py new file mode 100644 index 000000000..8fb18ef17 --- /dev/null +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -0,0 +1,711 @@ +# +# 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 logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ + EpFabrics +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils + + +class FabricDetails: + """ + ### Summary + Parent class for *FabricDetails() subclasses. + See subclass docstrings for details. + + ### Raises + - ``ValueError`` if: + - Mandatory properties are not set. + - RestSend object raises ``TypeError`` or ``ValueError``. + - ``params`` is missing ``check_mode`` key. + - ``params`` is missing ``state`` key. + + params is AnsibleModule.params + """ + + def __init__(self, params): + self.class_name = self.__class__.__name__ + + self.params = params + self.check_mode = self.params.get("check_mode", None) + if self.check_mode is None: + msg = f"{self.class_name}.__init__(): " + msg += "check_mode is required" + raise ValueError(msg) + + self.state = self.params.get("state", None) + if self.state is None: + msg = f"{self.class_name}.__init__(): " + msg += "state is required" + raise ValueError(msg) + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + msg = "ENTERED FabricDetails() (v2)" + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + + self.data = {} + self.conversion = ConversionUtils() + self.ep_fabrics = EpFabrics() + + self._init_properties() + + def _init_properties(self): + self.properties = {} + self.properties["rest_send"] = None + self.properties["results"] = None + + def register_result(self): + """ + ### Summary + Update the results object with the current state of the fabric + details and register the result. + + ### Raises + + """ + self.results.action = "fabric_details" + self.results.response_current = self.rest_send.response_current + self.results.result_current = self.rest_send.result_current + if self.results.response_current.get("RETURN_CODE") == 200: + self.results.failed = False + else: + self.results.failed = True + # FabricDetails never changes the controller state + self.results.changed = False + self.results.register_task_result() + + def validate_refresh_parameters(self) -> None: + """ + ### Summary + Validate that mandatory parameters are set before calling refresh(). + + ### Raises + - ``ValueError`` if instance.rest_send is not set. + - ``ValueError`` if instance.results is not set. + """ + method_name = inspect.stack()[0][3] + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.rest_send must be set before calling " + msg += f"{self.class_name}.refresh()." + raise ValueError(msg) + if self.results is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.results must be set before calling " + msg += f"{self.class_name}.refresh()." + raise ValueError(msg) + + def refresh_super(self): + """ + ### Summary + Refresh the fabric details from the controller and + populate self.data with the results. + + ### Raises + - ``ValueError`` if the RestSend object raises + ``TypeError`` or ``ValueError``. + + ### Notes + - ``self.data`` is a dictionary of fabric details, keyed on + fabric name. + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + try: + self.validate_refresh_parameters() + except ValueError as error: + raise ValueError(error) from error + + try: + self.rest_send.path = self.ep_fabrics.path + self.rest_send.verb = self.ep_fabrics.verb + + # We always want to get the controller's current fabric state, + # regardless of the current value of check_mode. + # We save the current check_mode and timeout settings, set + # rest_send.check_mode to False so the request will be sent + # to the controller, and then restore the original settings. + + self.rest_send.save_settings() + self.rest_send.check_mode = False + self.rest_send.timeout = 1 + self.rest_send.commit() + self.rest_send.restore_settings() + except (TypeError, ValueError) as error: + raise ValueError(error) from error + + self.data = {} + if self.rest_send.response_current.get("DATA") is None: + # The DATA key should always be present. We should never hit this. + return + for item in self.rest_send.response_current.get("DATA"): + fabric_name = item.get("nvPairs", {}).get("FABRIC_NAME", None) + if fabric_name is None: + return + self.data[fabric_name] = item + + self.register_result() + + msg = f"{self.class_name}.{method_name}: calling self.rest_send.commit() DONE" + self.log.debug(msg) + + def _get(self, item): + """ + overridden in subclasses + """ + + def _get_nv_pair(self, item): + """ + overridden in subclasses + """ + + @property + def all_data(self): + """ + Return all fabric details from the controller (i.e. self.data) + """ + return self.data + + @property + def asn(self): + """ + Return the BGP asn of the fabric specified with filter, if it exists. + Return None otherwise + + Type: string + Possible values: + - e.g. 65000 + - None + """ + try: + return self._get("asn") + except ValueError as error: + msg = f"Failed to retrieve asn: Error detail: {error}" + self.log.debug(msg) + return None + + @property + def deployment_freeze(self): + """ + Return the nvPairs.DEPLOYMENT_FREEZE of the fabric specified with filter, + if it exists. + Return None otherwise + + Type: string + Possible values: + - true + - false + """ + try: + return self._get_nv_pair("DEPLOYMENT_FREEZE") + except ValueError as error: + msg = f"Failed to retrieve deployment_freeze: Error detail: {error}" + self.log.debug(msg) + return None + + @property + def enable_pbr(self): + """ + Return the PBR enable state of the fabric specified with filter, + if it exists. + Return None otherwise + + Type: boolean + Possible values: + - True + - False + - None + """ + try: + return self._get_nv_pair("ENABLE_PBR") + except ValueError as error: + msg = f"Failed to retrieve enable_pbr: Error detail: {error}" + self.log.debug(msg) + return None + + @property + def fabric_id(self): + """ + Return the fabricId of the fabric specified with filter, + if it exists. + Return None otherwise + + Type: string + Possible values: + - e.g. FABRIC-5 + - None + """ + try: + return self._get("fabricId") + except ValueError as error: + msg = f"Failed to retrieve fabric_id: Error detail: {error}" + self.log.debug(msg) + return None + + @property + def fabric_type(self): + """ + Return the nvPairs.FABRIC_TYPE of the fabric specified with filter, + if it exists. + Return None otherwise + + Type: string + Possible values: + - Switch_Fabric + - None + """ + try: + return self._get_nv_pair("FABRIC_TYPE") + except ValueError as error: + msg = f"Failed to retrieve fabric_type: Error detail: {error}" + self.log.debug(msg) + return None + + @property + def is_read_only(self): + """ + Return the nvPairs.IS_READ_ONLY of the fabric specified with filter, + if it exists. + Return None otherwise + + Type: string + Possible values: + - true + - false + """ + try: + return self._get_nv_pair("IS_READ_ONLY") + except ValueError as error: + msg = f"Failed to retrieve is_read_only: Error detail: {error}" + self.log.debug(msg) + return None + + @property + def replication_mode(self): + """ + Return the nvPairs.REPLICATION_MODE of the fabric specified with filter, + if it exists. + Return None otherwise + + Type: string + Possible values: + - Ingress + - Multicast + - None + """ + try: + return self._get_nv_pair("REPLICATION_MODE") + except ValueError as error: + msg = f"Failed to retrieve replication_mode: Error detail: {error}" + self.log.debug(msg) + return None + + @property + def rest_send(self): + """ + ### Summary + An instance of the RestSend class. + + ### Raises + - setter: ``TypeError`` if the value is not an instance of RestSend. + + ### getter + Return an instance of the RestSend class. + + ### setter + Set an instance of the RestSend class. + """ + return self.properties["rest_send"] + + @rest_send.setter + def rest_send(self, value): + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "RestSend" + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) + self.properties["rest_send"] = value + + @property + def results(self): + """ + ### Summary + An instance of the Results class. + + ### Raises + - setter: ``TypeError`` if the value is not an instance of Results. + + ### getter + Return an instance of the Results class. + + ### setter + Set an instance of the Results class. + """ + return self.properties["results"] + + @results.setter + def results(self, value): + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "Results" + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) + self.properties["results"] = value + + @property + def template_name(self): + """ + Return the templateName of the fabric specified with filter, + if it exists. + Return None otherwise + + Type: string + Possible values: + - Easy_Fabric + - TODO - add other values + - None + """ + try: + return self._get("templateName") + except ValueError as error: + msg = f"Failed to retrieve template_name: Error detail: {error}" + self.log.debug(msg) + return None + + +class FabricDetailsByName(FabricDetails): + """ + Retrieve fabric details from the controller and provide + property accessors for the fabric attributes. + + Usage (where params is AnsibleModule.params): + + ```python + sender = Sender() # class that implements the sender interface + sender.ansible_module = ansible_module + + rest_send = RestSend() + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + + instance = FabricDetailsByName(params) + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "MyFabric" + # BGP AS for fabric "MyFabric" + bgp_as = instance.asn + + # all fabric details for "MyFabric" + fabric_dict = instance.filtered_data + if fabric_dict is None: + # fabric does not exist on the controller + # etc... + ``` + + Or: + + ```python + sender = Sender() # class that implements the sender interface + sender.ansible_module = ansible_module + + rest_send = RestSend() + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + + instance = FabricDetailsByName(params) + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + all_fabrics = instance.all_data + ``` + + - Where ``all_fabrics`` will be a dictionary of all fabrics + on the controller, keyed on fabric name. + """ + + def __init__(self, params): + super().__init__(params) + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED FabricDetailsByName() " + msg += f"params {params}." + self.log.debug(msg) + + self.data_subclass = {} + self.build_properties() + + def build_properties(self): + """ + ### Summary + Build the properties dictionary for the class. + The dictionary has already been initialized in the parent class. + + ### Raises + None + """ + self.properties["filter"] = None + + def refresh(self): + """ + ### Refresh fabric_name current details from the controller + + ### Raises + - ``ValueError`` if: + - Mandatory properties are not set. + """ + self.refresh_super() + self.data_subclass = copy.deepcopy(self.data) + + def _get(self, item): + """ + Retrieve the value of the top-level (non-nvPair) item for fabric_name + (anything not in the nvPairs dictionary). + + - raise ``ValueError`` if ``self.filter`` has not been set. + - raise ``ValueError`` if ``self.filter`` (fabric_name) does not exist + on the controller. + - raise ``ValueError`` if item is not a valid property name for the fabric. + + See also: ``_get_nv_pair()`` + """ + method_name = inspect.stack()[0][3] + + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.filter {self.filter} " + self.log.debug(msg) + + if self.filter is None: + msg = f"{self.class_name}.{method_name}: " + msg += "set instance.filter to a fabric name " + msg += f"before accessing property {item}." + raise ValueError(msg) + + if self.data_subclass.get(self.filter) is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"fabric_name {self.filter} does not exist on the controller." + raise ValueError(msg) + + if self.data_subclass[self.filter].get(item) is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.filter} unknown property name: {item}." + raise ValueError(msg) + + return self.conversion.make_none( + self.conversion.make_boolean(self.data_subclass[self.filter].get(item)) + ) + + def _get_nv_pair(self, item): + """ + # Retrieve the value of the nvPair item for fabric_name. + + - raise ``ValueError`` if ``self.filter`` has not been set. + - raise ``ValueError`` if ``self.filter`` (fabric_name) does not exist on the controller. + - raise ``ValueError`` if item is not a valid property name for the fabric. + + See also: ``self._get()`` + """ + method_name = inspect.stack()[0][3] + + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.filter {self.filter} " + self.log.debug(msg) + + if self.filter is None: + msg = f"{self.class_name}.{method_name}: " + msg += "set instance.filter to a fabric name " + msg += f"before accessing property {item}." + raise ValueError(msg) + + if self.data_subclass.get(self.filter) is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"fabric_name {self.filter} " + msg += "does not exist on the controller." + raise ValueError(msg) + + if self.data_subclass[self.filter].get("nvPairs", {}).get(item) is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"fabric_name {self.filter} " + msg += f"unknown property name: {item}." + raise ValueError(msg) + + return self.conversion.make_none( + self.conversion.make_boolean( + self.data_subclass[self.filter].get("nvPairs").get(item) + ) + ) + + @property + def filtered_data(self): + """ + - Return a dictionary of the fabric matching self.filter. + - Return None if the fabric does not exist on the controller. + - raise ``ValueError`` if self.filter has not been set. + """ + method_name = inspect.stack()[0][3] + if self.filter is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.filter must be set before calling " + msg += f"{self.class_name}.filtered_data" + raise ValueError(msg) + return self.data_subclass.get(self.filter, None) + + @property + def filter(self): + """ + Set the fabric_name of the fabric to query. + + This needs to be set before accessing this class's properties. + """ + return self.properties.get("filter") + + @filter.setter + def filter(self, value): + self.properties["filter"] = value + + +class FabricDetailsByNvPair(FabricDetails): + """ + Retrieve fabric details from the controller filtered + by nvPair key and value. This sets the filtered_data + property to a dictionary of all fabrics on the controller + that match filter_key and filter_value. + + Usage (where params is AnsibleModule.params): + + instance = FabricDetailsNvPair(params) + instance.refresh() + instance.filter_key = "DCI_SUBNET_RANGE" + instance.filter_value = "10.33.0.0/16" + fabrics = instance.filtered_data + """ + + def __init__(self, params): + super().__init__(params) + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + msg = "ENTERED FabricDetailsByNvPair() " + self.log.debug(msg) + + self.data_subclass = {} + + self.build_properties() + + def build_properties(self): + """ + ### Summary + Build the properties dictionary for the class. + The dictionary has already been initialized in the parent class. + + ### Raises + None + """ + self.properties["filter_key"] = None + self.properties["filter_value"] = None + + def refresh(self): + """ + Refresh fabric_name current details from the controller. + + - raise ValueError if self.filter_key has not been set. + """ + method_name = inspect.stack()[0][3] + + if self.filter_key is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"set {self.class_name}.filter_key to a nvPair key " + msg += f"before calling {self.class_name}.refresh()." + raise ValueError(msg) + if self.filter_value is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"set {self.class_name}.filter_value to a nvPair value " + msg += f"before calling {self.class_name}.refresh()." + raise ValueError(msg) + + self.refresh_super() + for item, value in self.data.items(): + if value.get("nvPairs", {}).get(self.filter_key) == self.filter_value: + self.data_subclass[item] = value + + @property + def filtered_data(self): + """ + - Return a ``dict`` of the fabric(s) matching ``self.filter_key`` + and ``self.filter_value``. + - Return an empty ``dict`` if the fabric does not exist on + the controller. + """ + return self.data_subclass + + @property + def filter_key(self): + """ + - getter: Return the nvPairs key to filter on. + - setter: Set the nvPairs key to filter on. + + This should be an exact match for the key in the nvPairs + dictionary for the fabric. + """ + return self.properties.get("filter_key") + + @filter_key.setter + def filter_key(self, value): + self.properties["filter_key"] = value + + @property + def filter_value(self): + """ + - getter: Return the nvPairs value to filter on. + - setter: Set the nvPairs value to filter on. + + This should be an exact match for the value in the nvPairs + dictionary for the fabric. + """ + return self.properties.get("filter_value") + + @filter_value.setter + def filter_value(self, value): + self.properties["filter_value"] = value diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 183702631..416a441ee 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -1202,11 +1202,15 @@ def commit(self) -> None: except ValueError as error: raise ValueError(error) from error - # If we got this far, the request was successful. - self.results.diff_current = self.have + # If we got this far, the requests were successful. + self.results.action = "maintenance_mode_info" self.results.changed = False - self.results.action = "query" + self.results.diff_current = self.have self.results.failed = False + self.results.response_current = {"MESSAGE": "MaintenanceModeInfo OK."} + self.results.response_current.update({"METHOD": "NA"}) + self.results.response_current.update({"REQUEST_PATH": "NA"}) + self.results.response_current.update({"RETURN_CODE": 200}) self.results.result_current = {"changed": False, "success": True} self.results.register_task_result() diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 3d31e148f..33b1896af 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -418,7 +418,7 @@ def responses(): assert instance.results.diff[1].get("config_deploy", None) is True assert instance.results.diff[1].get("sequence_number", None) == 2 - assert instance.results.metadata[0].get("action", None) == "maintenance_mode" + assert instance.results.metadata[0].get("action", None) == "change_sytem_mode" assert instance.results.metadata[0].get("sequence_number", None) == 1 assert instance.results.metadata[0].get("state", None) == "merged" @@ -525,7 +525,7 @@ def responses(): assert isinstance(instance.results.result, list) assert len(instance.results.diff[0]) == 1 - assert instance.results.metadata[0].get("action", None) == "maintenance_mode" + assert instance.results.metadata[0].get("action", None) == "change_sytem_mode" assert instance.results.metadata[0].get("sequence_number", None) == 1 assert instance.results.metadata[0].get("state", None) == "merged" From 260f56fcd793b93703db1a0d45224aba9c1aa89d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 3 Jun 2024 16:19:22 -1000 Subject: [PATCH 115/230] FabricDetails() v2: 49% unit test coverage. 1. FabricDetails()__init__() v2: improve error messages when check_mode and state are missing from params. 2. FabricDetails() v2: Initial batch of unit tests. --- .../module_utils/fabric/fabric_details_v2.py | 12 +- .../fixtures/responses_FabricDetails_V2.json | 344 ++++++++++++++++ .../dcnm_fabric/test_fabric_details_v2.py | 368 ++++++++++++++++++ tests/unit/modules/dcnm/dcnm_fabric/utils.py | 20 + 4 files changed, 739 insertions(+), 5 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetails_V2.json create mode 100644 tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index 8fb18ef17..fd26f74fa 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -46,18 +46,20 @@ class FabricDetails: def __init__(self, params): self.class_name = self.__class__.__name__ - + method_name = inspect.stack()[0][3] self.params = params self.check_mode = self.params.get("check_mode", None) if self.check_mode is None: - msg = f"{self.class_name}.__init__(): " - msg += "check_mode is required" + msg = f"{self.class_name}.{method_name}: " + msg += "check_mode is missing from params. " + msg += f"params: {params}." raise ValueError(msg) self.state = self.params.get("state", None) if self.state is None: - msg = f"{self.class_name}.__init__(): " - msg += "state is required" + msg = f"{self.class_name}.{method_name}: " + msg += "state is missing from params. " + msg += f"params: {params}." raise ValueError(msg) self.log = logging.getLogger(f"dcnm.{self.class_name}") diff --git a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetails_V2.json b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetails_V2.json new file mode 100644 index 000000000..d3fac4a7c --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetails_V2.json @@ -0,0 +1,344 @@ +{ + "test_notes": [ + "Mocked responses for FabricDetails() class" + ], + "test_fabric_details_v2_00100a": { + "TEST_NOTES": [ + "DATA is an empty list", + "RETURN_CODE is 200" + ], + "DATA": [], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_v2_00110a": { + "TEST_NOTES": [ + "DATA key is missing", + "Negative test case", + "RETURN_CODE is 200" + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_v2_00120a": { + "TEST_NOTES": [ + "DATA contains one fabric dict", + "RETURN_CODE is 200" + ], + "DATA": [ + { + "asn": "65001", + "createdOn": 1711411093680, + "deviceType": "n9k", + "fabricId": "FABRIC-2", + "fabricName": "f1", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN EVPN", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", + "id": 2, + "modifiedOn": 1711411096857, + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplate": "Default_Network_Universal", + "nvPairs": { + "AAA_REMOTE_IP_ENABLED": "false", + "AAA_SERVER_CONF": "", + "ACTIVE_MIGRATION": "false", + "ADVERTISE_PIP_BGP": "false", + "ADVERTISE_PIP_ON_BORDER": "true", + "AGENT_INTF": "eth0", + "AI_ML_QOS_POLICY": "AI_Fabric_QOS_400G", + "ALLOW_NXC": "true", + "ALLOW_NXC_PREV": "true", + "ANYCAST_BGW_ADVERTISE_PIP": "false", + "ANYCAST_GW_MAC": "2020.0000.00aa", + "ANYCAST_LB_ID": "", + "ANYCAST_RP_IP_RANGE": "10.254.254.0/24", + "ANYCAST_RP_IP_RANGE_INTERNAL": "", + "AUTO_SYMMETRIC_DEFAULT_VRF": "false", + "AUTO_SYMMETRIC_VRF_LITE": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", + "AUTO_VRFLITE_IFC_DEFAULT_VRF": "false", + "BANNER": "", + "BFD_AUTH_ENABLE": "false", + "BFD_AUTH_KEY": "", + "BFD_AUTH_KEY_ID": "", + "BFD_ENABLE": "false", + "BFD_ENABLE_PREV": "", + "BFD_IBGP_ENABLE": "false", + "BFD_ISIS_ENABLE": "false", + "BFD_OSPF_ENABLE": "false", + "BFD_PIM_ENABLE": "false", + "BGP_AS": "65001", + "BGP_AS_PREV": "65001", + "BGP_AUTH_ENABLE": "false", + "BGP_AUTH_KEY": "", + "BGP_AUTH_KEY_TYPE": "3", + "BGP_LB_ID": "0", + "BOOTSTRAP_CONF": "", + "BOOTSTRAP_ENABLE": "false", + "BOOTSTRAP_ENABLE_PREV": "false", + "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", + "BOOTSTRAP_MULTISUBNET_INTERNAL": "", + "BRFIELD_DEBUG_FLAG": "Disable", + "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", + "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", + "CDP_ENABLE": "false", + "COPP_POLICY": "strict", + "DCI_SUBNET_RANGE": "10.33.0.0/16", + "DCI_SUBNET_TARGET_MASK": "30", + "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", + "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", + "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", + "DEFAULT_VRF_REDIS_BGP_RMAP": "", + "DEPLOYMENT_FREEZE": "false", + "DHCP_ENABLE": "false", + "DHCP_END": "", + "DHCP_END_INTERNAL": "", + "DHCP_IPV6_ENABLE": "", + "DHCP_IPV6_ENABLE_INTERNAL": "", + "DHCP_START": "", + "DHCP_START_INTERNAL": "", + "DNS_SERVER_IP_LIST": "", + "DNS_SERVER_VRF": "", + "DOMAIN_NAME_INTERNAL": "", + "ENABLE_AAA": "false", + "ENABLE_AGENT": "false", + "ENABLE_AI_ML_QOS_POLICY": "false", + "ENABLE_AI_ML_QOS_POLICY_FLAP": "false", + "ENABLE_DEFAULT_QUEUING_POLICY": "false", + "ENABLE_EVPN": "true", + "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", + "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", + "ENABLE_L3VNI_NO_VLAN": "false", + "ENABLE_MACSEC": "false", + "ENABLE_NETFLOW": "false", + "ENABLE_NETFLOW_PREV": "false", + "ENABLE_NGOAM": "true", + "ENABLE_NXAPI": "true", + "ENABLE_NXAPI_HTTP": "true", + "ENABLE_PBR": "false", + "ENABLE_PVLAN": "false", + "ENABLE_PVLAN_PREV": "false", + "ENABLE_SGT": "false", + "ENABLE_SGT_PREV": "false", + "ENABLE_TENANT_DHCP": "true", + "ENABLE_TRM": "false", + "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", + "ESR_OPTION": "PBR", + "EXTRA_CONF_INTRA_LINKS": "", + "EXTRA_CONF_LEAF": "", + "EXTRA_CONF_SPINE": "", + "EXTRA_CONF_TOR": "", + "EXT_FABRIC_TYPE": "", + "FABRIC_INTERFACE_TYPE": "p2p", + "FABRIC_MTU": "9216", + "FABRIC_MTU_PREV": "9216", + "FABRIC_NAME": "f1", + "FABRIC_TYPE": "Switch_Fabric", + "FABRIC_VPC_DOMAIN_ID": "", + "FABRIC_VPC_DOMAIN_ID_PREV": "", + "FABRIC_VPC_QOS": "false", + "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", + "FEATURE_PTP": "false", + "FEATURE_PTP_INTERNAL": "false", + "FF": "Easy_Fabric", + "GRFIELD_DEBUG_FLAG": "Disable", + "HD_TIME": "180", + "HOST_INTF_ADMIN_STATE": "true", + "IBGP_PEER_TEMPLATE": "", + "IBGP_PEER_TEMPLATE_LEAF": "", + "INBAND_DHCP_SERVERS": "", + "INBAND_MGMT": "false", + "INBAND_MGMT_PREV": "false", + "ISIS_AREA_NUM": "0001", + "ISIS_AREA_NUM_PREV": "", + "ISIS_AUTH_ENABLE": "false", + "ISIS_AUTH_KEY": "", + "ISIS_AUTH_KEYCHAIN_KEY_ID": "", + "ISIS_AUTH_KEYCHAIN_NAME": "", + "ISIS_LEVEL": "level-2", + "ISIS_OVERLOAD_ELAPSE_TIME": "", + "ISIS_OVERLOAD_ENABLE": "", + "ISIS_P2P_ENABLE": "", + "L2_HOST_INTF_MTU": "9216", + "L2_HOST_INTF_MTU_PREV": "9216", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "L3VNI_MCAST_GROUP": "", + "L3_PARTITION_ID_RANGE": "50000-59000", + "LINK_STATE_ROUTING": "ospf", + "LINK_STATE_ROUTING_TAG": "UNDERLAY", + "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", + "LOOPBACK0_IPV6_RANGE": "", + "LOOPBACK0_IP_RANGE": "10.2.0.0/22", + "LOOPBACK1_IPV6_RANGE": "", + "LOOPBACK1_IP_RANGE": "10.3.0.0/22", + "MACSEC_ALGORITHM": "", + "MACSEC_CIPHER_SUITE": "", + "MACSEC_FALLBACK_ALGORITHM": "", + "MACSEC_FALLBACK_KEY_STRING": "", + "MACSEC_KEY_STRING": "", + "MACSEC_REPORT_TIMER": "", + "MGMT_GW": "", + "MGMT_GW_INTERNAL": "", + "MGMT_PREFIX": "", + "MGMT_PREFIX_INTERNAL": "", + "MGMT_V6PREFIX": "", + "MGMT_V6PREFIX_INTERNAL": "", + "MPLS_HANDOFF": "false", + "MPLS_ISIS_AREA_NUM": "0001", + "MPLS_ISIS_AREA_NUM_PREV": "", + "MPLS_LB_ID": "", + "MPLS_LOOPBACK_IP_RANGE": "", + "MSO_CONNECTIVITY_DEPLOYED": "", + "MSO_CONTROLER_ID": "", + "MSO_SITE_GROUP_NAME": "", + "MSO_SITE_ID": "", + "MST_INSTANCE_RANGE": "", + "MULTICAST_GROUP_SUBNET": "239.1.1.0/25", + "NETFLOW_EXPORTER_LIST": "", + "NETFLOW_MONITOR_LIST": "", + "NETFLOW_RECORD_LIST": "", + "NETWORK_VLAN_RANGE": "2300-2999", + "NTP_SERVER_IP_LIST": "", + "NTP_SERVER_VRF": "", + "NVE_LB_ID": "1", + "NXAPI_HTTPS_PORT": "443", + "NXAPI_HTTP_PORT": "80", + "NXC_DEST_VRF": "", + "NXC_PROXY_PORT": "8080", + "NXC_PROXY_SERVER": "", + "NXC_SRC_INTF": "", + "OBJECT_TRACKING_NUMBER_RANGE": "100-299", + "OSPF_AREA_ID": "0.0.0.0", + "OSPF_AUTH_ENABLE": "false", + "OSPF_AUTH_KEY": "", + "OSPF_AUTH_KEY_ID": "", + "OVERLAY_MODE": "cli", + "OVERLAY_MODE_PREV": "cli", + "OVERWRITE_GLOBAL_NXC": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV": "false", + "PER_VRF_LOOPBACK_IP_RANGE": "", + "PER_VRF_LOOPBACK_IP_RANGE_V6": "", + "PHANTOM_RP_LB_ID1": "", + "PHANTOM_RP_LB_ID2": "", + "PHANTOM_RP_LB_ID3": "", + "PHANTOM_RP_LB_ID4": "", + "PIM_HELLO_AUTH_ENABLE": "false", + "PIM_HELLO_AUTH_KEY": "", + "PM_ENABLE": "false", + "PM_ENABLE_PREV": "false", + "POWER_REDUNDANCY_MODE": "ps-redundant", + "PREMSO_PARENT_FABRIC": "", + "PTP_DOMAIN_ID": "", + "PTP_LB_ID": "", + "REPLICATION_MODE": "Multicast", + "ROUTER_ID_RANGE": "", + "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", + "RP_COUNT": "2", + "RP_LB_ID": "254", + "RP_MODE": "asm", + "RR_COUNT": "2", + "SEED_SWITCH_CORE_INTERFACES": "", + "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", + "SGT_ID_RANGE": "", + "SGT_NAME_PREFIX": "", + "SGT_PREPROVISION": "", + "SITE_ID": "65001", + "SLA_ID_RANGE": "10000-19999", + "SNMP_SERVER_HOST_TRAP": "true", + "SPINE_COUNT": "0", + "SPINE_SWITCH_CORE_INTERFACES": "", + "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", + "SSPINE_COUNT": "0", + "STATIC_UNDERLAY_IP_ALLOC": "false", + "STP_BRIDGE_PRIORITY": "", + "STP_ROOT_OPTION": "unmanaged", + "STP_VLAN_RANGE": "", + "STRICT_CC_MODE": "false", + "SUBINTERFACE_RANGE": "2-511", + "SUBNET_RANGE": "10.4.0.0/16", + "SUBNET_TARGET_MASK": "30", + "SYSLOG_SERVER_IP_LIST": "", + "SYSLOG_SERVER_VRF": "", + "SYSLOG_SEV": "", + "TCAM_ALLOCATION": "true", + "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", + "UNDERLAY_IS_V6": "false", + "UNNUM_BOOTSTRAP_LB_ID": "", + "UNNUM_DHCP_END": "", + "UNNUM_DHCP_END_INTERNAL": "", + "UNNUM_DHCP_START": "", + "UNNUM_DHCP_START_INTERNAL": "", + "UPGRADE_FROM_VERSION": "", + "USE_LINK_LOCAL": "true", + "V6_SUBNET_RANGE": "", + "V6_SUBNET_TARGET_MASK": "126", + "VPC_AUTO_RECOVERY_TIME": "360", + "VPC_DELAY_RESTORE": "150", + "VPC_DELAY_RESTORE_TIME": "60", + "VPC_DOMAIN_ID_RANGE": "1-1000", + "VPC_ENABLE_IPv6_ND_SYNC": "true", + "VPC_PEER_KEEP_ALIVE_OPTION": "management", + "VPC_PEER_LINK_PO": "500", + "VPC_PEER_LINK_VLAN": "3600", + "VRF_LITE_AUTOCONFIG": "Manual", + "VRF_VLAN_RANGE": "2000-2299", + "abstract_anycast_rp": "anycast_rp", + "abstract_bgp": "base_bgp", + "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", + "abstract_bgp_rr": "evpn_bgp_rr", + "abstract_dhcp": "base_dhcp", + "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", + "abstract_extra_config_leaf": "extra_config_leaf", + "abstract_extra_config_spine": "extra_config_spine", + "abstract_extra_config_tor": "extra_config_tor", + "abstract_feature_leaf": "base_feature_leaf_upg", + "abstract_feature_spine": "base_feature_spine_upg", + "abstract_isis": "base_isis_level2", + "abstract_isis_interface": "isis_interface", + "abstract_loopback_interface": "int_fabric_loopback_11_1", + "abstract_multicast": "base_multicast_11_1", + "abstract_ospf": "base_ospf", + "abstract_ospf_interface": "ospf_interface_11_1", + "abstract_pim_interface": "pim_interface", + "abstract_route_map": "route_map", + "abstract_routed_host": "int_routed_host", + "abstract_trunk_host": "int_trunk_host", + "abstract_vlan_interface": "int_fabric_vlan_11_1", + "abstract_vpc_domain": "base_vpc_domain_11_1", + "default_network": "Default_Network_Universal", + "default_pvlan_sec_network": "", + "default_vrf": "Default_VRF_Universal", + "enableRealTimeBackup": "", + "enableScheduledBackup": "", + "network_extension_template": "Default_Network_Extension_Universal", + "scheduledTime": "", + "temp_anycast_gateway": "anycast_gateway", + "temp_vpc_domain_mgmt": "vpc_domain_mgmt", + "temp_vpc_peer_link": "int_vpc_peer_link_po", + "vrf_extension_template": "Default_VRF_Extension_Universal" + }, + "operStatus": "HEALTHY", + "provisionMode": "DCNMTopDown", + "replicationMode": "Multicast", + "siteId": "65001", + "templateName": "Easy_Fabric", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplate": "Default_VRF_Universal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + } +} diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py new file mode 100644 index 000000000..871aed2bf --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py @@ -0,0 +1,368 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-import +# pylint: disable=redefined-outer-name +# pylint: disable=protected-access +# pylint: disable=unused-argument +# pylint: disable=invalid-name + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import inspect + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ + EpFabrics +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ + FabricDetails +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + MockSender +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( + ResponseGenerator, does_not_raise, fabric_details_v2_fixture, + responses_fabric_details_v2) + + +def test_fabric_details_v2_00000(fabric_details_v2) -> None: + """ + ### Classes and Methods + - FabricCommon + - __init__() + - FabricDetails + - __init__() + + ### Test + - Class attributes are initialized to expected values + - Exception is not raised + """ + with does_not_raise(): + instance = fabric_details_v2 + assert instance.class_name == "FabricDetails" + assert instance.data == {} + assert isinstance(instance.ep_fabrics, EpFabrics) + assert isinstance(instance.conversion, ConversionUtils) + + +def test_fabric_details_v2_00010() -> None: + """ + ### Classes and Methods + - FabricCommon + - __init__() + - FabricDetails + - __init__() + + ### Test + - ``ValueError`` is raised when ``params`` is missing key ``check_mode``. + """ + match = r"FabricDetails\.__init__:\s+" + match += r"check_mode is missing from params\. params:.*\." + with pytest.raises(ValueError, match=match): + instance = FabricDetails({"state": "merged"}) # pylint: disable=unused-variable + + +def test_fabric_details_v2_00020() -> None: + """ + ### Classes and Methods + - FabricCommon + - __init__() + - FabricDetails + - __init__() + + ### Test + - ``ValueError`` is raised when ``params`` is missing key ``state``. + """ + match = r"FabricDetails\.__init__:\s+" + match += r"state is missing from params\. params:.*\." + with pytest.raises(ValueError, match=match): + instance = FabricDetails( + {"check_mode": False} + ) # pylint: disable=unused-variable + + +def test_fabric_details_v2_00100(fabric_details_v2) -> None: + """ + ### Classes and Methods + - FabricCommon() + - __init__() + - FabricDetails() + - __init__() + - refresh_super() + + ### Summary + - Verify refresh_super() behavior when: + - RETURN_CODE is 200. + - DATA is an empty list, indicating no fabrics + exist on the controller. + + ### Code Flow - Setup + - FabricDetails() is instantiated + - FabricDetails().RestSend() is instantiated + - FabricDetails().Results() is instantiated + - FabricDetails().refresh_super() is called + - responses_FabricDetails contains a dict with: + - RETURN_CODE == 200 + - DATA == [] + + ### Code Flow - Test + - FabricDetails().refresh_super() is called + + ### Expected Result + - Exception is not raised + - Results() are updated + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_v2(key) + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + + with does_not_raise(): + rest_send = RestSend({"state": "merged", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = mock_sender + rest_send.unit_test = True + instance = fabric_details_v2 + instance.rest_send = rest_send + instance.results = Results() + + with does_not_raise(): + instance.refresh_super() + + assert isinstance(instance.results.diff, list) + assert isinstance(instance.results.result, list) + assert isinstance(instance.results.response, list) + + assert len(instance.results.diff) == 1 + assert len(instance.results.result) == 1 + assert len(instance.results.response) == 1 + + assert instance.results.diff[0].get("sequence_number", None) == 1 + + assert instance.results.response[0].get("RETURN_CODE", None) == 200 + assert instance.results.result[0].get("found", None) is True + assert instance.results.result[0].get("success", None) is True + + assert False in instance.results.failed + assert True not in instance.results.failed + assert False in instance.results.changed + assert True not in instance.results.changed + + +def test_fabric_details_v2_00110(monkeypatch, fabric_details_v2) -> None: + """ + ### Classes and Methods + - FabricCommon() + - __init__() + - FabricDetails() + - __init__() + - refresh_super() + + ### Summary + - Verify refresh_super() behavior when: + - RETURN_CODE is 200. + - DATA is missing (negative test) + + ### Code Flow - Setup + - FabricDetails() is instantiated + - FabricDetails().RestSend() is instantiated + - FabricDetails().Results() is instantiated + - FabricDetails().refresh_super() is called + - responses_FabricDetails contains a dict with: + - RETURN_CODE == 200 + - DATA is missing + + ### Code Flow - Test + - FabricDetails().refresh_super() is called + + ### Expected Result + - Exception is not raised + - Results() are updated + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_v2(key) + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + + with does_not_raise(): + rest_send = RestSend({"state": "merged", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = mock_sender + rest_send.unit_test = True + instance = fabric_details_v2 + instance.rest_send = rest_send + instance.results = Results() + + with does_not_raise(): + instance.refresh_super() + + assert isinstance(instance.results.diff, list) + assert isinstance(instance.results.result, list) + assert isinstance(instance.results.response, list) + + assert len(instance.results.diff) == 0 + assert len(instance.results.result) == 0 + assert len(instance.results.response) == 0 + + +def test_fabric_details_v2_00120(monkeypatch, fabric_details_v2) -> None: + """ + ### Classes and Methods + - FabricCommon() + - __init__() + - FabricDetails() + - __init__() + - refresh_super() + + ### Summary + - Verify refresh_super() behavior when: + - RETURN_CODE is 200. + - Controller response contains one fabric (f1). + + ### Code Flow - Setup + - FabricDetails() is instantiated + - FabricDetails().RestSend() is instantiated + - FabricDetails().Results() is instantiated + - FabricDetails().refresh_super() is called + - responses_FabricDetails contains a dict with: + - RETURN_CODE == 200 + - DATA == [] + + ###Code Flow - Test + - FabricDetails().refresh_super() is called + + ### Expected Result + - Exception is not raised + - instance.all_data returns expected fabric data + - Results() are updated + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_v2(key) + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + + with does_not_raise(): + rest_send = RestSend({"state": "merged", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = mock_sender + rest_send.unit_test = True + instance = fabric_details_v2 + instance.rest_send = rest_send + instance.results = Results() + + with does_not_raise(): + instance.refresh_super() + + assert isinstance(instance.results.diff, list) + assert isinstance(instance.results.result, list) + assert isinstance(instance.results.response, list) + + assert len(instance.results.diff) == 1 + assert len(instance.results.result) == 1 + assert len(instance.results.response) == 1 + + assert instance.results.diff[0].get("sequence_number", None) == 1 + + assert instance.results.response[0].get("RETURN_CODE", None) == 200 + assert instance.results.result[0].get("found", None) is True + assert instance.results.result[0].get("success", None) is True + + assert False in instance.results.failed + assert True not in instance.results.failed + assert False in instance.results.changed + assert True not in instance.results.changed + + assert instance.all_data.get("f1", {}).get("asn", None) == "65001" + assert instance.all_data.get("f1", {}).get("nvPairs", {}).get("FABRIC_NAME") == "f1" + + +def test_fabric_details_v2_00200(fabric_details_v2) -> None: + """ + ### Classes and Methods + - FabricCommon() + - __init__() + - FabricDetails() + - __init__() + - _get() + + ### Summary + - Verify FabricDetails()._get() returns None since it's implemented + only in subclasses + """ + with does_not_raise(): + instance = fabric_details_v2 + assert instance._get("foo") is None + + +def test_fabric_details_v2_00300(fabric_details_v2) -> None: + """ + ### Classes and Methods + - FabricCommon() + - __init__() + - FabricDetails() + - __init__() + - _get_nv_pair() + + ### Summary + - Verify FabricDetails()._get_nv_pair() returns None since it's implemented + only in subclasses + """ + with does_not_raise(): + instance = fabric_details_v2 + assert instance._get_nv_pair("foo") is None + + +def test_fabric_details_v2_00400(fabric_details_v2) -> None: + """ + ### Classes and Methods + - FabricCommon() + - __init__() + - FabricDetails() + - __init__() + - all_data() + + ### Summary + - Verify FabricDetails().all_data() returns FabricDetails().data + """ + with does_not_raise(): + instance = fabric_details_v2 + instance.data = {"foo": "bar"} + assert instance.all_data == {"foo": "bar"} diff --git a/tests/unit/modules/dcnm/dcnm_fabric/utils.py b/tests/unit/modules/dcnm/dcnm_fabric/utils.py index 2b3ba4dd1..f4c3147be 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/utils.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/utils.py @@ -37,6 +37,8 @@ FabricDelete from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import ( FabricDetails, FabricDetailsByName, FabricDetailsByNvPair) +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ + FabricDetails as FabricDetailsV2 from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_summary import \ FabricSummary from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ @@ -227,6 +229,14 @@ def fabric_details_fixture(): return FabricDetails(instance.params) +@pytest.fixture(name="fabric_details_v2") +def fabric_details_v2_fixture(): + """ + mock FabricDetails() v2 + """ + return FabricDetailsV2(params) + + @pytest.fixture(name="fabric_details_by_name") def fabric_details_by_name_fixture(): """ @@ -497,6 +507,16 @@ def responses_fabric_details(key: str) -> Dict[str, str]: return data +def responses_fabric_details_v2(key: str) -> Dict[str, str]: + """ + Return responses for FabricDetails version 2 + """ + data_file = "responses_FabricDetails_V2" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + def responses_fabric_details_by_name(key: str) -> Dict[str, str]: """ Return responses for FabricDetailsByName From f197d572773cab6b30ced4d92c83d490b9e069b6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 4 Jun 2024 07:30:24 -1000 Subject: [PATCH 116/230] Use class decorators to remove duplicated code 1. Properties(): New class containing: a. properties shared by many classes. b. class decorator wrapper methods 2. MaintenanceMode(): Replace rest_send and results properties with decorators. 3. MaintenanceModeInfo(): Replace rest_send and results properties with decorators. 4. test_maintenance_mode.py: Update unit tests to reflect the above. --- .../module_utils/common/maintenance_mode.py | 183 +++--------------- plugins/module_utils/common/properties.py | 123 ++++++++++++ .../common/test_maintenance_mode.py | 7 +- 3 files changed, 150 insertions(+), 163 deletions(-) create mode 100644 plugins/module_utils/common/properties.py diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 9df591373..4d9a2e20b 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -11,12 +11,14 @@ # 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" +# Required for class decorators +# pylint: disable=no-member + import copy import inspect import logging @@ -27,12 +29,16 @@ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ SwitchDetails from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ FabricDetailsByName +@Properties.add_rest_send +@Properties.add_results class MaintenanceMode: """ ### Summary @@ -46,8 +52,6 @@ class MaintenanceMode: - ``ValueError`` in the following properties: - ``config`` if config contains invalid content. - - ``rest_send`` if value is not an instance of RestSend. - - ``results`` if value is not an instance of Results. - ``commit`` if config, rest_send, or results are not set. - ``commit`` if ``EpMaintenanceModeEnable`` or ``EpMaintenanceModeDisable`` raise ``ValueError``. @@ -136,23 +140,20 @@ def __init__(self, params): self.serial_number_to_ip_address = {} self.valid_modes = ["maintenance", "normal"] - self._init_properties() self.conversion = ConversionUtils() self.ep_maintenance_mode_enable = EpMaintenanceModeEnable() self.ep_maintenance_mode_disable = EpMaintenanceModeDisable() + self._config = None + self._rest_send = None + self._results = None + msg = "ENTERED MaintenanceMode(): " msg += f"check_mode: {self.check_mode}, " msg += f"state: {self.state}" self.log.debug(msg) - def _init_properties(self): - self._properties = {} - self._properties["config"] = None - self._properties["rest_send"] = None - self._properties["results"] = None - def verify_config_parameters(self, value) -> None: """ ### Summary @@ -580,7 +581,7 @@ def config(self) -> list: ] ``` """ - return self._properties["config"] + return self._config @config.setter def config(self, value): @@ -588,77 +589,11 @@ def config(self, value): self.verify_config_parameters(value) except (TypeError, ValueError) as error: raise ValueError(error) from error - self._properties["config"] = value - - @property - def rest_send(self): - """ - ### Summary - An instance of the RestSend class. - - ### Raises - - setter: ``TypeError`` if the value is not an instance of RestSend. - - ### getter - Return an instance of the RestSend class. - - ### setter - Set an instance of the RestSend class. - """ - return self._properties["rest_send"] - - @rest_send.setter - def rest_send(self, value): - method_name = inspect.stack()[0][3] - _class_have = None - _class_need = "RestSend" - msg = f"{self.class_name}.{method_name}: " - msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." - try: - _class_have = value.class_name - except AttributeError as error: - msg += f"Error detail: {error}." - raise TypeError(msg) from error - if _class_have != _class_need: - raise TypeError(msg) - self._properties["rest_send"] = value - - @property - def results(self): - """ - ### Summary - An instance of the Results class. - - ### Raises - - setter: ``TypeError`` if the value is not an instance of Results. - - ### getter - Return an instance of the Results class. - - ### setter - Set an instance of the Results class. - """ - return self._properties["results"] - - @results.setter - def results(self, value): - method_name = inspect.stack()[0][3] - _class_have = None - _class_need = "Results" - msg = f"{self.class_name}.{method_name}: " - msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." - try: - _class_have = value.class_name - except AttributeError as error: - msg += f" Error detail: {error}." - raise TypeError(msg) from error - if _class_have != _class_need: - raise TypeError(msg) - self._properties["results"] = value + self._config = value +@Properties.add_rest_send +@Properties.add_results class MaintenanceModeInfo: """ ### Summary @@ -755,18 +690,14 @@ def __init__(self, params): self.fabric_details = FabricDetailsByName(self.params) self.switch_details = SwitchDetails() - self._init_properties() + self._config = None + self._info = None + self._rest_send = None + self._results = None msg = "ENTERED MaintenanceModeInfo(): " self.log.debug(msg) - def _init_properties(self): - self._properties = {} - self._properties["config"] = None - self._properties["info"] = None - self._properties["rest_send"] = None - self._properties["results"] = None - def verify_refresh_parameters(self) -> None: """ ### Summary @@ -997,7 +928,7 @@ def config(self) -> list: ["172.22.150.2", "172.22.150.3"] ``` """ - return self._properties["config"] + return self._config @config.setter def config(self, value): @@ -1015,7 +946,7 @@ def config(self, value): msg += "containing ip addresses. " msg += f"Got type: {type(item).__name__}." raise TypeError(msg) - self._properties["config"] = value + self._config = value @property def fabric_deployment_disabled(self): @@ -1160,7 +1091,7 @@ def info(self) -> dict: msg += f"{self.class_name}.refresh() must be called before " msg += f"accessing {self.class_name}.{method_name}." raise ValueError(msg) - return copy.deepcopy(self._properties["info"]) + return copy.deepcopy(self._info) @info.setter def info(self, value: dict): @@ -1169,7 +1100,7 @@ def info(self, value: dict): msg += "value must be a dict. " msg += f"Got value {value} of type {type(value).__name__}." raise TypeError(msg) - self._properties["info"] = value + self._info = value @property def mode(self): @@ -1185,74 +1116,6 @@ def mode(self): """ return self._get("mode") - @property - def rest_send(self): - """ - ### Summary - An instance of the RestSend class. - - ### Raises - - setter: ``TypeError`` if the value is not an instance of RestSend. - - ### getter - Return an instance of the RestSend class. - - ### setter - Set an instance of the RestSend class. - """ - return self._properties["rest_send"] - - @rest_send.setter - def rest_send(self, value): - method_name = inspect.stack()[0][3] - _class_have = None - _class_need = "RestSend" - msg = f"{self.class_name}.{method_name}: " - msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." - try: - _class_have = value.class_name - except AttributeError as error: - msg += f"Error detail: {error}." - raise TypeError(msg) from error - if _class_have != _class_need: - raise TypeError(msg) - self._properties["rest_send"] = value - - @property - def results(self): - """ - ### Summary - An instance of the Results class. - - ### Raises - - setter: ``TypeError`` if the value is not an instance of Results. - - ### getter - Return an instance of the Results class. - - ### setter - Set an instance of the Results class. - """ - return self._properties["results"] - - @results.setter - def results(self, value): - method_name = inspect.stack()[0][3] - _class_have = None - _class_need = "Results" - msg = f"{self.class_name}.{method_name}: " - msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." - try: - _class_have = value.class_name - except AttributeError as error: - msg += f" Error detail: {error}." - raise TypeError(msg) from error - if _class_have != _class_need: - raise TypeError(msg) - self._properties["results"] = value - @property def role(self): """ diff --git a/plugins/module_utils/common/properties.py b/plugins/module_utils/common/properties.py new file mode 100644 index 000000000..acb1dd31e --- /dev/null +++ b/plugins/module_utils/common/properties.py @@ -0,0 +1,123 @@ +# 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" + +# Required for class decorators +# pylint: disable=no-member + +import inspect + + +class Properties: + """ + ### Summary + Commonly-used properties and class decorator wrapper methods. + + ### Raises + The following properties raise a ``TypeError`` if the value is not an + instance of the expected class: + - ``rest_send`` + - ``results`` + + ### Properties + - ``rest_send``: Set and return nn instance of the ``RestSend`` class. + - ``results``: Set and return an instance of the ``Results`` class. + """ + + @property + def rest_send(self): + """ + ### Summary + An instance of the RestSend class. + + ### Raises + - setter: ``TypeError`` if the value is not an instance of RestSend. + + ### getter + Return an instance of the RestSend class. + + ### setter + Set an instance of the RestSend class. + """ + return self._rest_send + + @rest_send.setter + def rest_send(self, value): + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "RestSend" + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) + self._rest_send = value + + @property + def results(self): + """ + ### Summary + An instance of the Results class. + + ### Raises + - setter: ``TypeError`` if the value is not an instance of Results. + + ### getter + Return an instance of the Results class. + + ### setter + Set an instance of the Results class. + """ + return self._results + + @results.setter + def results(self, value): + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "Results" + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) + self._results = value + + def add_rest_send(self): + """ + ### Summary + Class decorator method to set the ``rest_send`` property. + """ + self.rest_send = Properties.rest_send + return self + + def add_results(self): + """ + ### Summary + Class decorator method to set the ``results`` property. + """ + self.results = Properties.results + return self diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 33b1896af..90987611c 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -75,9 +75,9 @@ def test_maintenance_mode_00000(maintenance_mode) -> None: """ with does_not_raise(): instance = maintenance_mode - assert instance._properties["config"] is None - assert instance._properties["rest_send"] is None - assert instance._properties["results"] is None + assert instance._rest_send is None + assert instance._results is None + assert instance._config is None assert instance.action == "maintenance_mode" assert instance.class_name == "MaintenanceMode" assert instance.config is None @@ -86,6 +86,7 @@ def test_maintenance_mode_00000(maintenance_mode) -> None: assert instance.serial_number_to_ip_address == {} assert instance.valid_modes == ["maintenance", "normal"] assert instance.state == "merged" + assert instance.config is None assert instance.rest_send is None assert instance.results is None assert isinstance(instance.conversion, ConversionUtils) From eeb51b2dc84ab2de8dc4b0e5e9bfd6126f82b8f8 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 4 Jun 2024 07:59:46 -1000 Subject: [PATCH 117/230] Fix several item assignments MaintenanceModeInfo(): In converting this class to use _var rather than properties["var"], I forgot a few occurances. Cleaned this up. --- plugins/module_utils/common/maintenance_mode.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 4d9a2e20b..85e9ff4bc 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -863,19 +863,19 @@ def _get(self, item): msg += f"property {item}." raise ValueError(msg) - if self.filter not in self._properties["info"]: + if self.filter not in self._info: msg = f"{self.class_name}.{method_name}: " msg += f"Switch with ip_address {self.filter} does not exist on " msg += "the controller." raise ValueError(msg) - if item not in self._properties["info"][self.filter]: + if item not in self._info[self.filter]: msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} does not have a key named {item}." raise ValueError(msg) return self.conversions.make_boolean( - self.conversions.make_none(self._properties["info"][self.filter].get(item)) + self.conversions.make_none(self._info[self.filter].get(item)) ) @property @@ -896,11 +896,11 @@ def filter(self): ``filter`` must be set before accessing this class's properties. """ - return self._properties.get("filter") + return self._filter @filter.setter def filter(self, value): - self._properties["filter"] = value + self._filter = value @property def config(self) -> list: @@ -1086,7 +1086,7 @@ def info(self) -> dict: ``` """ method_name = inspect.stack()[0][3] - if self._properties["info"] is None: + if self._info is None: msg = f"{self.class_name}.{method_name}: " msg += f"{self.class_name}.refresh() must be called before " msg += f"accessing {self.class_name}.{method_name}." From c17625c06a901b3ce6ce320a5ed323f4918f491d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 4 Jun 2024 08:35:20 -1000 Subject: [PATCH 118/230] SwitchDetails(): inject rest_send and results with class decorators SwitchDetails(): Remove rest_send and results and inject from Properties() class. SwitchDetails(): Update class docstrings for consistency. --- plugins/module_utils/common/switch_details.py | 380 +++++++++++------- 1 file changed, 226 insertions(+), 154 deletions(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 9151915e2..ffdb88e8f 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -18,6 +18,9 @@ __metaclass__ = type __author__ = "Allen Robel" +# Required for class decorators +# pylint: disable=no-member + import inspect import logging @@ -27,8 +30,12 @@ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ ControllerResponseError +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties +@Properties.add_rest_send +@Properties.add_results class SwitchDetails: """ Retrieve switch details from the controller and provide property accessors @@ -39,7 +46,7 @@ class SwitchDetails: - The controller RETURN_CODE is not 200. - ``ValueError`` if: - Mandatory parameters are not set. - - There was an error configuring RestSend() e.g. invalid + - There was an error configuring ``RestSend()`` e.g. invalid property values, etc. ### Usage @@ -71,13 +78,10 @@ def __init__(self): self.path = self.ep_all_switches.path self.verb = self.ep_all_switches.verb - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["filter"] = None - self.properties["info"] = {} - self.properties["params"] = None + self._filter = None + self._info = None + self._rest_send = None + self._results = None def validate_refresh_parameters(self) -> None: """ @@ -193,9 +197,9 @@ def refresh(self): raise ControllerResponseError(error) from error data = self.results.response_current.get("DATA") - self.properties["info"] = {} + self._info = {} for switch in data: - self.properties["info"][switch["ipAddress"]] = switch + self._info[switch["ipAddress"]] = switch def _get(self, item): """ @@ -214,19 +218,19 @@ def _get(self, item): msg += f"property {item}." raise ValueError(msg) - if self.filter not in self.properties["info"]: + if self.filter not in self._info: msg = f"{self.class_name}.{method_name}: " msg += f"Switch with ip_address {self.filter} does not exist on " msg += "the controller." raise ValueError(msg) - if item not in self.properties["info"][self.filter]: + if item not in self._info[self.filter]: msg = f"{self.class_name}.{method_name}: " msg += f"{self.filter} does not have a key named {item}." raise ValueError(msg) return self.conversions.make_boolean( - self.conversions.make_none(self.properties["info"][self.filter].get(item)) + self.conversions.make_none(self._info[self.filter].get(item)) ) @property @@ -246,76 +250,113 @@ def filter(self): ``filter`` must be set before accessing this class's properties. """ - return self.properties.get("filter") + return self._filter @filter.setter def filter(self, value): - self.properties["filter"] = value + self._filter = value @property def fabric_name(self): """ - - Return the ``fabricName`` of the filtered switch, if it exists. - - Return ``None`` otherwise. - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``fabricName`` of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``fabricName`` of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("fabricName") @property def freeze_mode(self): """ - - Return the ``freezeMode`` of the filtered switch's fabric, - if it exists. - - Return ``None`` otherwise. - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``freezeMode`` of the filtered switch's fabric. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``freezeMode`` of the filtered switch's fabric, + if it exists. + - ``None`` otherwise. """ return self._get("freezeMode") @property def hostname(self): """ - - Return the ``hostName`` of the filtered switch, if it exists. - - Return ``None`` otherwise. - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``hostName`` of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + ### Returns + - The ``hostName`` of the filtered switch, if it exists. + - ``None`` otherwise. + ### NOTES - - ``hostname`` is None for NDFC version 12.1.2e - - Better to use ``logical_name`` which is populated - in both NDFC versions 12.1.2e and 12.1.3b + - ``hostname`` is None for NDFC version 12.1.2e + - Better to use ``logical_name`` which is populated + in both NDFC versions 12.1.2e and 12.1.3b """ return self._get("hostName") @property def info(self): """ - - Return parsed data from the GET request. - - Return ``None`` otherwise + ### Summary + Parsed data from the GET request. + + ### Raises + None - NOTE: Keyed on ip_address + ### Returns + - Parsed data from the GET request, if it exists. + - ``None`` otherwise + + ### NOTES + - Keyed on ip_address """ - return self.properties["info"] + return self._info @property def is_non_nexus(self): """ - - Return the ``isNonNexus`` status of the filtered switch, if it exists. - - Return ``None`` otherwise - - Example: false, true - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``isNonNexus`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``isNonNexus`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("isNonNexus") @property def logical_name(self): """ - - Return the ``logicalName`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``logicalName`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``logicalName`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("logicalName") @@ -371,24 +412,37 @@ def maintenance_mode(self): @property def managable(self): """ - - Yes, managable is misspelled. It is spelled this way in the - controller response. - - Return the ``managable`` status of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``managable`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``managable`` value of the filtered switch, if it exists. + - ``None`` otherwise. - Example: false, true + + ### NOTES + - Yes, managable is misspelled. It is spelled this way in the + controller response. """ return self._get("managable") @property def mode(self): """ - - Return the ``mode`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``mode`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. - - ``mode`` is converted from Titlecase to lowercase. + + ### Returns + - The ``mode`` value of the filtered switch, if it exists. + - ``None`` otherwise. - Example: maintenance, migration, normal, inconsistent """ mode = self._get("mode") @@ -399,20 +453,32 @@ def mode(self): @property def model(self): """ - - Return the ``model`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``model`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``model`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("model") @property def oper_status(self): """ - - Return the ``operStatus`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``operStatus`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``operStatus`` value of the filtered switch, if it exists. + - ``None`` otherwise. - Example: Minor """ return self._get("operStatus") @@ -420,11 +486,18 @@ def oper_status(self): @property def platform(self): """ - - Return the ``platform`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``platform`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + ### Returns + - The ``platform`` value of the filtered switch, if it exists. + - ``None`` otherwise. + - Example: N9K (derived from N9K-C93180YC-EX) + ### NOTES - ``platform`` is derived from ``model``. It is not in the controller response. @@ -437,178 +510,177 @@ def platform(self): @property def release(self): """ - - Return the ``release`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``release`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``release`` value of the filtered switch, if it exists. + - ``None`` otherwise. - Example: 10.2(5) """ return self._get("release") @property - def rest_send(self): + def role(self): """ ### Summary - An instance of the RestSend class. + The ``switchRole`` value of the filtered switch. ### Raises - - setter: ``TypeError`` if the value is not an instance of RestSend. - - ### getter - Return an instance of the RestSend class. + - ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. - ### setter - Set an instance of the RestSend class. + ### Returns + - The ``switchRole`` value of the filtered switch, if it exists. + - ``None`` otherwise. + - Example: spine """ - return self.properties["rest_send"] - - @rest_send.setter - def rest_send(self, value): - method_name = inspect.stack()[0][3] - _class_have = None - _class_need = "RestSend" - msg = f"{self.class_name}.{method_name}: " - msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." - try: - _class_have = value.class_name - except AttributeError as error: - msg += f"Error detail: {error}." - raise TypeError(msg) from error - if _class_have != _class_need: - raise TypeError(msg) - self.properties["rest_send"] = value + return self._get("switchRole") @property - def results(self): + def serial_number(self): """ ### Summary - An instance of the Results class. + The ``serialNumber`` value of the filtered switch. ### Raises - - setter: ``TypeError`` if the value is not an instance of Results. - - ### getter - Return an instance of the Results class. - - ### setter - Set an instance of the Results class. - """ - return self.properties["results"] - - @results.setter - def results(self, value): - method_name = inspect.stack()[0][3] - _class_have = None - _class_need = "Results" - msg = f"{self.class_name}.{method_name}: " - msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." - try: - _class_have = value.class_name - except AttributeError as error: - msg += f" Error detail: {error}." - raise TypeError(msg) from error - if _class_have != _class_need: - raise TypeError(msg) - self.properties["results"] = value - - @property - def role(self): - """ - - Return the ``switchRole`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. - """ - return self._get("switchRole") - @property - def serial_number(self): - """ - - Return the ``serialNumber`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter - and ``_get`` method. + ### Returns + - The ``serialNumber`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("serialNumber") @property def source_interface(self): """ - - Return the ``sourceInterface`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``sourceInterface`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``sourceInterface`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("sourceInterface") @property def source_vrf(self): """ - - Return the ``sourceVrf`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``sourceVrf`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``sourceVrf`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("sourceVrf") @property def status(self): """ - - Return the ``status`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``status`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``status`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("status") @property def switch_db_id(self): """ - - Return the ``switchDbID`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``switchDbID`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``switchDbID`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("switchDbID") @property def switch_role(self): """ - - Return the ``switchRole`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``switchRole`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``switchRole`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("switchRole") @property def switch_uuid(self): """ - - Return the ``swUUID`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``swUUID`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``swUUID`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("swUUID") @property def switch_uuid_id(self): """ - - Return the ``swUUIDId`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``swUUIDId`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``swUUIDId`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("swUUIDId") @property def system_mode(self): """ - - Return the ``systemMode`` of the filtered switch, if it exists. - - Return ``None`` otherwise - - Raises ``ValueError`` (potentially). See ``filter`` setter + ### Summary + The ``systemMode`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter and ``_get`` method. + + ### Returns + - The ``systemMode`` value of the filtered switch, if it exists. + - ``None`` otherwise. """ return self._get("systemMode") From 4fdfe761500f91a1d5ebe9d449bc8585462dab56 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 4 Jun 2024 09:32:17 -1000 Subject: [PATCH 119/230] FabricDetails() v2: inject rest_send and results with class decorators FabricDetails(): Remove rest_send and results and inject from Properties() class. FabricDetails(): Update class docstrings for consistency. Properties(): Fix missing space in rest_send error message. --- plugins/module_utils/common/properties.py | 2 +- .../module_utils/fabric/fabric_details_v2.py | 191 +++++++----------- .../dcnm_fabric/test_fabric_details_v2.py | 58 ++++-- 3 files changed, 107 insertions(+), 144 deletions(-) diff --git a/plugins/module_utils/common/properties.py b/plugins/module_utils/common/properties.py index acb1dd31e..1ae51c292 100644 --- a/plugins/module_utils/common/properties.py +++ b/plugins/module_utils/common/properties.py @@ -66,7 +66,7 @@ def rest_send(self, value): try: _class_have = value.class_name except AttributeError as error: - msg += f"Error detail: {error}." + msg += f" Error detail: {error}." raise TypeError(msg) from error if _class_have != _class_need: raise TypeError(msg) diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index fd26f74fa..e047c0ac2 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -26,8 +26,12 @@ EpFabrics from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties +@Properties.add_rest_send +@Properties.add_results class FabricDetails: """ ### Summary @@ -73,12 +77,6 @@ def __init__(self, params): self.conversion = ConversionUtils() self.ep_fabrics = EpFabrics() - self._init_properties() - - def _init_properties(self): - self.properties = {} - self.properties["rest_send"] = None - self.properties["results"] = None def register_result(self): """ @@ -326,74 +324,6 @@ def replication_mode(self): self.log.debug(msg) return None - @property - def rest_send(self): - """ - ### Summary - An instance of the RestSend class. - - ### Raises - - setter: ``TypeError`` if the value is not an instance of RestSend. - - ### getter - Return an instance of the RestSend class. - - ### setter - Set an instance of the RestSend class. - """ - return self.properties["rest_send"] - - @rest_send.setter - def rest_send(self, value): - method_name = inspect.stack()[0][3] - _class_have = None - _class_need = "RestSend" - msg = f"{self.class_name}.{method_name}: " - msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." - try: - _class_have = value.class_name - except AttributeError as error: - msg += f"Error detail: {error}." - raise TypeError(msg) from error - if _class_have != _class_need: - raise TypeError(msg) - self.properties["rest_send"] = value - - @property - def results(self): - """ - ### Summary - An instance of the Results class. - - ### Raises - - setter: ``TypeError`` if the value is not an instance of Results. - - ### getter - Return an instance of the Results class. - - ### setter - Set an instance of the Results class. - """ - return self.properties["results"] - - @results.setter - def results(self, value): - method_name = inspect.stack()[0][3] - _class_have = None - _class_need = "Results" - msg = f"{self.class_name}.{method_name}: " - msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." - try: - _class_have = value.class_name - except AttributeError as error: - msg += f" Error detail: {error}." - raise TypeError(msg) from error - if _class_have != _class_need: - raise TypeError(msg) - self.properties["results"] = value - @property def template_name(self): """ @@ -423,7 +353,8 @@ class FabricDetailsByName(FabricDetails): Usage (where params is AnsibleModule.params): ```python - sender = Sender() # class that implements the sender interface + params = {"check_mode": False, "state": "merged"} + sender = Sender() # class implementing the sender interface sender.ansible_module = ansible_module rest_send = RestSend() @@ -476,18 +407,7 @@ def __init__(self, params): self.log.debug(msg) self.data_subclass = {} - self.build_properties() - - def build_properties(self): - """ - ### Summary - Build the properties dictionary for the class. - The dictionary has already been initialized in the parent class. - - ### Raises - None - """ - self.properties["filter"] = None + self._filter = None def refresh(self): """ @@ -596,31 +516,48 @@ def filtered_data(self): @property def filter(self): """ + ### Summary Set the fabric_name of the fabric to query. - This needs to be set before accessing this class's properties. + ### Raises + None + + ### NOTES + ``filter`` must be set before accessing this class's properties. """ - return self.properties.get("filter") + return self._filter @filter.setter def filter(self, value): - self.properties["filter"] = value + self._filter = value class FabricDetailsByNvPair(FabricDetails): """ - Retrieve fabric details from the controller filtered - by nvPair key and value. This sets the filtered_data - property to a dictionary of all fabrics on the controller - that match filter_key and filter_value. + ### Summary + Retrieve fabric details from the controller filtered by nvPair key + and value. Calling ``refresh`` retrieves data for all fabrics. + After having called ``refresh`` data for a fabric accessed by setting + ``filter_key`` and ``filter_value`` which sets the ``filtered_data`` + property to a dictionary containing fabrics on the controller + that match ``filter_key`` and ``filter_value``. + + ### Usage + ```python + params = {"check_mode": False, "state": "query"} + sender = Sender() # class implementing the sender interface + sender.ansible_module = ansible_module - Usage (where params is AnsibleModule.params): + rest_send = RestSend() + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() instance = FabricDetailsNvPair(params) instance.refresh() instance.filter_key = "DCI_SUBNET_RANGE" instance.filter_value = "10.33.0.0/16" fabrics = instance.filtered_data + ``` """ def __init__(self, params): @@ -633,26 +570,19 @@ def __init__(self, params): self.log.debug(msg) self.data_subclass = {} + self._filter_key = None + self._filter_value = None - self.build_properties() - - def build_properties(self): - """ - ### Summary - Build the properties dictionary for the class. - The dictionary has already been initialized in the parent class. - - ### Raises - None - """ - self.properties["filter_key"] = None - self.properties["filter_value"] = None def refresh(self): """ + ### Summary Refresh fabric_name current details from the controller. - - raise ValueError if self.filter_key has not been set. + ### Raises + - ``ValueError`` if: + - ``filter_key`` has not been set. + - ``filter_value`` has not been set. """ method_name = inspect.stack()[0][3] @@ -675,39 +605,54 @@ def refresh(self): @property def filtered_data(self): """ - - Return a ``dict`` of the fabric(s) matching ``self.filter_key`` - and ``self.filter_value``. - - Return an empty ``dict`` if the fabric does not exist on - the controller. + ### Summary + A dictionary of the fabric(s) matching ``filter_key`` and + ``filter_value``. + + ### Raises + None + + ### Returns + - A ``dict`` of the fabric(s) matching ``filter_key`` and + ``filter_value``. + - An empty ``dict`` if the fabric does not exist on the controller. """ return self.data_subclass @property def filter_key(self): """ - - getter: Return the nvPairs key to filter on. - - setter: Set the nvPairs key to filter on. + ### Summary + The nvPairs key on which to filter. - This should be an exact match for the key in the nvPairs + ### Raises + None + + ### Notes + ``filter_key``should be an exact match for the key in the nvPairs dictionary for the fabric. """ - return self.properties.get("filter_key") + return self._filter_key @filter_key.setter def filter_key(self, value): - self.properties["filter_key"] = value + self._filter_key = value @property def filter_value(self): """ - - getter: Return the nvPairs value to filter on. - - setter: Set the nvPairs value to filter on. + ### Summary + The nvPairs value on which to filter. - This should be an exact match for the value in the nvPairs + ### Raises + None + + ### Notes + ``filter_value`` should be an exact match for the value in the nvPairs dictionary for the fabric. """ - return self.properties.get("filter_value") + return self._filter_value @filter_value.setter def filter_value(self, value): - self.properties["filter_value"] = value + self._filter_value = value diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py index 871aed2bf..710278c11 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py @@ -54,8 +54,6 @@ def test_fabric_details_v2_00000(fabric_details_v2) -> None: """ ### Classes and Methods - - FabricCommon - - __init__() - FabricDetails - __init__() @@ -74,8 +72,6 @@ def test_fabric_details_v2_00000(fabric_details_v2) -> None: def test_fabric_details_v2_00010() -> None: """ ### Classes and Methods - - FabricCommon - - __init__() - FabricDetails - __init__() @@ -91,8 +87,6 @@ def test_fabric_details_v2_00010() -> None: def test_fabric_details_v2_00020() -> None: """ ### Classes and Methods - - FabricCommon - - __init__() - FabricDetails - __init__() @@ -102,16 +96,14 @@ def test_fabric_details_v2_00020() -> None: match = r"FabricDetails\.__init__:\s+" match += r"state is missing from params\. params:.*\." with pytest.raises(ValueError, match=match): - instance = FabricDetails( + instance = FabricDetails( # pylint: disable=unused-variable {"check_mode": False} - ) # pylint: disable=unused-variable + ) def test_fabric_details_v2_00100(fabric_details_v2) -> None: """ ### Classes and Methods - - FabricCommon() - - __init__() - FabricDetails() - __init__() - refresh_super() @@ -182,8 +174,6 @@ def responses(): def test_fabric_details_v2_00110(monkeypatch, fabric_details_v2) -> None: """ ### Classes and Methods - - FabricCommon() - - __init__() - FabricDetails() - __init__() - refresh_super() @@ -242,8 +232,6 @@ def responses(): def test_fabric_details_v2_00120(monkeypatch, fabric_details_v2) -> None: """ ### Classes and Methods - - FabricCommon() - - __init__() - FabricDetails() - __init__() - refresh_super() @@ -317,8 +305,6 @@ def responses(): def test_fabric_details_v2_00200(fabric_details_v2) -> None: """ ### Classes and Methods - - FabricCommon() - - __init__() - FabricDetails() - __init__() - _get() @@ -335,8 +321,6 @@ def test_fabric_details_v2_00200(fabric_details_v2) -> None: def test_fabric_details_v2_00300(fabric_details_v2) -> None: """ ### Classes and Methods - - FabricCommon() - - __init__() - FabricDetails() - __init__() - _get_nv_pair() @@ -353,8 +337,6 @@ def test_fabric_details_v2_00300(fabric_details_v2) -> None: def test_fabric_details_v2_00400(fabric_details_v2) -> None: """ ### Classes and Methods - - FabricCommon() - - __init__() - FabricDetails() - __init__() - all_data() @@ -366,3 +348,39 @@ def test_fabric_details_v2_00400(fabric_details_v2) -> None: instance = fabric_details_v2 instance.data = {"foo": "bar"} assert instance.all_data == {"foo": "bar"} + + +MATCH_00500 = r"FabricDetails\.rest_send:\s+" +MATCH_00500 += r"value must be an instance of RestSend\.\s+" +MATCH_00500 += r"Got value.*of type.*\.\s+" +MATCH_00500 += r"Error detail:.*\." + + +@pytest.mark.parametrize( + "param, does_raise, expected", + [ + (None, True, pytest.raises(TypeError, match=MATCH_00500)), + (1, True, pytest.raises(TypeError, match=MATCH_00500)), + ("foo", True, pytest.raises(TypeError, match=MATCH_00500)), + ({"foo": "bar"}, True, pytest.raises(TypeError, match=MATCH_00500)), + (RestSend({"state": "merged", "check_mode": False}), False, does_not_raise()), + ], +) +def test_fabric_details_v2_00500( + fabric_details_v2, param, does_raise, expected +) -> None: + """ + ### Classes and Methods + - FabricDetails() + - __init__() + - rest_send.setter + + ### Summary + - Verify FabricDetails().rest_send raises ``TypeError`` when + passed a value other than a RestSend() instance. + """ + with expected: + instance = fabric_details_v2 + instance.rest_send = param + if does_raise is False: + assert instance.rest_send == param From 3d06c6312d8084b843db1c6c6fa32fcb55828865 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 4 Jun 2024 09:40:22 -1000 Subject: [PATCH 120/230] FabricDetails() v2: Fix PEP8 too many blank lines Also, add the following to elide no-member error reporting: # pylint: disable=no-member --- plugins/module_utils/fabric/fabric_details_v2.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index e047c0ac2..f6333dbab 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -18,6 +18,9 @@ __metaclass__ = type __author__ = "Allen Robel" +# Required for class decorators +# pylint: disable=no-member + import copy import inspect import logging @@ -77,7 +80,6 @@ def __init__(self, params): self.conversion = ConversionUtils() self.ep_fabrics = EpFabrics() - def register_result(self): """ ### Summary @@ -573,7 +575,6 @@ def __init__(self, params): self._filter_key = None self._filter_value = None - def refresh(self): """ ### Summary From 6879cd42347eceeecbc53afcaa9481892fa37a07 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 4 Jun 2024 10:30:55 -1000 Subject: [PATCH 121/230] MaintenanceModeInfo(): Move to maintenance_mode_info.py For cases where we just need to query the maintenance mode state, we can save some imports by moving MaintenanceModeInfo() to a separate file. Moving classes into separate files also helps with editing tasks like search/replace and lessens the likelihood of editing the wrong class. --- .../module_utils/common/maintenance_mode.py | 557 ----------------- .../common/maintenance_mode_info.py | 588 ++++++++++++++++++ plugins/modules/dcnm_maintenance_mode.py | 6 +- 3 files changed, 592 insertions(+), 559 deletions(-) create mode 100644 plugins/module_utils/common/maintenance_mode_info.py diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 85e9ff4bc..839c57530 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -31,10 +31,6 @@ ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ Properties -from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ - SwitchDetails -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ - FabricDetailsByName @Properties.add_rest_send @@ -590,556 +586,3 @@ def config(self, value): except (TypeError, ValueError) as error: raise ValueError(error) from error self._config = value - - -@Properties.add_rest_send -@Properties.add_results -class MaintenanceModeInfo: - """ - ### Summary - - Retrieve the maintenance mode state of switches. - - ### Raises - - ``TypeError`` in the following public properties: - - ``config`` if value is not a list. - - ``rest_send`` if value is not an instance of RestSend. - - ``results`` if value is not an instance of Results. - - - ``ValueError`` in the following public methods: - - ``refresh()`` if: - - ``config`` has not been set. - - ``rest_send`` has not been set. - - ``results`` has not been set. - - ### Details - Updates ``MaintenanceModeInfo().results`` to reflect success/failure of - the operation on the controller. - - Example value for ``config`` in the ``Usage`` section below: - ```json - ["192.168.1.2", "192.168.1.3"] - ``` - - Example value for ``info`` in the ``Usage`` section below: - ```json - { - "192.169.1.2": { - deployment_disabled: true - fabric_freeze_mode: true, - fabric_name: "MyFabric", - fabric_read_only: true - mode: "maintenance", - role: "spine", - serial_number: "FCI1234567" - }, - "192.169.1.3": { - deployment_disabled: false, - fabric_freeze_mode: false, - fabric_name: "YourFabric", - fabric_read_only: false - mode: "normal", - role: "leaf", - serial_number: "FCH2345678" - } - } - ``` - - ### Usage - - Where: - - ``params`` is ``AnsibleModule.params`` - - ``config`` is per the above example. - - ``sender`` is an instance of a Sender() class. - See ``dcnm_sender.py`` for usage. - - ```python - ansible_module = AnsibleModule() - # - params = AnsibleModule.params - instance = MaintenanceModeInfo(params) - - sender = Sender() - sender.ansible_module = ansible_module - rest_send = RestSend() - rest_send.sender = sender - try: - instance.config = config - instance.rest_send = rest_send - instance.results = Results() - instance.refresh() - except (TypeError, ValueError) as error: - handle_error(error) - deployment_disabled = instance.deployment_disabled - fabric_freeze_mode = instance.fabric_freeze_mode - fabric_name = instance.fabric_name - fabric_read_only = instance.fabric_read_only - info = instance.info - mode = instance.mode - role = instance.role - serial_number = instance.serial_number - ``` - """ - - def __init__(self, params): - self.class_name = self.__class__.__name__ - - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.action = "maintenance_mode_info" - - self.params = params - self.conversions = ConversionUtils() - self.fabric_details = FabricDetailsByName(self.params) - self.switch_details = SwitchDetails() - - self._config = None - self._info = None - self._rest_send = None - self._results = None - - msg = "ENTERED MaintenanceModeInfo(): " - self.log.debug(msg) - - def verify_refresh_parameters(self) -> None: - """ - ### Summary - Verify that required parameters are present before - calling ``refresh()``. - - ### Raises - - ``ValueError`` if: - - ``config`` is not set. - - ``rest_send`` is not set. - - ``results`` is not set. - """ - method_name = inspect.stack()[0][3] - if self.config is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.config must be set " - msg += "before calling refresh." - raise ValueError(msg) - if self.rest_send is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.rest_send must be set " - msg += "before calling refresh." - raise ValueError(msg) - if self.results is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.results must be set " - msg += "before calling refresh." - raise ValueError(msg) - - def refresh(self): - """ - ### Summary - Build ``self.info``, a dict containing the current maintenance mode - status of all switches in self.config. - - ### Raises - - ``ValueError`` if: - - ``SwitchDetails()`` raises ``ControllerResponseError`` - - ``SwitchDetails()`` raises ``ValueError`` - - ``FabricDetails()`` raises ``ControllerResponseError`` - - switch with ``ip_address`` does not exist on the controller. - - ### self.info structure - info is a dict, keyed on switch_ip, where each element is a dict - with the following structure: - - ``fabric_name``: The name of the switch's hosting fabric. - - ``freeze_mode``: The current state of the switch's hosting fabric. - If freeze_mode is True, configuration changes cannot be made to the - fabric or the switches within the fabric. - - ``mode``: The current maintenance mode of the switch. - - ``role``: The role of the switch in the hosting fabric. - - ``serial_number``: The serial number of the switch. - - ```json - { - "192.169.1.2": { - fabric_deployment_disabled: true - fabric_freeze_mode: true, - fabric_name: "MyFabric", - fabric_read_only: true - mode: "maintenance", - role: "spine", - serial_number: "FCI1234567" - }, - "192.169.1.3": { - fabric_deployment_disabled: false, - fabric_freeze_mode: false, - fabric_name: "YourFabric", - fabric_read_only: false - mode: "normal", - role: "leaf", - serial_number: "FCH2345678" - } - } - ``` - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - - self.verify_refresh_parameters() - - try: - self.switch_details.rest_send = self.rest_send - self.fabric_details.rest_send = self.rest_send - - self.switch_details.results = self.results - self.fabric_details.results = self.results - except TypeError as error: - raise ValueError(error) from error - - try: - self.switch_details.refresh() - except (ControllerResponseError, ValueError) as error: - raise ValueError(error) from error - - try: - self.fabric_details.refresh() - except (ControllerResponseError, ValueError) as error: - raise ValueError(error) from error - - info = {} - # Populate info dict - for ip_address in self.config: - self.switch_details.filter = ip_address - - try: - serial_number = self.switch_details.serial_number - except ValueError as error: - raise ValueError(error) from error - - if serial_number is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"Switch with ip_address {ip_address} " - msg += "does not exist on the controller." - raise ValueError(msg) - - fabric_name = self.switch_details.fabric_name - freeze_mode = self.switch_details.freeze_mode - mode = self.switch_details.maintenance_mode - role = self.switch_details.switch_role - - try: - self.fabric_details.filter = fabric_name - except ValueError as error: - raise ValueError(error) from error - fabric_read_only = self.fabric_details.is_read_only - - info[ip_address] = {} - info[ip_address].update({"fabric_name": fabric_name}) - if freeze_mode is True: - info[ip_address].update({"fabric_freeze_mode": True}) - else: - info[ip_address].update({"fabric_freeze_mode": False}) - if fabric_read_only is True: - info[ip_address].update({"fabric_read_only": True}) - else: - info[ip_address].update({"fabric_read_only": False}) - if freeze_mode is True or fabric_read_only is True: - info[ip_address].update({"fabric_deployment_disabled": True}) - else: - info[ip_address].update({"fabric_deployment_disabled": False}) - info[ip_address].update({"mode": mode}) - if role is not None: - info[ip_address].update({"role": role}) - else: - info[ip_address].update({"role": "na"}) - info[ip_address].update({"serial_number": serial_number}) - self.info = copy.deepcopy(info) - - def _get(self, item): - """ - Return the value of the item from the filtered switch. - - ### Raises - - ``ValueError`` if ``filter`` is not set. - - ``ValueError`` if ``filter`` is not in the controller response. - - ``ValueError`` if item is not in the filtered switch dict. - """ - method_name = inspect.stack()[0][3] - - if self.filter is None: - msg = f"{self.class_name}.{method_name}: " - msg += "set instance.filter before accessing " - msg += f"property {item}." - raise ValueError(msg) - - if self.filter not in self._info: - msg = f"{self.class_name}.{method_name}: " - msg += f"Switch with ip_address {self.filter} does not exist on " - msg += "the controller." - raise ValueError(msg) - - if item not in self._info[self.filter]: - msg = f"{self.class_name}.{method_name}: " - msg += f"{self.filter} does not have a key named {item}." - raise ValueError(msg) - - return self.conversions.make_boolean( - self.conversions.make_none(self._info[self.filter].get(item)) - ) - - @property - def filter(self): - """ - ### Summary - Set the query filter. - - ### Raises - None. However, if ``filter`` is not set, or ``filter`` is set to - an ip_address for a switch that does not exist on the controller, - ``ValueError`` will be raised when accessing the various getter - properties. - - ### Details - The filter should be the ip_address of the switch from which to - retrieve details. - - ``filter`` must be set before accessing this class's properties. - """ - return self._filter - - @filter.setter - def filter(self, value): - self._filter = value - - @property - def config(self) -> list: - """ - ### Summary - A list of switch ip addresses for which maintenance mode state - will be retrieved. - - ### Raises - - setter: ``TypeError`` if: - - ``config`` is not a ``list``. - - Elements of ``config`` are not ``str``. - - ### getter - Return ``config``. - - ### setter - Set ``config``. - - ### Value structure - value is a ``list`` of ip addresses - - ### Example - ```json - ["172.22.150.2", "172.22.150.3"] - ``` - """ - return self._config - - @config.setter - def config(self, value): - method_name = inspect.stack()[0][3] - if not isinstance(value, list): - msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.config must be a list. " - msg += f"Got type: {type(value).__name__}." - raise TypeError(msg) - - for item in value: - if not isinstance(item, str): - msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.config must be a list of strings " - msg += "containing ip addresses. " - msg += f"Got type: {type(item).__name__}." - raise TypeError(msg) - self._config = value - - @property - def fabric_deployment_disabled(self): - """ - ### Summary - The current ``fabric_deployment_disabled`` state of the - filtered switch's hosting fabric. - - ### Raises - - ``ValueError`` if: - - ``filter`` is not set. - - ``filter`` is not in the controller response. - - ``deployment_disabled`` is not in the filtered switch dict. - - ### Valid values - - ``True``: The fabric is in a state where configuration changes - cannot be made. - - ``False``: The fabric is in a state where configuration changes - can be made. - """ - return self._get("fabric_deployment_disabled") - - @property - def fabric_freeze_mode(self): - """ - ### Summary - The freezeMode state of the fabric in which the - filtered switch resides. - - ### Raises - - ``ValueError`` if: - - ``filter`` is not set. - - ``filter`` is not in the controller response. - - ``fabric_name`` is not in the filtered switch dict. - - ### Valid values - - ``True``: The fabric is in a state where configuration changes - cannot be made. - - ``False``: The fabric is in a state where configuration changes - can be made. - """ - return self._get("fabric_freeze_mode") - - @property - def fabric_name(self): - """ - ### Summary - The name of the fabric in which the - filtered switch resides. - - ### Raises - - ``ValueError`` if: - - ``filter`` is not set. - - ``filter`` is not in the controller response. - - ``fabric_name`` is not in the filtered switch dict. - """ - return self._get("fabric_name") - - @property - def fabric_read_only(self): - """ - ### Summary - The read-only state of the fabric in which the - filtered switch resides. - - ### Raises - - ``ValueError`` if: - - ``filter`` is not set. - - ``filter`` is not in the controller response. - - ``fabric_name`` is not in the filtered switch dict. - - ### Valid values - - ``True``: The fabric is in a state where configuration changes - cannot be made. - - ``False``: The fabric is in a state where configuration changes - can be made. - """ - return self._get("fabric_freeze_mode") - - @property - def info(self) -> dict: - """ - ### Summary - Return or set the current maintenance mode state of the switches - represented by the ip_addresses in self.config. - - ### Raises - - ``ValueError`` if: - - ``refresh()`` has not been called before accessing ``info``. - - ### getter - Return ``info``. - - ### setter - Set ``info``. - - ### ``info`` structure - ``info`` is a dict, keyed on switch_ip, where each element is a dict - with the following structure: - - ``fabric_deployment_disabled``: The current state of the switch's - hosting fabric. If fabric_deployment_disabled is True, - configuration changes cannot be made to the fabric or the switches - within the fabric. - - ``fabric_name``: The name of the switch's hosting fabric. - - ``fabric_freeze_mode``: The current state of the switch's - hosting fabric. If freeze_mode is True, configuration changes - cannot be made to the fabric or the switches within the fabric. - - ``fabric_read_only``: The current state of the switch's - hosting fabric. If fabric_read_only is True, configuration changes - cannot be made to the fabric or the switches within the fabric. - - ``mode``: The current maintenance mode of the switch. - - ``role``: The role of the switch in the hosting fabric. - - ``serial_number``: The serial number of the switch. - - ### Example info dict - ```json - { - "192.169.1.2": { - fabric_deployment_disabled: true - fabric_freeze_mode: true, - fabric_name: "MyFabric", - fabric_read_only: true - mode: "maintenance", - role: "spine", - serial_number: "FCI1234567" - }, - "192.169.1.3": { - fabric_deployment_disabled: false - fabric_freeze_mode: false, - fabric_name: "YourFabric", - fabric_read_only: false - mode: "normal", - role: "leaf", - serial_number: "FCH2345678" - } - } - ``` - """ - method_name = inspect.stack()[0][3] - if self._info is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.refresh() must be called before " - msg += f"accessing {self.class_name}.{method_name}." - raise ValueError(msg) - return copy.deepcopy(self._info) - - @info.setter - def info(self, value: dict): - if not isinstance(value, dict): - msg = f"{self.class_name}.info.setter: " - msg += "value must be a dict. " - msg += f"Got value {value} of type {type(value).__name__}." - raise TypeError(msg) - self._info = value - - @property - def mode(self): - """ - ### Summary - The current maintenance mode of the filtered switch. - - ### Raises - - ``ValueError`` if: - - ``filter`` is not set. - - ``filter`` is not in the controller response. - - ``mode`` is not in the filtered switch dict. - """ - return self._get("mode") - - @property - def role(self): - """ - ### Summary - The role of the filtered switch in the hosting fabric. - - ### Raises - - ``ValueError`` if: - - ``filter`` is not set. - - ``filter`` is not in the controller response. - - ``role`` is not in the filtered switch dict. - """ - return self._get("role") - - @property - def serial_number(self): - """ - ### Summary - The serial number of the filtered switch. - - ### Raises - - ``ValueError`` if: - - ``filter`` is not set. - - ``filter`` is not in the controller response. - - ``serial_number`` is not in the filtered switch dict. - """ - return self._get("serial_number") diff --git a/plugins/module_utils/common/maintenance_mode_info.py b/plugins/module_utils/common/maintenance_mode_info.py new file mode 100644 index 000000000..3d489b9bd --- /dev/null +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -0,0 +1,588 @@ +# 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" + +# Required for class decorators +# pylint: disable=no-member + +import copy +import inspect +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties +from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ + SwitchDetails +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ + FabricDetailsByName + + +@Properties.add_rest_send +@Properties.add_results +class MaintenanceModeInfo: + """ + ### Summary + - Retrieve the maintenance mode state of switches. + + ### Raises + - ``TypeError`` in the following public properties: + - ``config`` if value is not a list. + - ``rest_send`` if value is not an instance of RestSend. + - ``results`` if value is not an instance of Results. + + - ``ValueError`` in the following public methods: + - ``refresh()`` if: + - ``config`` has not been set. + - ``rest_send`` has not been set. + - ``results`` has not been set. + + ### Details + Updates ``MaintenanceModeInfo().results`` to reflect success/failure of + the operation on the controller. + + Example value for ``config`` in the ``Usage`` section below: + ```json + ["192.168.1.2", "192.168.1.3"] + ``` + + Example value for ``info`` in the ``Usage`` section below: + ```json + { + "192.169.1.2": { + deployment_disabled: true + fabric_freeze_mode: true, + fabric_name: "MyFabric", + fabric_read_only: true + mode: "maintenance", + role: "spine", + serial_number: "FCI1234567" + }, + "192.169.1.3": { + deployment_disabled: false, + fabric_freeze_mode: false, + fabric_name: "YourFabric", + fabric_read_only: false + mode: "normal", + role: "leaf", + serial_number: "FCH2345678" + } + } + ``` + + ### Usage + - Where: + - ``params`` is ``AnsibleModule.params`` + - ``config`` is per the above example. + - ``sender`` is an instance of a Sender() class. + See ``dcnm_sender.py`` for usage. + + ```python + ansible_module = AnsibleModule() + # + params = AnsibleModule.params + instance = MaintenanceModeInfo(params) + + sender = Sender() + sender.ansible_module = ansible_module + rest_send = RestSend() + rest_send.sender = sender + try: + instance.config = config + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + except (TypeError, ValueError) as error: + handle_error(error) + deployment_disabled = instance.deployment_disabled + fabric_freeze_mode = instance.fabric_freeze_mode + fabric_name = instance.fabric_name + fabric_read_only = instance.fabric_read_only + info = instance.info + mode = instance.mode + role = instance.role + serial_number = instance.serial_number + ``` + """ + + def __init__(self, params): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.action = "maintenance_mode_info" + + self.params = params + self.conversions = ConversionUtils() + self.fabric_details = FabricDetailsByName(self.params) + self.switch_details = SwitchDetails() + + self._config = None + self._info = None + self._rest_send = None + self._results = None + + msg = "ENTERED MaintenanceModeInfo(): " + self.log.debug(msg) + + def verify_refresh_parameters(self) -> None: + """ + ### Summary + Verify that required parameters are present before + calling ``refresh()``. + + ### Raises + - ``ValueError`` if: + - ``config`` is not set. + - ``rest_send`` is not set. + - ``results`` is not set. + """ + method_name = inspect.stack()[0][3] + if self.config is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.config must be set " + msg += "before calling refresh." + raise ValueError(msg) + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.rest_send must be set " + msg += "before calling refresh." + raise ValueError(msg) + if self.results is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.results must be set " + msg += "before calling refresh." + raise ValueError(msg) + + def refresh(self): + """ + ### Summary + Build ``self.info``, a dict containing the current maintenance mode + status of all switches in self.config. + + ### Raises + - ``ValueError`` if: + - ``SwitchDetails()`` raises ``ControllerResponseError`` + - ``SwitchDetails()`` raises ``ValueError`` + - ``FabricDetails()`` raises ``ControllerResponseError`` + - switch with ``ip_address`` does not exist on the controller. + + ### self.info structure + info is a dict, keyed on switch_ip, where each element is a dict + with the following structure: + - ``fabric_name``: The name of the switch's hosting fabric. + - ``freeze_mode``: The current state of the switch's hosting fabric. + If freeze_mode is True, configuration changes cannot be made to the + fabric or the switches within the fabric. + - ``mode``: The current maintenance mode of the switch. + - ``role``: The role of the switch in the hosting fabric. + - ``serial_number``: The serial number of the switch. + + ```json + { + "192.169.1.2": { + fabric_deployment_disabled: true + fabric_freeze_mode: true, + fabric_name: "MyFabric", + fabric_read_only: true + mode: "maintenance", + role: "spine", + serial_number: "FCI1234567" + }, + "192.169.1.3": { + fabric_deployment_disabled: false, + fabric_freeze_mode: false, + fabric_name: "YourFabric", + fabric_read_only: false + mode: "normal", + role: "leaf", + serial_number: "FCH2345678" + } + } + ``` + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + self.verify_refresh_parameters() + + try: + self.switch_details.rest_send = self.rest_send + self.fabric_details.rest_send = self.rest_send + + self.switch_details.results = self.results + self.fabric_details.results = self.results + except TypeError as error: + raise ValueError(error) from error + + try: + self.switch_details.refresh() + except (ControllerResponseError, ValueError) as error: + raise ValueError(error) from error + + try: + self.fabric_details.refresh() + except (ControllerResponseError, ValueError) as error: + raise ValueError(error) from error + + info = {} + # Populate info dict + for ip_address in self.config: + self.switch_details.filter = ip_address + + try: + serial_number = self.switch_details.serial_number + except ValueError as error: + raise ValueError(error) from error + + if serial_number is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"Switch with ip_address {ip_address} " + msg += "does not exist on the controller." + raise ValueError(msg) + + fabric_name = self.switch_details.fabric_name + freeze_mode = self.switch_details.freeze_mode + mode = self.switch_details.maintenance_mode + role = self.switch_details.switch_role + + try: + self.fabric_details.filter = fabric_name + except ValueError as error: + raise ValueError(error) from error + fabric_read_only = self.fabric_details.is_read_only + + info[ip_address] = {} + info[ip_address].update({"fabric_name": fabric_name}) + if freeze_mode is True: + info[ip_address].update({"fabric_freeze_mode": True}) + else: + info[ip_address].update({"fabric_freeze_mode": False}) + if fabric_read_only is True: + info[ip_address].update({"fabric_read_only": True}) + else: + info[ip_address].update({"fabric_read_only": False}) + if freeze_mode is True or fabric_read_only is True: + info[ip_address].update({"fabric_deployment_disabled": True}) + else: + info[ip_address].update({"fabric_deployment_disabled": False}) + info[ip_address].update({"mode": mode}) + if role is not None: + info[ip_address].update({"role": role}) + else: + info[ip_address].update({"role": "na"}) + info[ip_address].update({"serial_number": serial_number}) + self.info = copy.deepcopy(info) + + def _get(self, item): + """ + Return the value of the item from the filtered switch. + + ### Raises + - ``ValueError`` if ``filter`` is not set. + - ``ValueError`` if ``filter`` is not in the controller response. + - ``ValueError`` if item is not in the filtered switch dict. + """ + method_name = inspect.stack()[0][3] + + if self.filter is None: + msg = f"{self.class_name}.{method_name}: " + msg += "set instance.filter before accessing " + msg += f"property {item}." + raise ValueError(msg) + + if self.filter not in self._info: + msg = f"{self.class_name}.{method_name}: " + msg += f"Switch with ip_address {self.filter} does not exist on " + msg += "the controller." + raise ValueError(msg) + + if item not in self._info[self.filter]: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.filter} does not have a key named {item}." + raise ValueError(msg) + + return self.conversions.make_boolean( + self.conversions.make_none(self._info[self.filter].get(item)) + ) + + @property + def filter(self): + """ + ### Summary + Set the query filter. + + ### Raises + None. However, if ``filter`` is not set, or ``filter`` is set to + an ip_address for a switch that does not exist on the controller, + ``ValueError`` will be raised when accessing the various getter + properties. + + ### Details + The filter should be the ip_address of the switch from which to + retrieve details. + + ``filter`` must be set before accessing this class's properties. + """ + return self._filter + + @filter.setter + def filter(self, value): + self._filter = value + + @property + def config(self) -> list: + """ + ### Summary + A list of switch ip addresses for which maintenance mode state + will be retrieved. + + ### Raises + - setter: ``TypeError`` if: + - ``config`` is not a ``list``. + - Elements of ``config`` are not ``str``. + + ### getter + Return ``config``. + + ### setter + Set ``config``. + + ### Value structure + value is a ``list`` of ip addresses + + ### Example + ```json + ["172.22.150.2", "172.22.150.3"] + ``` + """ + return self._config + + @config.setter + def config(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, list): + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.config must be a list. " + msg += f"Got type: {type(value).__name__}." + raise TypeError(msg) + + for item in value: + if not isinstance(item, str): + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.config must be a list of strings " + msg += "containing ip addresses. " + msg += f"Got type: {type(item).__name__}." + raise TypeError(msg) + self._config = value + + @property + def fabric_deployment_disabled(self): + """ + ### Summary + The current ``fabric_deployment_disabled`` state of the + filtered switch's hosting fabric. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``deployment_disabled`` is not in the filtered switch dict. + + ### Valid values + - ``True``: The fabric is in a state where configuration changes + cannot be made. + - ``False``: The fabric is in a state where configuration changes + can be made. + """ + return self._get("fabric_deployment_disabled") + + @property + def fabric_freeze_mode(self): + """ + ### Summary + The freezeMode state of the fabric in which the + filtered switch resides. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``fabric_name`` is not in the filtered switch dict. + + ### Valid values + - ``True``: The fabric is in a state where configuration changes + cannot be made. + - ``False``: The fabric is in a state where configuration changes + can be made. + """ + return self._get("fabric_freeze_mode") + + @property + def fabric_name(self): + """ + ### Summary + The name of the fabric in which the + filtered switch resides. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``fabric_name`` is not in the filtered switch dict. + """ + return self._get("fabric_name") + + @property + def fabric_read_only(self): + """ + ### Summary + The read-only state of the fabric in which the + filtered switch resides. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``fabric_name`` is not in the filtered switch dict. + + ### Valid values + - ``True``: The fabric is in a state where configuration changes + cannot be made. + - ``False``: The fabric is in a state where configuration changes + can be made. + """ + return self._get("fabric_freeze_mode") + + @property + def info(self) -> dict: + """ + ### Summary + Return or set the current maintenance mode state of the switches + represented by the ip_addresses in self.config. + + ### Raises + - ``ValueError`` if: + - ``refresh()`` has not been called before accessing ``info``. + + ### getter + Return ``info``. + + ### setter + Set ``info``. + + ### ``info`` structure + ``info`` is a dict, keyed on switch_ip, where each element is a dict + with the following structure: + - ``fabric_deployment_disabled``: The current state of the switch's + hosting fabric. If fabric_deployment_disabled is True, + configuration changes cannot be made to the fabric or the switches + within the fabric. + - ``fabric_name``: The name of the switch's hosting fabric. + - ``fabric_freeze_mode``: The current state of the switch's + hosting fabric. If freeze_mode is True, configuration changes + cannot be made to the fabric or the switches within the fabric. + - ``fabric_read_only``: The current state of the switch's + hosting fabric. If fabric_read_only is True, configuration changes + cannot be made to the fabric or the switches within the fabric. + - ``mode``: The current maintenance mode of the switch. + - ``role``: The role of the switch in the hosting fabric. + - ``serial_number``: The serial number of the switch. + + ### Example info dict + ```json + { + "192.169.1.2": { + fabric_deployment_disabled: true + fabric_freeze_mode: true, + fabric_name: "MyFabric", + fabric_read_only: true + mode: "maintenance", + role: "spine", + serial_number: "FCI1234567" + }, + "192.169.1.3": { + fabric_deployment_disabled: false + fabric_freeze_mode: false, + fabric_name: "YourFabric", + fabric_read_only: false + mode: "normal", + role: "leaf", + serial_number: "FCH2345678" + } + } + ``` + """ + method_name = inspect.stack()[0][3] + if self._info is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"{self.class_name}.refresh() must be called before " + msg += f"accessing {self.class_name}.{method_name}." + raise ValueError(msg) + return copy.deepcopy(self._info) + + @info.setter + def info(self, value: dict): + if not isinstance(value, dict): + msg = f"{self.class_name}.info.setter: " + msg += "value must be a dict. " + msg += f"Got value {value} of type {type(value).__name__}." + raise TypeError(msg) + self._info = value + + @property + def mode(self): + """ + ### Summary + The current maintenance mode of the filtered switch. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``mode`` is not in the filtered switch dict. + """ + return self._get("mode") + + @property + def role(self): + """ + ### Summary + The role of the filtered switch in the hosting fabric. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``role`` is not in the filtered switch dict. + """ + return self._get("role") + + @property + def serial_number(self): + """ + ### Summary + The serial number of the filtered switch. + + ### Raises + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + - ``serial_number`` is not in the filtered switch dict. + """ + return self._get("serial_number") diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 416a441ee..21bb01b40 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -131,8 +131,10 @@ Sender from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import \ Log -from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import ( - MaintenanceMode, MaintenanceModeInfo) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import \ + MaintenanceMode +from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode_info import \ + MaintenanceModeInfo from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults_v2 import \ From 4d668c0d7625d5c40321b104a26597eab4c2fe18 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 4 Jun 2024 11:31:18 -1000 Subject: [PATCH 122/230] Common(): inject rest_send with class decorator dcnm_maintenance_mode.py: 1. Common(): inject rest_send property from Properties(). 2. main(): isolate AnsibleModule to the top of the function. 3. Merged().commit(): raise ValueError if rest_send is not set. 4. Query().commit(): raise ValueError if rest_send is not set. --- plugins/modules/dcnm_maintenance_mode.py | 107 +++++++++++------------ 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 21bb01b40..6402aee04 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -141,6 +141,8 @@ ParamsMergeDefaults from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate_v2 import \ ParamsValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ ResponseHandler from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ @@ -698,6 +700,7 @@ def validator(self, value) -> None: self._properties["validator"] = value +@Properties.add_rest_send class Common: """ Common methods, properties, and resources for all states. @@ -706,53 +709,49 @@ class Common: def __init__(self, params): """ ### Raises - - ``ValueError`` if params does not contain ``check_mode`` - - ``ValueError`` if params does not contain ``state`` + - ``ValueError`` if: + - ``params`` does not contain ``check_mode`` + - ``params`` does not contain ``state`` """ self.class_name = self.__class__.__name__ + method_name = inspect.stack()[0][3] + self.params = params self.log = logging.getLogger(f"dcnm.{self.class_name}") - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.check_mode = self.params.get("check_mode", None) if self.check_mode is None: - msg = f"{self.class_name}.__init__(): " + msg = f"{self.class_name}.{method_name}: " msg += "check_mode is required" raise ValueError(msg) self.state = self.params.get("state", None) if self.state is None: - msg = f"{self.class_name}.__init__(): " + msg = f"{self.class_name}.{method_name}: " msg += "state is required" raise ValueError(msg) - self._init_properties() - - self.results = Results() - self.results.state = self.state - self.results.check_mode = self.check_mode - - msg = f"ENTERED Common().{method_name}: " - msg += f"state: {self.state}, " - msg += f"check_mode: {self.check_mode}" - self.log.debug(msg) - - # populated in self.validate_input() - self.payloads = {} - self.config = self.params.get("config") if not isinstance(self.config, dict): + msg = f"{self.class_name}.{method_name}: " msg = "expected dict type for self.config. " msg += f"got {type(self.config).__name__}" raise ValueError(msg) + self.results = Results() + self.results.state = self.state + self.results.check_mode = self.check_mode + self.have = {} + # populated in self.validate_input() + self.payloads = {} self.query = [] self.want = [] - def _init_properties(self): - self._properties = {} - self._properties["ansible_module"] = None + msg = f"ENTERED Common().{method_name}: " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) def get_want(self) -> None: """ @@ -774,24 +773,6 @@ def get_want(self) -> None: except (TypeError, ValueError) as error: raise ValueError(error) from error - @property - def rest_send(self): - """ - getter: return an instance of RestSend - setter: set an instance of RestSend - """ - return self._properties["rest_send"] - - @rest_send.setter - def rest_send(self, value): - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - if not isinstance(value, RestSend): - msg = f"{self.class_name}.{method_name}: " - msg += "expected RestSend instance. " - msg += f"got {type(value).__name__}." - raise ValueError(msg) - self._properties["rest_send"] = value - class Merged(Common): """ @@ -1027,19 +1008,25 @@ def commit(self): ### Raises - ``ValueError`` if: + - ``rest_send`` is not set. - ``get_want()`` raises ``ValueError`` - ``get_have()`` raises ``ValueError`` - ``send_need()`` raises ``ValueError`` """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] msg = f"{self.class_name}.{method_name}: entered" self.log.debug(msg) + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += "rest_send must be set before calling commit." + raise ValueError(msg) + try: self.get_want() except ValueError as error: raise ValueError(error) from error - # Return if there's nothing to do + if len(self.want) == 0: return @@ -1188,14 +1175,25 @@ def commit(self) -> None: and update ``self.results`` with the query results. ### Raises - - ``ValueError`` if get_want() raises ``ValueError`` - - ``ValueError`` if get_have() raises ``ValueError`` + - ``ValueError`` if: + - ``rest_send`` is not set. + - ``get_want()`` raises ``ValueError`` + - ``get_have()`` raises ``ValueError`` """ + method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: entered" + self.log.debug(msg) + + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += "rest_send must be set before calling commit." + raise ValueError(msg) + try: self.get_want() except ValueError as error: raise ValueError(error) from error - # Return if there's nothing to do + if len(self.want) == 0: return @@ -1235,6 +1233,9 @@ def main(): ansible_module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True ) + params = copy.deepcopy(ansible_module.params) + params["check_mode"] = ansible_module.check_mode + # Logging setup try: log = Log() @@ -1242,26 +1243,24 @@ def main(): except ValueError as error: ansible_module.fail_json(str(error)) - ansible_module.params["check_mode"] = ansible_module.check_mode - sender = Sender() sender.ansible_module = ansible_module - rest_send = RestSend(ansible_module.params) + rest_send = RestSend(params) rest_send.response_handler = ResponseHandler() rest_send.sender = sender - if ansible_module.params["state"] == "merged": + if params["state"] == "merged": try: - task = Merged(ansible_module.params) - task.rest_send = rest_send + task = Merged(params) + task.rest_send = rest_send # pylint: disable=attribute-defined-outside-init task.commit() except ValueError as error: ansible_module.fail_json(f"{error}", **task.results.failed_result) - elif ansible_module.params["state"] == "query": + elif params["state"] == "query": try: - task = Query(ansible_module.params) - task.rest_send = rest_send + task = Query(params) + task.rest_send = rest_send # pylint: disable=attribute-defined-outside-init task.commit() except ValueError as error: ansible_module.fail_json(f"{error}", **task.results.failed_result) From 410a56b306f3836a13a457467d3d3b90f3903795 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 4 Jun 2024 14:33:11 -1000 Subject: [PATCH 123/230] MaintenanceMode: 94% unit test coverage 1. test_maintenance_mode_00220: - Modify to test for deploy == False and deploy == True in addition to mode tests. - Now tests the following cases: - mode == maintenance, deploy == False - mode == maintenance, deploy == True - mode == normal, deploy == False - mode == normal, deploy == True 2. test_maintenance_mode_00800: - Verify MaintenanceMode().change_system_mode() raises ``ValueError`` when ``MaintenanceMode().results()`` raises any of: - ``TypeError`` - ``ValueError`` 3. dcnm_maintenance_mode.py: use params in error message rather than ansible_module.params --- plugins/modules/dcnm_maintenance_mode.py | 2 +- .../common/test_maintenance_mode.py | 91 ++++++++++++++++++- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 6402aee04..6fd236817 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -1268,7 +1268,7 @@ def main(): else: # We should never get here since the state parameter has # already been validated. - msg = f"Unknown state {ansible_module.params['state']}" + msg = f"Unknown state {params['state']}" ansible_module.fail_json(msg) task.results.build_final_result() diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 90987611c..ef614ea81 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -342,13 +342,15 @@ def mock_deploy_switches(*args, **kwargs): @pytest.mark.parametrize( - "mode", + "mode, deploy", [ - ("maintenance"), - ("normal"), + ("maintenance", True), + ("maintenance", False), + ("normal", True), + ("normal", False), ], ) -def test_maintenance_mode_00220(maintenance_mode, mode) -> None: +def test_maintenance_mode_00220(maintenance_mode, mode, deploy) -> None: """ Classes and Methods - MaintenanceMode() @@ -391,6 +393,7 @@ def responses(): config = copy.deepcopy(CONFIG[0]) config["mode"] = mode + config["deploy"] = deploy with does_not_raise(): rest_send = RestSend({"state": "merged", "check_mode": False}) @@ -869,3 +872,83 @@ def serial_number(self, value): monkeypatch.setattr(instance, endpoint_instance, MockEndpoint()) with pytest.raises(expected_exception, match=mock_message): instance.commit() + + +@pytest.mark.parametrize( + "mock_exception, expected_exception, mock_message", + [ + (TypeError, ValueError, r"Converted TypeError to ValueError"), + (ValueError, ValueError, r"Converted ValueError to ValueError"), + ], +) +def test_maintenance_mode_00800( + maintenance_mode, mock_exception, expected_exception, mock_message +) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - change_system_mode() + + + Summary + - Verify MaintenanceMode().change_system_mode() raises ``ValueError`` + when ``MaintenanceMode().results()`` raises any of: + - ``TypeError`` + - ``ValueError`` + + + Code Flow - Setup + - MaintenanceMode() is instantiated + - Required attributes are set + - Results().response_current.setter is mocked to raise each of the above + exceptions + + Code Flow - Test + - MaintenanceMode().commit() is called for each exception + + Expected Result + - ``ValueError`` is raised + - Exception message matches expected + """ + class MockResults: + """ + Mock the Results class + """ + class_name = "Results" + + def register_task_result(self, *args): + """ + do nothing + """ + + @property + def response_current(self): + """ + mock response_current getter + """ + return {"success": True} + + @response_current.setter + def response_current(self, *args): + raise mock_exception(mock_message) + + def responses(): + yield {"RETURN_CODE": 200, "MESSAGE": "OK", "DATA": {"status": "Success"}} + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + + with does_not_raise(): + rest_send = RestSend({"state": "merged", "check_mode": False}) + rest_send.sender = mock_sender + rest_send.response_handler = ResponseHandler() + instance = maintenance_mode + instance.rest_send = rest_send + instance.rest_send.unit_test = True + instance.rest_send.timeout = 1 + instance.config = CONFIG + instance.results = MockResults() + + with pytest.raises(expected_exception, match=mock_message): + instance.commit() From 9fd40cdb2eb7663f9e77e7fe7b4d8b82a7a563ec Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 4 Jun 2024 15:01:51 -1000 Subject: [PATCH 124/230] FabricDetails() v2: Hardening. FabricDetails().__init__() can potentially raise ValueError. Hence, need to catch this exception, per below. - FabricDetailsByName(): Wrap super()..__init__() in try-except block. - FabricDetailsByNvPair(): Wrap super()..__init__() in try-except block. --- plugins/module_utils/fabric/fabric_details_v2.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index f6333dbab..a13f35029 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -400,8 +400,14 @@ class FabricDetailsByName(FabricDetails): """ def __init__(self, params): - super().__init__(params) self.class_name = self.__class__.__name__ + try: + super().__init__(params) + except ValueError as error: + msg = "FabricDetailsByName.__init__: " + msg += "Failed in super().__init__(). " + msg += f"Error detail: {error}" + raise ValueError(msg) from error self.log = logging.getLogger(f"dcnm.{self.class_name}") msg = "ENTERED FabricDetailsByName() " @@ -563,8 +569,14 @@ class FabricDetailsByNvPair(FabricDetails): """ def __init__(self, params): - super().__init__(params) self.class_name = self.__class__.__name__ + try: + super().__init__(params) + except ValueError as error: + msg = "FabricDetailsByNvPair.__init__: " + msg += "Failed in super().__init__(). " + msg += f"Error detail: {error}" + raise ValueError(msg) from error self.log = logging.getLogger(f"dcnm.{self.class_name}") From 73a331f1fea9741459202b8cf38f5c8d8ee839ca Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 4 Jun 2024 15:11:12 -1000 Subject: [PATCH 125/230] FabricDetails() v2: More hardening. FabricDetails().refresh_super() can potentially raise ValueError. Hence, need to catch this exception, per below. - FabricDetailsByName().refresh(): Wrap self.super_refresh() in try-except block. - FabricDetailsByNvPair(): Wrap self.super_refresh() in try-except block. --- plugins/module_utils/fabric/fabric_details_v2.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index a13f35029..e52cfc89d 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -425,7 +425,13 @@ def refresh(self): - ``ValueError`` if: - Mandatory properties are not set. """ - self.refresh_super() + try: + self.refresh_super() + except ValueError as error: + msg = "Failed to refresh fabric details: " + msg += f"Error detail: {error}." + raise ValueError(msg) from error + self.data_subclass = copy.deepcopy(self.data) def _get(self, item): @@ -610,7 +616,13 @@ def refresh(self): msg += f"before calling {self.class_name}.refresh()." raise ValueError(msg) - self.refresh_super() + try: + self.refresh_super() + except ValueError as error: + msg = "Failed to refresh fabric details: " + msg += f"Error detail: {error}." + raise ValueError(msg) from error + for item, value in self.data.items(): if value.get("nvPairs", {}).get(self.filter_key) == self.filter_value: self.data_subclass[item] = value From 15c315fe508f74f275eaea34b4bcf04b04a63cb7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 5 Jun 2024 08:09:00 -1000 Subject: [PATCH 126/230] MaintenanceMode(): 100% unit test coverage MaintenanceMode().__init__(): instantiate EpFabricConfigDeploy() so it becomes a testable attribute for test case test_maintenance_mode_00800. MaintenanceMode().deploy_switch(): Add a period (.) to the end of ControllerResponseError message to improve the corresponding unit test regex to cover the whole message. Renamed test case: test_maintenance_mode_00800 -> test_maintenance_mode_00900 Added the following test cases: test_maintenance_mode_00800: - Verify MaintenanceMode().deploy_switches() raises ``ValueError`` when ``EpFabricConfigDeploy`` raises any of: - ``TypeError`` - ``ValueError`` test_maintenance_mode_01000: - Verify MaintenanceMode().commit() raises ``ValueError`` when ``MaintenanceMode().deploy_switches()`` raises ``ControllerResponseError`` when the RETURN_CODE in the response is not 200. --- .../module_utils/common/maintenance_mode.py | 5 +- .../common/test_maintenance_mode.py | 172 +++++++++++++++++- 2 files changed, 170 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 839c57530..48ce18abd 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -140,6 +140,7 @@ def __init__(self, params): self.conversion = ConversionUtils() self.ep_maintenance_mode_enable = EpMaintenanceModeEnable() self.ep_maintenance_mode_disable = EpMaintenanceModeDisable() + self.ep_fabric_config_deploy = EpFabricConfigDeploy() self._config = None self._rest_send = None @@ -481,7 +482,7 @@ def deploy_switches(self) -> None: method_name = inspect.stack()[0][3] self.build_deploy_dict() self.build_serial_number_to_ip_address() - endpoint = EpFabricConfigDeploy() + endpoint = self.ep_fabric_config_deploy for fabric_name, serial_numbers in self.deploy_dict.items(): # Build endpoint try: @@ -527,7 +528,7 @@ def deploy_switches(self) -> None: msg += f"fabric_name {fabric_name}, " msg += "serial_numbers " msg += f"{','.join(serial_numbers)}. " - msg += f"Got response {self.results.response_current}" + msg += f"Got response {self.results.response_current}." raise ControllerResponseError(msg) @property diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index ef614ea81..9b42976cd 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -815,18 +815,20 @@ def test_maintenance_mode_00700( - ``TypeError`` - ``ValueError`` - Code Flow - Setup - MaintenanceMode() is instantiated - Required attributes are set - - EpMaintenanceModeEnable() is mocked to raise each of the above exceptions + - EpMaintenanceModeEnable() is mocked to raise each + of the above exceptions + - EpMaintenanceModeDisable() is mocked to raise each + of the above exceptions Code Flow - Test - MaintenanceMode().commit() is called for each exception Expected Result - - ``ValueError`` is raised - - Exception message matches expected + - ``ValueError`` is raised. + - Exception message matches expected. """ class MockEndpoint: @@ -874,6 +876,103 @@ def serial_number(self, value): instance.commit() +@pytest.mark.parametrize( + "endpoint_instance, mock_exception, expected_exception, mock_message", + [ + ("ep_fabric_config_deploy", TypeError, ValueError, "Bad type"), + ("ep_fabric_config_deploy", ValueError, ValueError, "Bad value"), + ], +) +def test_maintenance_mode_00800( + monkeypatch, + maintenance_mode, + endpoint_instance, + mock_exception, + expected_exception, + mock_message, +) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - commit() + + Summary + - Verify MaintenanceMode().deploy_switches() raises ``ValueError`` + when ``EpFabricConfigDeploy`` raises any of: + - ``TypeError`` + - ``ValueError`` + + + Code Flow - Setup + - MaintenanceMode() is instantiated + - Required attributes are set + - EpFabricConfigDeploy() is mocked to raise each of the above exceptions + + Code Flow - Test + - MaintenanceMode().commit() is called for each exception + + Expected Result + - ``TypeError`` and ``ValueError`` are raised. + - Exception message matches expected. + """ + + class MockEndpoint: + """ + Mock EpFabricConfigDeploy() class + """ + + def __init__(self): + self._fabric_name = None + self._switch_id = None + + @property + def fabric_name(self): + """ + Mock fabric_name getter/setter + """ + return self._fabric_name + + @fabric_name.setter + def fabric_name(self, value): + raise mock_exception(mock_message) + + @property + def switch_id(self): + """ + Mock switch_id getter/setter + """ + return self._switch_id + + @switch_id.setter + def switch_id(self, value): + self._switch_id = value + + def responses(): + yield {"MESSAGE": "OK", "RETURN_CODE": 200, "DATA": {"status": "Success"}} + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "merged", "check_mode": False}) + rest_send.sender = mock_sender + rest_send.response_handler = ResponseHandler() + rest_send.unit_test = True + rest_send.timeout = 1 + + config = copy.deepcopy(CONFIG[0]) + config["deploy"] = True + + with does_not_raise(): + instance = maintenance_mode + instance.config = [config] + instance.rest_send = rest_send + instance.results = Results() + + monkeypatch.setattr(instance, endpoint_instance, MockEndpoint()) + with pytest.raises(expected_exception, match=mock_message): + instance.commit() + + @pytest.mark.parametrize( "mock_exception, expected_exception, mock_message", [ @@ -881,7 +980,7 @@ def serial_number(self, value): (ValueError, ValueError, r"Converted ValueError to ValueError"), ], ) -def test_maintenance_mode_00800( +def test_maintenance_mode_00900( maintenance_mode, mock_exception, expected_exception, mock_message ) -> None: """ @@ -911,10 +1010,12 @@ def test_maintenance_mode_00800( - ``ValueError`` is raised - Exception message matches expected """ + class MockResults: """ Mock the Results class """ + class_name = "Results" def register_task_result(self, *args): @@ -952,3 +1053,64 @@ def responses(): with pytest.raises(expected_exception, match=mock_message): instance.commit() + + +def test_maintenance_mode_01000(monkeypatch, maintenance_mode) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - commit() + + Summary + - Verify MaintenanceMode().commit() raises ``ValueError`` when + ``MaintenanceMode().deploy_switches()`` raises + ``ControllerResponseError`` when the RETURN_CODE in the + response is not 200. + + Code Flow - Setup + - MaintenanceMode() is instantiated + - Required attributes are set + + Code Flow - Test + - MaintenanceMode().commit() is called with simulated responses: + - 200 response for ``change_system_mode()`` + - 500 response ``deploy_switches()`` + + Expected Result + - ``ValueError``is raised. + - Exception message matches expected. + """ + + def responses(): + yield {"MESSAGE": "OK", "RETURN_CODE": 200, "DATA": {"status": "Success"}} + yield { + "MESSAGE": "Internal server error", + "RETURN_CODE": 500, + "DATA": {"status": "Success"}, + } + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "merged", "check_mode": False}) + rest_send.sender = mock_sender + rest_send.response_handler = ResponseHandler() + rest_send.unit_test = True + rest_send.timeout = 1 + + config = copy.deepcopy(CONFIG[0]) + config["deploy"] = True + + with does_not_raise(): + instance = maintenance_mode + instance.config = [config] + instance.rest_send = rest_send + instance.results = Results() + + match = r"MaintenanceMode\.deploy_switches:\s+" + match += r"Unable to deploy switches:\s+" + match += r"fabric_name VXLAN_Fabric,\s+" + match += r"serial_numbers FDO22180ASJ\.\s+" + match += r"Got response.*\." + with pytest.raises(ValueError, match=match): + instance.commit() From 199ff6649f98d73bb2daf3ac132f40eb4a889d4a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 5 Jun 2024 11:13:23 -1000 Subject: [PATCH 127/230] MaintenanceModeInfo: 49% unit test coverage 1. test_maintenance_mode_info.py: initial unit tests. 2. test_mainteance_mode.py: organize asserts alphabetically. 3. common_utils.py: Add fixtures and responses for MaintenanceModeInfo() 4. SwitchDetails: self.conversions should be self.conversion. --- .../common/maintenance_mode_info.py | 10 +- plugins/module_utils/common/switch_details.py | 6 +- .../unit/module_utils/common/common_utils.py | 30 + .../fixtures/responses_SwitchDetails.json | 125 ++ .../common/test_maintenance_mode.py | 14 +- .../common/test_maintenance_mode_info.py | 1123 +++++++++++++++++ 6 files changed, 1296 insertions(+), 12 deletions(-) create mode 100644 tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json create mode 100644 tests/unit/module_utils/common/test_maintenance_mode_info.py diff --git a/plugins/module_utils/common/maintenance_mode_info.py b/plugins/module_utils/common/maintenance_mode_info.py index 3d489b9bd..7db9be3e1 100644 --- a/plugins/module_utils/common/maintenance_mode_info.py +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -129,7 +129,7 @@ def __init__(self, params): self.action = "maintenance_mode_info" self.params = params - self.conversions = ConversionUtils() + self.conversion = ConversionUtils() self.fabric_details = FabricDetailsByName(self.params) self.switch_details = SwitchDetails() @@ -222,10 +222,14 @@ def refresh(self): self.verify_refresh_parameters() try: + self.log.debug("ZZZ: set self.switch_details.rest_send") self.switch_details.rest_send = self.rest_send + self.log.debug("ZZZ: set self.fabric_details.rest_send") self.fabric_details.rest_send = self.rest_send + self.log.debug("ZZZ: set self.switch_details.results") self.switch_details.results = self.results + self.log.debug("ZZZ: set self.fabric_details.results") self.fabric_details.results = self.results except TypeError as error: raise ValueError(error) from error @@ -317,8 +321,8 @@ def _get(self, item): msg += f"{self.filter} does not have a key named {item}." raise ValueError(msg) - return self.conversions.make_boolean( - self.conversions.make_none(self._info[self.filter].get(item)) + return self.conversion.make_boolean( + self.conversion.make_none(self._info[self.filter].get(item)) ) @property diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index ffdb88e8f..0c4eb0922 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -73,7 +73,7 @@ def __init__(self): self.log.debug("ENTERED common.SwitchDetails()") self.action = "switch_details" - self.conversions = ConversionUtils() + self.conversion = ConversionUtils() self.ep_all_switches = EpAllSwitches() self.path = self.ep_all_switches.path self.verb = self.ep_all_switches.verb @@ -229,8 +229,8 @@ def _get(self, item): msg += f"{self.filter} does not have a key named {item}." raise ValueError(msg) - return self.conversions.make_boolean( - self.conversions.make_none(self._info[self.filter].get(item)) + return self.conversion.make_boolean( + self.conversion.make_none(self._info[self.filter].get(item)) ) @property diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index 2c25dfc09..5a3df0cef 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -30,6 +30,8 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import \ MaintenanceMode +from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode_info import \ + MaintenanceModeInfo from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ @@ -234,6 +236,14 @@ def maintenance_mode_fixture(): return MaintenanceMode(params) +@pytest.fixture(name="maintenance_mode_info") +def maintenance_mode_info_fixture(): + """ + return MaintenanceModeInfo + """ + return MaintenanceModeInfo(params) + + @pytest.fixture(name="merge_dicts") def merge_dicts_fixture(): """ @@ -331,3 +341,23 @@ def responses_maintenance_mode(key: str) -> Dict[str, str]: response = load_fixture(response_file).get(key) print(f"responses_maintenance_mode: {key} : {response}") return response + + +def responses_maintenance_mode_info(key: str) -> Dict[str, str]: + """ + Return data in responses_MaintenanceModeInfo.json + """ + response_file = "responses_MaintenanceModeInfo" + response = load_fixture(response_file).get(key) + print(f"responses_maintenance_mode_info: {key} : {response}") + return response + + +def responses_switch_details(key: str) -> Dict[str, str]: + """ + Return data in responses_SwitchDetails.json + """ + response_file = "responses_SwitchDetails" + response = load_fixture(response_file).get(key) + print(f"responses_switch_details: {key} : {response}") + return response diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json new file mode 100644 index 000000000..1368b4d30 --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -0,0 +1,125 @@ +{ + "test_maintenance_mode_info_00200a": { + "DATA": [ + { + "activeSupSlot": 0, + "availPorts": 0, + "ccStatus": "NA", + "cfsSyslogStatus": 1, + "colDBId": 0, + "connUnitStatus": 0, + "consistencyState": false, + "contact": null, + "cpuUsage": 0, + "deviceType": "External", + "displayHdrs": null, + "displayValues": null, + "domain": null, + "domainID": 0, + "elementType": null, + "fabricId": 3, + "fabricName": "FOO", + "fabricTechnology": "LANClassic", + "fcoeEnabled": false, + "fex": false, + "fexMap": {}, + "fid": 0, + "freezeMode": null, + "health": -1, + "hostName": "cvd-1314-leaf", + "index": 0, + "intentedpeerName": "", + "interfaces": null, + "ipAddress": "172.22.150.105", + "ipDomain": "", + "isEchSupport": false, + "isLan": false, + "isNonNexus": false, + "isPmCollect": false, + "isTrapDelayed": false, + "isVpcConfigured": false, + "is_smlic_enabled": false, + "keepAliveState": null, + "lastScanTime": 0, + "licenseDetail": null, + "licenseViolation": false, + "linkName": null, + "location": null, + "logicalName": "cvd-1314-leaf", + "managable": true, + "mds": false, + "membership": null, + "memoryUsage": 0, + "mgmtAddress": null, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "moduleIndexOffset": 9999, + "modules": null, + "monitorMode": true, + "name": null, + "network": null, + "nonMdsModel": null, + "npvEnabled": false, + "numberOfPorts": 0, + "operMode": null, + "operStatus": "Minor", + "peer": null, + "peerSerialNumber": null, + "peerSwitchDbId": 0, + "peerlinkState": null, + "ports": 0, + "present": true, + "primaryIP": "", + "primarySwitchDbID": 0, + "principal": null, + "protoDiscSettings": null, + "recvIntf": null, + "release": "10.2(5)", + "role": null, + "sanAnalyticsCapable": false, + "scope": null, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "sendIntf": null, + "serialNumber": "FDO211218FV", + "sourceInterface": "mgmt0", + "sourceVrf": "management", + "standbySupState": 0, + "status": "ok", + "swType": null, + "swUUID": "DCNM-UUID-132770", + "swUUIDId": 132770, + "swWwn": null, + "swWwnName": null, + "switchDbID": 502030, + "switchRole": "leaf", + "switchRoleEnum": "Leaf", + "sysDescr": "", + "systemMode": "Normal", + "uid": 0, + "unmanagableCause": "", + "upTime": 0, + "upTimeNumber": 0, + "upTimeStr": "98 days, 21:55:52", + "usedPorts": 0, + "username": null, + "vdcId": 0, + "vdcMac": null, + "vdcName": "", + "vendor": "Cisco", + "version": null, + "vpcDomain": 0, + "vrf": "management", + "vsanWwn": null, + "vsanWwnName": null, + "waitForSwitchModeChg": false, + "wwn": null + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + } +} \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 9b42976cd..82eca7188 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -75,20 +75,22 @@ def test_maintenance_mode_00000(maintenance_mode) -> None: """ with does_not_raise(): instance = maintenance_mode + + assert instance._config is None assert instance._rest_send is None assert instance._results is None - assert instance._config is None + assert instance.action == "maintenance_mode" + assert instance.check_mode is False assert instance.class_name == "MaintenanceMode" assert instance.config is None - assert instance.check_mode is False assert instance.deploy_dict == {} - assert instance.serial_number_to_ip_address == {} - assert instance.valid_modes == ["maintenance", "normal"] - assert instance.state == "merged" - assert instance.config is None assert instance.rest_send is None assert instance.results is None + assert instance.serial_number_to_ip_address == {} + assert instance.state == "merged" + assert instance.valid_modes == ["maintenance", "normal"] + assert isinstance(instance.conversion, ConversionUtils) assert isinstance(instance.ep_maintenance_mode_disable, EpMaintenanceModeDisable) assert isinstance(instance.ep_maintenance_mode_enable, EpMaintenanceModeEnable) diff --git a/tests/unit/module_utils/common/test_maintenance_mode_info.py b/tests/unit/module_utils/common/test_maintenance_mode_info.py new file mode 100644 index 000000000..96609c48f --- /dev/null +++ b/tests/unit/module_utils/common/test_maintenance_mode_info.py @@ -0,0 +1,1123 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-import +# pylint: disable=redefined-outer-name +# pylint: disable=protected-access +# pylint: disable=unused-argument +# pylint: disable=invalid-name + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import inspect + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError +from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode_info import \ + MaintenanceModeInfo +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results +from ansible_collections.cisco.dcnm.tests.unit.mocks.mock_fabric_details_by_name import \ + MockFabricDetailsByName +from ansible_collections.cisco.dcnm.tests.unit.mocks.mock_switch_details import \ + MockSwitchDetails +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + MockSender, ResponseGenerator, does_not_raise, + maintenance_mode_info_fixture, responses_switch_details) + +FABRIC_NAME = "VXLAN_Fabric" +CONFIG = ["192.168.1.2"] +PARAMS = {"state": "query", "check_mode": False} + + +def test_maintenance_mode_info_00000(maintenance_mode_info) -> None: + """ + Classes and Methods + - MaintenanceModeInfo + - __init__() + + Test + - Class attributes are initialized to expected values + - Exception is not raised + """ + with does_not_raise(): + instance = maintenance_mode_info + assert instance._config is None + assert instance._info is None + assert instance._rest_send is None + assert instance._results is None + + assert instance.action == "maintenance_mode_info" + assert instance.class_name == "MaintenanceModeInfo" + assert instance.config is None + assert instance.rest_send is None + assert instance.results is None + + assert isinstance(instance.conversion, ConversionUtils) + + +def test_maintenance_mode_info_00100(maintenance_mode_info) -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - verify_refresh_parameters() + - refresh() + + ### Summary + - Verify MaintenanceModeInfo().refresh() raises ``ValueError`` when + ``config`` is not set. + + ### Code Flow - Setup + - MaintenanceModeInfo() is instantiated. + - Other required attributes are set. + + ### Code Flow - Test + - ``MaintenanceModeInfo().refresh()`` is called without having first set + ``MaintenanceModeInfo().config``. + + ### Expected Result + - ``ValueError`` is raised. + - Exception message matches expected. + """ + with does_not_raise(): + instance = maintenance_mode_info + instance.rest_send = RestSend({}) + instance.results = Results() + + match = r"MaintenanceModeInfo\.verify_refresh_parameters: " + match += r"MaintenanceModeInfo\.config must be set before calling\s+" + match += r"refresh\." + with pytest.raises(ValueError, match=match): + instance.refresh() + + +def test_maintenance_mode_info_00110(maintenance_mode_info) -> None: + """ + ### Classes and Methods + - ``MaintenanceModeInfo()`` + - __init__() + - verify_refresh_parameters() + - refresh() + + ### Summary + - Verify ``refresh()`` raises ``ValueError`` when ``rest_send`` + is not set. + + ### Code Flow - Setup + - ``MaintenanceModeInfo()`` is instantiated + - Other required attributes are set + + Code Flow - Test + - ``refresh()`` is called without having first set ``rest_send``. + + ### Expected Result + - ``ValueError`` is raised. + - Exception message matches expected. + """ + with does_not_raise(): + instance = maintenance_mode_info + instance.results = Results() + instance.config = CONFIG + + match = r"MaintenanceModeInfo\.verify_refresh_parameters: " + match += r"MaintenanceModeInfo\.rest_send must be set before calling\s+" + match += r"refresh\." + with pytest.raises(ValueError, match=match): + instance.refresh() + + +def test_maintenance_mode_info_00120(maintenance_mode_info) -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - verify_refresh_parameters() + - refresh() + + ### Summary + - Verify ``refresh()`` raises ``ValueError`` when ``results`` is not set. + + ### Code Flow - Setup + - ``MaintenanceModeInfo()`` is instantiated. + - Other required attributes are set. + + ### Code Flow - Test + - ``refresh()`` is called without having first set ``results``. + + ### Expected Result + - ``ValueError`` is raised. + - Exception message matches expected. + """ + with does_not_raise(): + instance = maintenance_mode_info + instance.rest_send = RestSend({}) + instance.config = CONFIG + + match = r"MaintenanceModeInfo\.verify_refresh_parameters: " + match += r"MaintenanceModeInfo\.results must be set before calling\s+" + match += r"refresh\." + with pytest.raises(ValueError, match=match): + instance.refresh() + + +@pytest.mark.parametrize( + "mock_class, mock_property, mock_exception, expected_exception, mock_message", + [ + ( + "FabricDetailsByName", + "results", + TypeError, + ValueError, + "Bad type: fabric_details.results", + ), + ( + "FabricDetailsByName", + "rest_send", + TypeError, + ValueError, + "Bad type: fabric_details.rest_send", + ), + ( + "SwitchDetails", + "results", + TypeError, + ValueError, + "Bad type: switch_details.results", + ), + ( + "SwitchDetails", + "rest_send", + TypeError, + ValueError, + "Bad type: switch_details.rest_send", + ), + ], +) +def test_maintenance_mode_info_00200( + monkeypatch, + mock_class, + mock_property, + mock_exception, + expected_exception, + mock_message, +) -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + + ### Summary + - Verify ``refresh()`` raises ``ValueError`` when: + - ``fabric_details`` properties ``rest_send`` and ``results`` + raise ``TypeError``. + - ``switch_details`` properties ``rest_send`` and ``results`` + raise ``TypeError``. + + ### Code Flow - Setup + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + - ``FabricDetails()`` is mocked to conditionally raise ``TypeError``. + - ``SwitchDetails()`` is mocked to conditionally raise ``TypeError``. + + ### Code Flow - Test + - MaintenanceModeInfo().refresh() is called for each condition. + + ### Expected Result + - ``ValueError`` is raised. + - Exception message matches expected. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = mock_sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + mock_fabric_details = MockFabricDetailsByName() + mock_fabric_details.mock_class = mock_class + mock_fabric_details.mock_exception = mock_exception + mock_fabric_details.mock_message = mock_message + mock_fabric_details.mock_property = mock_property + + mock_switch_details = MockSwitchDetails() + mock_switch_details.mock_class = mock_class + mock_switch_details.mock_exception = mock_exception + mock_switch_details.mock_message = mock_message + mock_switch_details.mock_property = mock_property + + monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) + monkeypatch.setattr(instance, "switch_details", mock_switch_details) + + with does_not_raise(): + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + + with pytest.raises(expected_exception, match=mock_message): + instance.refresh() + + +# @pytest.mark.parametrize( +# "mock_exception, expected_exception, mock_message", +# [ +# (ControllerResponseError, ValueError, "Bad controller response"), +# (ValueError, ValueError, "Bad value"), +# ], +# ) +# def test_maintenance_mode_info_00210( +# monkeypatch, maintenance_mode_info, mock_exception, expected_exception, mock_message +# ) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - refresh() + +# Summary +# - Verify MaintenanceModeInfo().refresh() raises ``ValueError`` when +# ``MaintenanceModeInfo().deploy_switches`` raises any of: +# - ``ControllerResponseError`` +# - ``ValueError`` + + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated +# - Required attributes are set +# - change_system_mode() is mocked to do nothing +# - deploy_switches() is mocked to raise each of the above exceptions + +# Code Flow - Test +# - MaintenanceModeInfo().refresh() is called for each exception + +# Expected Result +# - ``ValueError`` is raised +# - Exception message matches expected +# """ + +# def mock_change_system_mode(*args, **kwargs): +# pass + +# def mock_deploy_switches(*args, **kwargs): +# raise mock_exception(mock_message) + +# with does_not_raise(): +# instance = maintenance_mode_info +# instance.config = CONFIG +# instance.rest_send = RestSend({}) +# instance.results = Results() + +# monkeypatch.setattr(instance, "change_system_mode", mock_change_system_mode) +# monkeypatch.setattr(instance, "deploy_switches", mock_deploy_switches) +# with pytest.raises(expected_exception, match=mock_message): +# instance.refresh() + + +# @pytest.mark.parametrize( +# "mode, deploy", +# [ +# ("maintenance", True), +# ("maintenance", False), +# ("normal", True), +# ("normal", False), +# ], +# ) +# def test_maintenance_mode_info_00220(maintenance_mode_info, mode, deploy) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - refresh() +# - change_system_mode() +# - deploy_switches() + +# Summary +# - Verify refresh() success case: +# - RETURN_CODE is 200. +# - Controller response contains expected structure and values. + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated +# - Sender() is mocked to return expected responses +# - Required attributes are set +# - MaintenanceModeInfo().refresh() is called +# - responses_MaintenanceMode contains a dict with: +# - RETURN_CODE == 200 +# - DATA == {"status": "Success"} + +# Code Flow - Test +# - MaintenanceModeInfo().refresh() is called + +# Expected Result +# - Exception is not raised +# - instance.response_data returns expected data +# - MaintenanceModeInfo()._properties are updated +# """ +# method_name = inspect.stack()[0][3] +# key = f"{method_name}a" + +# def responses(): +# yield responses_maintenance_mode_info(key) + +# mock_sender = MockSender() +# mock_sender.gen = ResponseGenerator(responses()) + +# config = copy.deepcopy(CONFIG[0]) +# config["mode"] = mode +# config["deploy"] = deploy + +# with does_not_raise(): +# rest_send = RestSend({"state": "merged", "check_mode": False}) +# rest_send.sender = mock_sender +# rest_send.response_handler = ResponseHandler() +# instance = maintenance_mode_info +# instance.rest_send = rest_send +# instance.rest_send.unit_test = True +# instance.rest_send.timeout = 1 +# instance.results = Results() +# instance.config = [config] + +# with does_not_raise(): +# instance.refresh() + +# assert isinstance(instance.results.diff, list) +# assert isinstance(instance.results.metadata, list) +# assert isinstance(instance.results.response, list) +# assert isinstance(instance.results.result, list) +# assert instance.results.diff[0].get("fabric_name", None) == FABRIC_NAME +# assert instance.results.diff[0].get("ip_address", None) == "192.168.1.2" +# assert instance.results.diff[0].get("maintenance_mode", None) == mode +# assert instance.results.diff[0].get("sequence_number", None) == 1 +# assert instance.results.diff[0].get("serial_number", None) == "FDO22180ASJ" + +# assert instance.results.diff[1].get("config_deploy", None) is True +# assert instance.results.diff[1].get("sequence_number", None) == 2 + +# assert instance.results.metadata[0].get("action", None) == "change_sytem_mode" +# assert instance.results.metadata[0].get("sequence_number", None) == 1 +# assert instance.results.metadata[0].get("state", None) == "merged" + +# assert instance.results.metadata[1].get("action", None) == "config_deploy" +# assert instance.results.metadata[1].get("sequence_number", None) == 2 +# assert instance.results.metadata[1].get("state", None) == "merged" + +# assert instance.results.response[0].get("DATA", {}).get("status") == "Success" +# assert instance.results.response[0].get("MESSAGE", None) == "OK" +# assert instance.results.response[0].get("RETURN_CODE", None) == 200 +# assert instance.results.response[0].get("METHOD", None) == "POST" + +# value = "Configuration deployment completed." +# assert instance.results.response[1].get("DATA", {}).get("status") == value +# assert instance.results.response[1].get("MESSAGE", None) == "OK" +# assert instance.results.response[1].get("RETURN_CODE", None) == 200 +# assert instance.results.response[1].get("METHOD", None) == "POST" + +# assert instance.results.result[0].get("changed", None) is True +# assert instance.results.result[0].get("success", None) is True + +# assert instance.results.result[1].get("changed", None) is True +# assert instance.results.result[1].get("success", None) is True + + +# @pytest.mark.parametrize( +# "mode", +# [ +# ("maintenance"), +# ("normal"), +# ], +# ) +# def test_maintenance_mode_info_00230(maintenance_mode_info, mode) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - refresh() +# - change_system_mode() +# - deploy_switches() + +# Summary +# - Verify refresh() unsuccessful case: +# - RETURN_CODE == 500. +# - refresh raises ``ValueError`` when change_system_mode() raises +# ``ControllerResponseError``. +# - Controller response contains expected structure and values. + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated +# - Sender() is mocked to return expected responses +# - Required attributes are set +# - MaintenanceModeInfo().refresh() is called +# - responses_MaintenanceMode contains a dict with: +# - RETURN_CODE == 500 +# - DATA == {"status": "Failure"} + +# Code Flow - Test +# - ``MaintenanceModeInfo().refresh()`` is called +# - ``change_system_mode()`` raises ``ControllerResponseError`` +# - ``refresh()`` raises ``ValueError`` + +# Expected Result +# - ``refresh()`` raises ``ValueError`` +# - instance.response_data returns expected data +# - MaintenanceModeInfo()._properties are updated +# """ +# method_name = inspect.stack()[0][3] +# key = f"{method_name}a" + +# def responses(): +# yield responses_maintenance_mode_info(key) +# # yield responses_config_deploy(key) + +# mock_sender = MockSender() +# mock_sender.gen = ResponseGenerator(responses()) + +# config = copy.deepcopy(CONFIG[0]) +# config["mode"] = mode + +# with does_not_raise(): +# rest_send = RestSend({"state": "merged", "check_mode": False}) +# rest_send.sender = mock_sender +# rest_send.response_handler = ResponseHandler() +# instance = maintenance_mode_info +# instance.rest_send = rest_send +# instance.rest_send.unit_test = True +# instance.rest_send.timeout = 1 +# instance.results = Results() +# instance.config = [config] + +# match = r"MaintenanceMode\.change_system_mode:\s+" +# match += r"Unable to change system mode on switch:\s+" +# match += rf"fabric_name {config['fabric_name']},\s+" +# match += rf"ip_address {config['ip_address']},\s+" +# match += rf"serial_number {config['serial_number']}\.\s+" +# match += r"Got response\s+.*" +# with pytest.raises(ValueError, match=match): +# instance.refresh() + +# assert isinstance(instance.results.diff, list) +# assert isinstance(instance.results.metadata, list) +# assert isinstance(instance.results.response, list) +# assert isinstance(instance.results.result, list) +# assert len(instance.results.diff[0]) == 1 + +# assert instance.results.metadata[0].get("action", None) == "change_sytem_mode" +# assert instance.results.metadata[0].get("sequence_number", None) == 1 +# assert instance.results.metadata[0].get("state", None) == "merged" + +# assert instance.results.response[0].get("DATA", {}).get("status") == "Failure" +# assert instance.results.response[0].get("MESSAGE", None) == "Internal Server Error" +# assert instance.results.response[0].get("RETURN_CODE", None) == 500 +# assert instance.results.response[0].get("METHOD", None) == "POST" + +# assert instance.results.result[0].get("changed", None) is False +# assert instance.results.result[0].get("success", None) is False + + +# def test_maintenance_mode_info_00300(maintenance_mode_info) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - verify_config_parameters() +# - config.setter + +# Summary +# - Verify MaintenanceModeInfo().verify_config_parameters() raises +# - ``TypeError`` if: +# - value is not a list +# - Verify MaintenanceModeInfo().config.setter re-raises: +# - ``TypeError`` as ``ValueError`` + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated +# - config is set to a non-list value + +# Code Flow - Test +# - MaintenanceModeInfo().config.setter is accessed with non-list + +# Expected Result +# - verify_config_parameters() raises ``TypeError``. +# - config.setter re-raises as ``ValueError``. +# - Exception message matches expected. +# """ +# with does_not_raise(): +# instance = maintenance_mode_info +# match = r"MaintenanceMode\.verify_config_parameters:\s+" +# match += r"MaintenanceMode\.config must be a list\.\s+" +# match += r"Got type: str\." +# with pytest.raises(ValueError, match=match): +# instance.config = "NOT_A_LIST" + + +# @pytest.mark.parametrize( +# "remove_param", +# [("deploy"), ("fabric_name"), ("ip_address"), ("mode"), ("serial_number")], +# ) +# def test_maintenance_mode_info_00310(maintenance_mode_info, remove_param) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - verify_config_parameters() +# - config.setter + +# Summary +# - Verify MaintenanceModeInfo().verify_config_parameters() raises +# - ``ValueError`` if: +# - deploy is missing from config +# - fabric_name is missing from config +# - ip_address is missing from config +# - mode is missing from config +# - serial_number is missing from config + + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated + +# Code Flow - Test +# - MaintenanceModeInfo().config is set to a dict with all of the above +# keys present, except that each key, in turn, is removed. + +# Expected Result +# - ``ValueError`` is raised +# - Exception message matches expected +# """ + +# with does_not_raise(): +# instance = maintenance_mode_info + +# config = copy.deepcopy(CONFIG[0]) +# del config[remove_param] +# match = rf"MaintenanceMode\.verify_{remove_param}:\s+" +# match += rf"config is missing mandatory key: {remove_param}\." +# with pytest.raises(ValueError, match=match): +# instance.config = [config] + + +# @pytest.mark.parametrize( +# "param, raises", +# [ +# (False, None), +# (True, None), +# (10, ValueError), +# ("FOO", ValueError), +# (["FOO"], ValueError), +# ({"FOO": "BAR"}, ValueError), +# ], +# ) +# def test_maintenance_mode_info_00400(maintenance_mode_info, param, raises) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - verify_config_parameters() +# - config.setter + +# Summary +# - Verify MaintenanceModeInfo().verify_config_parameters() re-raises +# - ``ValueError`` if: +# - ``deploy`` raises ``TypeError`` + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated + +# Code Flow - Test +# - MaintenanceModeInfo().config is set to a dict. +# - The dict is updated with deploy set to valid and invalid +# values of ``deploy`` + +# Expected Result +# - ``ValueError`` is raised when deploy is not a boolean +# - Exception message matches expected +# - Exception is not raised when deploy is a boolean +# """ + +# with does_not_raise(): +# instance = maintenance_mode_info + +# config = copy.deepcopy(CONFIG[0]) +# config["deploy"] = param +# match = r"MaintenanceMode\.verify_deploy:\s+" +# match += r"Expected boolean for deploy\.\s+" +# match += r"Got type\s+" +# if raises: +# with pytest.raises(raises, match=match): +# instance.config = [config] +# else: +# instance.config = [config] +# assert instance.config[0]["deploy"] == param + + +# @pytest.mark.parametrize( +# "param, raises", +# [ +# ("MyFabric", None), +# ("MyFabric_123", None), +# ("10MyFabric", ValueError), +# ("_MyFabric", ValueError), +# ("MyFabric&BadFabric", ValueError), +# ], +# ) +# def test_maintenance_mode_info_00500(maintenance_mode_info, param, raises) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - verify_config_parameters() +# - config.setter + +# Summary +# - Verify MaintenanceModeInfo().verify_config_parameters() re-raises +# - ``ValueError`` if: +# - ``fabric_name`` raises ``ValueError`` due to being an +# invalid value. + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated + +# Code Flow - Test +# - MaintenanceModeInfo().config is set to a dict. +# - The dict is updated with fabric_name set to valid and invalid +# values of ``fabric_name`` + +# Expected Result +# - ``ValueError`` is raised when fabric_name is not a valid value +# - Exception message matches expected +# - Exception is not raised when fabric_name is a valid value +# """ + +# with does_not_raise(): +# instance = maintenance_mode_info + +# config = copy.deepcopy(CONFIG[0]) +# config["fabric_name"] = param +# match = r"ConversionUtils\.validate_fabric_name:\s+" +# match += rf"Invalid fabric name: {param}\.\s+" +# match += r"Fabric name must start with a letter A-Z or a-z and contain\s+" +# match += r"only the characters in:" +# if raises: +# with pytest.raises(raises, match=match): +# instance.config = [config] +# else: +# instance.config = [config] +# assert instance.config[0]["fabric_name"] == param + + +# @pytest.mark.parametrize( +# "param, raises", +# [ +# ("maintenance", None), +# ("normal", None), +# (10, ValueError), +# (["192.168.1.2"], ValueError), +# ({"ip_address": "192.168.1.2"}, ValueError), +# ], +# ) +# def test_maintenance_mode_info_00600(maintenance_mode_info, param, raises) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - verify_config_parameters() +# - config.setter + +# Summary +# - Verify MaintenanceModeInfo().verify_config_parameters() re-raises +# - ``ValueError`` if: +# - ``mode`` raises ``ValueError`` due to being an +# invalid value. + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated + +# Code Flow - Test +# - MaintenanceModeInfo().config is set to a dict. +# - The dict is updated with mode set to valid and invalid +# values of ``mode`` + +# Expected Result +# - ``ValueError`` is raised when mode is not a valid value +# - Exception message matches expected +# - Exception is not raised when mode is a valid value +# """ + +# with does_not_raise(): +# instance = maintenance_mode_info + +# config = copy.deepcopy(CONFIG[0]) +# config["mode"] = param +# match = r"MaintenanceMode\.verify_mode:\s+" +# match += r"mode must be one of\s+" +# if raises: +# with pytest.raises(raises, match=match): +# instance.config = [config] +# else: +# instance.config = [config] +# assert instance.config[0]["mode"] == param + + +# @pytest.mark.parametrize( +# "endpoint_instance, mock_exception, expected_exception, mock_message", +# [ +# ("ep_maintenance_mode_disable", TypeError, ValueError, "Bad type"), +# ("ep_maintenance_mode_disable", ValueError, ValueError, "Bad value"), +# ("ep_maintenance_mode_enable", TypeError, ValueError, "Bad type"), +# ("ep_maintenance_mode_enable", ValueError, ValueError, "Bad value"), +# ], +# ) +# def test_maintenance_mode_info_00700( +# monkeypatch, +# maintenance_mode_info, +# endpoint_instance, +# mock_exception, +# expected_exception, +# mock_message, +# ) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - refresh() + +# Summary +# - Verify MaintenanceModeInfo().change_system_mode() raises ``ValueError`` +# when ``EpMaintenanceModeEnable`` or ``EpMaintenanceModeDisable`` raise +# any of: +# - ``TypeError`` +# - ``ValueError`` + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated +# - Required attributes are set +# - EpMaintenanceModeEnable() is mocked to raise each +# of the above exceptions +# - EpMaintenanceModeDisable() is mocked to raise each +# of the above exceptions + +# Code Flow - Test +# - MaintenanceModeInfo().refresh() is called for each exception + +# Expected Result +# - ``ValueError`` is raised. +# - Exception message matches expected. +# """ + +# class MockEndpoint: +# """ +# Mock Ep*() class +# """ + +# def __init__(self): +# self._fabric_name = None +# self._serial_number = None + +# @property +# def fabric_name(self): +# """ +# Mock fabric_name getter/setter +# """ +# return self._fabric_name + +# @fabric_name.setter +# def fabric_name(self, value): +# raise mock_exception(mock_message) + +# @property +# def serial_number(self): +# """ +# Mock serial_number getter/setter +# """ +# return self._serial_number + +# @serial_number.setter +# def serial_number(self, value): +# self._serial_number = value + +# with does_not_raise(): +# instance = maintenance_mode_info +# config = copy.deepcopy(CONFIG[0]) +# if endpoint_instance == "ep_maintenance_mode_disable": +# config["mode"] = "normal" +# instance.config = [config] +# instance.rest_send = RestSend({}) +# instance.results = Results() + +# monkeypatch.setattr(instance, endpoint_instance, MockEndpoint()) +# with pytest.raises(expected_exception, match=mock_message): +# instance.refresh() + + +# @pytest.mark.parametrize( +# "endpoint_instance, mock_exception, expected_exception, mock_message", +# [ +# ("ep_fabric_config_deploy", TypeError, ValueError, "Bad type"), +# ("ep_fabric_config_deploy", ValueError, ValueError, "Bad value"), +# ], +# ) +# def test_maintenance_mode_info_00800( +# monkeypatch, +# maintenance_mode_info, +# endpoint_instance, +# mock_exception, +# expected_exception, +# mock_message, +# ) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - refresh() + +# Summary +# - Verify MaintenanceModeInfo().deploy_switches() raises ``ValueError`` +# when ``EpFabricConfigDeploy`` raises any of: +# - ``TypeError`` +# - ``ValueError`` + + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated +# - Required attributes are set +# - EpFabricConfigDeploy() is mocked to raise each of the above exceptions + +# Code Flow - Test +# - MaintenanceModeInfo().refresh() is called for each exception + +# Expected Result +# - ``TypeError`` and ``ValueError`` are raised. +# - Exception message matches expected. +# """ + +# class MockEndpoint: +# """ +# Mock EpFabricConfigDeploy() class +# """ + +# def __init__(self): +# self._fabric_name = None +# self._switch_id = None + +# @property +# def fabric_name(self): +# """ +# Mock fabric_name getter/setter +# """ +# return self._fabric_name + +# @fabric_name.setter +# def fabric_name(self, value): +# raise mock_exception(mock_message) + +# @property +# def switch_id(self): +# """ +# Mock switch_id getter/setter +# """ +# return self._switch_id + +# @switch_id.setter +# def switch_id(self, value): +# self._switch_id = value + +# def responses(): +# yield {"MESSAGE": "OK", "RETURN_CODE": 200, "DATA": {"status": "Success"}} + +# mock_sender = MockSender() +# mock_sender.gen = ResponseGenerator(responses()) +# rest_send = RestSend({"state": "merged", "check_mode": False}) +# rest_send.sender = mock_sender +# rest_send.response_handler = ResponseHandler() +# rest_send.unit_test = True +# rest_send.timeout = 1 + +# config = copy.deepcopy(CONFIG[0]) +# config["deploy"] = True + +# with does_not_raise(): +# instance = maintenance_mode_info +# instance.config = [config] +# instance.rest_send = rest_send +# instance.results = Results() + +# monkeypatch.setattr(instance, endpoint_instance, MockEndpoint()) +# with pytest.raises(expected_exception, match=mock_message): +# instance.refresh() + + +# @pytest.mark.parametrize( +# "mock_exception, expected_exception, mock_message", +# [ +# (TypeError, ValueError, r"Converted TypeError to ValueError"), +# (ValueError, ValueError, r"Converted ValueError to ValueError"), +# ], +# ) +# def test_maintenance_mode_info_00900( +# maintenance_mode_info, mock_exception, expected_exception, mock_message +# ) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - change_system_mode() + + +# Summary +# - Verify MaintenanceModeInfo().change_system_mode() raises ``ValueError`` +# when ``MaintenanceModeInfo().results()`` raises any of: +# - ``TypeError`` +# - ``ValueError`` + + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated +# - Required attributes are set +# - Results().response_current.setter is mocked to raise each of the above +# exceptions + +# Code Flow - Test +# - MaintenanceModeInfo().refresh() is called for each exception + +# Expected Result +# - ``ValueError`` is raised +# - Exception message matches expected +# """ + +# class MockResults: +# """ +# Mock the Results class +# """ + +# class_name = "Results" + +# def register_task_result(self, *args): +# """ +# do nothing +# """ + +# @property +# def response_current(self): +# """ +# mock response_current getter +# """ +# return {"success": True} + +# @response_current.setter +# def response_current(self, *args): +# raise mock_exception(mock_message) + +# def responses(): +# yield {"RETURN_CODE": 200, "MESSAGE": "OK", "DATA": {"status": "Success"}} + +# mock_sender = MockSender() +# mock_sender.gen = ResponseGenerator(responses()) + +# with does_not_raise(): +# rest_send = RestSend({"state": "merged", "check_mode": False}) +# rest_send.sender = mock_sender +# rest_send.response_handler = ResponseHandler() +# instance = maintenance_mode_info +# instance.rest_send = rest_send +# instance.rest_send.unit_test = True +# instance.rest_send.timeout = 1 +# instance.config = CONFIG +# instance.results = MockResults() + +# with pytest.raises(expected_exception, match=mock_message): +# instance.refresh() + + +# def test_maintenance_mode_info_01000(monkeypatch, maintenance_mode_info) -> None: +# """ +# Classes and Methods +# - MaintenanceModeInfo() +# - __init__() +# - refresh() + +# Summary +# - Verify MaintenanceModeInfo().refresh() raises ``ValueError`` when +# ``MaintenanceModeInfo().deploy_switches()`` raises +# ``ControllerResponseError`` when the RETURN_CODE in the +# response is not 200. + +# Code Flow - Setup +# - MaintenanceModeInfo() is instantiated +# - Required attributes are set + +# Code Flow - Test +# - MaintenanceModeInfo().refresh() is called with simulated responses: +# - 200 response for ``change_system_mode()`` +# - 500 response ``deploy_switches()`` + +# Expected Result +# - ``ValueError``is raised. +# - Exception message matches expected. +# """ + +# def responses(): +# yield {"MESSAGE": "OK", "RETURN_CODE": 200, "DATA": {"status": "Success"}} +# yield { +# "MESSAGE": "Internal server error", +# "RETURN_CODE": 500, +# "DATA": {"status": "Success"}, +# } + +# mock_sender = MockSender() +# mock_sender.gen = ResponseGenerator(responses()) +# rest_send = RestSend({"state": "merged", "check_mode": False}) +# rest_send.sender = mock_sender +# rest_send.response_handler = ResponseHandler() +# rest_send.unit_test = True +# rest_send.timeout = 1 + +# config = copy.deepcopy(CONFIG[0]) +# config["deploy"] = True + +# with does_not_raise(): +# instance = maintenance_mode_info +# instance.config = [config] +# instance.rest_send = rest_send +# instance.results = Results() + +# match = r"MaintenanceMode\.deploy_switches:\s+" +# match += r"Unable to deploy switches:\s+" +# match += r"fabric_name VXLAN_Fabric,\s+" +# match += r"serial_numbers FDO22180ASJ\.\s+" +# match += r"Got response.*\." +# with pytest.raises(ValueError, match=match): +# instance.refresh() From 97a275a84c4a69c8ae4966c3ac4e7d45d32733cb Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 5 Jun 2024 11:28:23 -1000 Subject: [PATCH 128/230] Forgot to add the mocks in the last commit. Test cases were failing due to missing mocks. --- tests/unit/mocks/__init__.py | 0 .../unit/mocks/mock_fabric_details_by_name.py | 133 ++++++++++++++ tests/unit/mocks/mock_switch_details.py | 165 ++++++++++++++++++ 3 files changed, 298 insertions(+) create mode 100644 tests/unit/mocks/__init__.py create mode 100644 tests/unit/mocks/mock_fabric_details_by_name.py create mode 100644 tests/unit/mocks/mock_switch_details.py diff --git a/tests/unit/mocks/__init__.py b/tests/unit/mocks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/mocks/mock_fabric_details_by_name.py b/tests/unit/mocks/mock_fabric_details_by_name.py new file mode 100644 index 000000000..bac7673cb --- /dev/null +++ b/tests/unit/mocks/mock_fabric_details_by_name.py @@ -0,0 +1,133 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-import +# pylint: disable=redefined-outer-name +# pylint: disable=protected-access +# pylint: disable=unused-argument +# pylint: disable=invalid-name + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + + +class MockFabricDetailsByName: + """ + Mock the FabricDetailsByName class + """ + + def __init__(self) -> None: + + def null_mock_exception(): + pass + + self.class_name = "FabricDetailsByName" + self._mock_class = None + self._mock_exception = null_mock_exception + self._mock_message = None + self._mock_property = None + + self._rest_send = None + self._results = None + self._is_read_only = None + + def refresh(self): + """ + Mocked refresh method + """ + + @property + def mock_class(self): + """ + If this matches self.class_name, raise mock_exception. + """ + return self._mock_class + + @mock_class.setter + def mock_class(self, value): + self._mock_class = value + + @property + def mock_exception(self): + """ + The exception to raise. + """ + return self._mock_exception + + @mock_exception.setter + def mock_exception(self, value): + self._mock_exception = value + + @property + def mock_message(self): + """ + The message to include with the raised mock_exception. + """ + return self._mock_message + + @mock_message.setter + def mock_message(self, value): + self._mock_message = value + + @property + def mock_property(self): + """ + The property in which to raise the mock_exception. + """ + return self._mock_property + + @mock_property.setter + def mock_property(self, value): + self._mock_property = value + + @property + def rest_send(self): + """ + Mocked rest_send property + """ + return self._rest_send + + @rest_send.setter + def rest_send(self, value): + if self.mock_class == self.class_name and self.mock_property == "rest_send": + raise self.mock_exception(self.mock_message) + self._rest_send = value + + @property + def results(self): + """ + Mocked results property + """ + return self._results + + @results.setter + def results(self, value): + if self.mock_class == self.class_name and self.mock_property == "results": + raise self.mock_exception(self.mock_message) + self._results = value + + @property + def is_read_only(self): + """ + Mocked is_read_only property + """ + return self._is_read_only diff --git a/tests/unit/mocks/mock_switch_details.py b/tests/unit/mocks/mock_switch_details.py new file mode 100644 index 000000000..fcc614481 --- /dev/null +++ b/tests/unit/mocks/mock_switch_details.py @@ -0,0 +1,165 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-import +# pylint: disable=redefined-outer-name +# pylint: disable=protected-access +# pylint: disable=unused-argument +# pylint: disable=invalid-name + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + + +class MockSwitchDetails: + """ + Mock the SwitchDetails class + """ + + def __init__(self) -> None: + + def null_mock_exception(): + pass + + self.class_name = "SwitchDetails" + self._mock_class = None + self._mock_exception = null_mock_exception + self._mock_message = None + self._mock_property = None + + self._rest_send = None + self._results = None + self._serial_number = None + self._fabric_name = None + self._freeze_mode = None + self._maintenance_mode = None + self._switch_role = None + + def refresh(self): + """ + Mocked refresh method + """ + + @property + def mock_class(self): + """ + If this matches self.class_name, raise mock_exception. + """ + return self._mock_class + + @mock_class.setter + def mock_class(self, value): + self._mock_class = value + + @property + def mock_exception(self): + """ + The exception to raise. + """ + return self._mock_exception + + @mock_exception.setter + def mock_exception(self, value): + self._mock_exception = value + + @property + def mock_message(self): + """ + The message to include with the raised mock_exception. + """ + return self._mock_message + + @mock_message.setter + def mock_message(self, value): + self._mock_message = value + + @property + def mock_property(self): + """ + The property in which to raise the mock_exception. + """ + return self._mock_property + + @mock_property.setter + def mock_property(self, value): + self._mock_property = value + + @property + def rest_send(self): + """ + Mocked rest_send property + """ + return self._rest_send + + @rest_send.setter + def rest_send(self, value): + if self.mock_class == self.class_name and self.mock_property == "rest_send": + raise self.mock_exception(self.mock_message) # pylint: disable=not-callable + self._rest_send = value + + @property + def results(self): + """ + Mocked results property + """ + return self._results + + @results.setter + def results(self, value): + if self.mock_class == self.class_name and self.mock_property == "results": + raise self.mock_exception(self.mock_message) + self._results = value + + @property + def fabric_name(self): + """ + Mocked fabric_name property + """ + return self._fabric_name + + @property + def freeze_mode(self): + """ + Mocked freeze_mode property + """ + return self._freeze_mode + + @property + def maintenance_mode(self): + """ + Mocked maintenance_mode property + """ + return self._maintenance_mode + + @property + def serial_number(self): + """ + Mocked serial_number property + """ + return self._serial_number + + @property + def switch_role(self): + """ + Mocked switch_role property + """ + return self._switch_role From f9af68b69f8aae37c46dbc2a3bab752025658678 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 5 Jun 2024 11:38:40 -1000 Subject: [PATCH 129/230] MockSwitchDetails(): Remove pylint not-callable Missed this one in the last commit. --- tests/unit/mocks/mock_switch_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/mocks/mock_switch_details.py b/tests/unit/mocks/mock_switch_details.py index fcc614481..dba93bc32 100644 --- a/tests/unit/mocks/mock_switch_details.py +++ b/tests/unit/mocks/mock_switch_details.py @@ -113,7 +113,7 @@ def rest_send(self): @rest_send.setter def rest_send(self, value): if self.mock_class == self.class_name and self.mock_property == "rest_send": - raise self.mock_exception(self.mock_message) # pylint: disable=not-callable + raise self.mock_exception(self.mock_message) self._rest_send = value @property From 1817fcdaf71d2e0ab8074471e9af4662dbdd26bb Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 5 Jun 2024 12:31:05 -1000 Subject: [PATCH 130/230] Mock*(): Remove pylint disable= statements Didn't notice the "pylint disable=" statements at the top of each file which were copy/pasted from one of the unit test files where they are needed. --- tests/unit/mocks/mock_fabric_details_by_name.py | 10 ---------- tests/unit/mocks/mock_switch_details.py | 10 ---------- 2 files changed, 20 deletions(-) diff --git a/tests/unit/mocks/mock_fabric_details_by_name.py b/tests/unit/mocks/mock_fabric_details_by_name.py index bac7673cb..fbc494054 100644 --- a/tests/unit/mocks/mock_fabric_details_by_name.py +++ b/tests/unit/mocks/mock_fabric_details_by_name.py @@ -12,16 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# See the following regarding *_fixture imports -# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html -# Due to the above, we also need to disable unused-import -# Also, fixtures need to use *args to match the signature of the function they are mocking -# pylint: disable=unused-import -# pylint: disable=redefined-outer-name -# pylint: disable=protected-access -# pylint: disable=unused-argument -# pylint: disable=invalid-name - from __future__ import absolute_import, division, print_function __metaclass__ = type diff --git a/tests/unit/mocks/mock_switch_details.py b/tests/unit/mocks/mock_switch_details.py index dba93bc32..77ac1e700 100644 --- a/tests/unit/mocks/mock_switch_details.py +++ b/tests/unit/mocks/mock_switch_details.py @@ -12,16 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# See the following regarding *_fixture imports -# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html -# Due to the above, we also need to disable unused-import -# Also, fixtures need to use *args to match the signature of the function they are mocking -# pylint: disable=unused-import -# pylint: disable=redefined-outer-name -# pylint: disable=protected-access -# pylint: disable=unused-argument -# pylint: disable=invalid-name - from __future__ import absolute_import, division, print_function __metaclass__ = type From 80fd9a604e325f960cc81712ac66a8c6eee9db5f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 5 Jun 2024 14:50:55 -1000 Subject: [PATCH 131/230] MaintenanceModeInfo(): 64% unit test coverage, more... 1. MockSwitchDetails(): Populate properties from responses_SwitchDetails.json if mock_response_key is set. MockSwitchDetails().filter must be set to an IP address prior to setting mock_response_key. 2. MockSwitchDetails(): Update getters and setters to raise mock_exception if mock_class and mock_property match the getter/setter criteria. 3. MockFabricDetailsByName(): Update getters and setters to raise mock_exception if mock_class and mock_property match the getter/setter criteria. 4. MaintenanceModeInfo(): Remove some debug logs. --- .../common/maintenance_mode_info.py | 4 - .../unit/mocks/mock_fabric_details_by_name.py | 37 ++- tests/unit/mocks/mock_switch_details.py | 156 ++++++++++- .../fixtures/responses_SwitchDetails.json | 252 +++++++++++++++++ .../common/test_maintenance_mode_info.py | 261 +++++++++++++++++- 5 files changed, 690 insertions(+), 20 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode_info.py b/plugins/module_utils/common/maintenance_mode_info.py index 7db9be3e1..4262c541e 100644 --- a/plugins/module_utils/common/maintenance_mode_info.py +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -222,14 +222,10 @@ def refresh(self): self.verify_refresh_parameters() try: - self.log.debug("ZZZ: set self.switch_details.rest_send") self.switch_details.rest_send = self.rest_send - self.log.debug("ZZZ: set self.fabric_details.rest_send") self.fabric_details.rest_send = self.rest_send - self.log.debug("ZZZ: set self.switch_details.results") self.switch_details.results = self.results - self.log.debug("ZZZ: set self.fabric_details.results") self.fabric_details.results = self.results except TypeError as error: raise ValueError(error) from error diff --git a/tests/unit/mocks/mock_fabric_details_by_name.py b/tests/unit/mocks/mock_fabric_details_by_name.py index fbc494054..9fffcf983 100644 --- a/tests/unit/mocks/mock_fabric_details_by_name.py +++ b/tests/unit/mocks/mock_fabric_details_by_name.py @@ -44,6 +44,8 @@ def refresh(self): """ Mocked refresh method """ + if self.mock_class == self.class_name and self.mock_property == "refresh": + raise self.mock_exception(self.mock_message) @property def mock_class(self): @@ -89,16 +91,39 @@ def mock_property(self): def mock_property(self, value): self._mock_property = value + @property + def filter(self): + """ + Mocked filter property + """ + if self.mock_class == self.class_name and self.mock_property == "filter.getter": + raise self.mock_exception(self.mock_message) + return self._filter + + @filter.setter + def filter(self, value): + if self.mock_class == self.class_name and self.mock_property == "filter.setter": + raise self.mock_exception(self.mock_message) + self._filter = value + @property def rest_send(self): """ Mocked rest_send property """ + if ( + self.mock_class == self.class_name + and self.mock_property == "rest_send.getter" + ): + raise self.mock_exception(self.mock_message) return self._rest_send @rest_send.setter def rest_send(self, value): - if self.mock_class == self.class_name and self.mock_property == "rest_send": + if ( + self.mock_class == self.class_name + and self.mock_property == "rest_send.setter" + ): raise self.mock_exception(self.mock_message) self._rest_send = value @@ -107,11 +132,19 @@ def results(self): """ Mocked results property """ + if ( + self.mock_class == self.class_name + and self.mock_property == "results.getter" + ): + raise self.mock_exception(self.mock_message) return self._results @results.setter def results(self, value): - if self.mock_class == self.class_name and self.mock_property == "results": + if ( + self.mock_class == self.class_name + and self.mock_property == "results.setter" + ): raise self.mock_exception(self.mock_message) self._results = value diff --git a/tests/unit/mocks/mock_switch_details.py b/tests/unit/mocks/mock_switch_details.py index 77ac1e700..f5768c366 100644 --- a/tests/unit/mocks/mock_switch_details.py +++ b/tests/unit/mocks/mock_switch_details.py @@ -19,6 +19,9 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + responses_switch_details + class MockSwitchDetails: """ @@ -35,19 +38,63 @@ def null_mock_exception(): self._mock_exception = null_mock_exception self._mock_message = None self._mock_property = None + self._mock_response_key = None - self._rest_send = None - self._results = None - self._serial_number = None + self.response = None + self.response_data = None + + self._filter = None + self._info = {} self._fabric_name = None self._freeze_mode = None self._maintenance_mode = None + self._mode = None + self._rest_send = None + self._results = None + self._serial_number = None self._switch_role = None + self._system_mode = None def refresh(self): """ Mocked refresh method """ + if self.mock_class == self.class_name and self.mock_property == "refresh": + raise self.mock_exception(self.mock_message) + + def populate_info(self): + """ + Populate the info dict. + """ + self._info = {} + self.response = responses_switch_details(self.mock_response_key) + self.response_data = self.response.get("DATA", []) + for switch in self.response_data: + self._info[switch["ipAddress"]] = switch + + def populate_mocked_properties(self): + """ + Set the mocked property values from the contents of the mocked response. + """ + if self.mock_response_key: + self.populate_info() + if self.filter is None: + raise ValueError( + "filter must be set before calling populate_mocked_properties()" + ) + + self.serial_number = self._info.get(self.filter, {}).get("serialNumber") + self.fabric_name = self._info.get(self.filter, {}).get("fabricName") + self.freeze_mode = self._info.get(self.filter, {}).get("freezeMode") + self.mode = self._info.get(self.filter, {}).get("mode") + self.system_mode = self._info.get(self.filter, {}).get("systemMode") + + if str(self.mode).lower() == "migration": + self.maintenance_mode = "migration" + elif str(self.mode).lower() != str(self.system_mode).lower(): + self.maintenance_mode = "inconsistent" + else: + self.maintenance_mode = self.mode @property def mock_class(self): @@ -93,16 +140,55 @@ def mock_property(self): def mock_property(self, value): self._mock_property = value + @property + def filter(self): + """ + IP Address of the switch with which to filter self._info() + """ + if self.mock_class == self.class_name and self.mock_property == "filter.getter": + raise self.mock_exception(self.mock_message) + return self._filter + + @filter.setter + def filter(self, value): + if self.mock_class == self.class_name and self.mock_property == "filter.setter": + raise self.mock_exception(self.mock_message) + self._filter = value + + @property + def mock_response_key(self): + """ + The key used to extract controller response from the mocked response + in ``responses_SwitchDetails.json``. + + When setter is accessed, call ``populate_properties()`` to set the + mocked property values from the contents of the mocked response. + """ + return self._mock_response_key + + @mock_response_key.setter + def mock_response_key(self, value): + self._mock_response_key = value + self.populate_mocked_properties() + @property def rest_send(self): """ Mocked rest_send property """ + if ( + self.mock_class == self.class_name + and self.mock_property == "rest_send.getter" + ): + raise self.mock_exception(self.mock_message) return self._rest_send @rest_send.setter def rest_send(self, value): - if self.mock_class == self.class_name and self.mock_property == "rest_send": + if ( + self.mock_class == self.class_name + and self.mock_property == "rest_send.setter" + ): raise self.mock_exception(self.mock_message) self._rest_send = value @@ -111,11 +197,19 @@ def results(self): """ Mocked results property """ + if ( + self.mock_class == self.class_name + and self.mock_property == "results.getter" + ): + raise self.mock_exception(self.mock_message) return self._results @results.setter def results(self, value): - if self.mock_class == self.class_name and self.mock_property == "results": + if ( + self.mock_class == self.class_name + and self.mock_property == "results.setter" + ): raise self.mock_exception(self.mock_message) self._results = value @@ -126,6 +220,10 @@ def fabric_name(self): """ return self._fabric_name + @fabric_name.setter + def fabric_name(self, value): + self._fabric_name = value + @property def freeze_mode(self): """ @@ -133,6 +231,10 @@ def freeze_mode(self): """ return self._freeze_mode + @freeze_mode.setter + def freeze_mode(self, value): + self._freeze_mode = value + @property def maintenance_mode(self): """ @@ -140,16 +242,60 @@ def maintenance_mode(self): """ return self._maintenance_mode + @maintenance_mode.setter + def maintenance_mode(self, value): + self._maintenance_mode = value + + @property + def mode(self): + """ + Mocked mode property + """ + return self._mode + + @mode.setter + def mode(self, value): + self._mode = value + @property def serial_number(self): """ Mocked serial_number property """ + if ( + self.mock_class == self.class_name + and self.mock_property == "serial_number.getter" + ): + raise self.mock_exception(self.mock_message) return self._serial_number + @serial_number.setter + def serial_number(self, value): + if ( + self.mock_class == self.class_name + and self.mock_property == "serial_number.setter" + ): + raise self.mock_exception(self.mock_message) + self._serial_number = value + @property def switch_role(self): """ Mocked switch_role property """ return self._switch_role + + @switch_role.setter + def switch_role(self, value): + self._switch_role = value + + @property + def system_mode(self): + """ + Mocked switch_role property + """ + return self._system_mode + + @system_mode.setter + def system_mode(self, value): + self._system_mode = value diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json index 1368b4d30..2dbaf014e 100644 --- a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -121,5 +121,257 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00300a": { + "TEST_NOTES": [ + "DATA does not contain switch with ip address 192.168.1.2" + ], + "DATA": [ + { + "activeSupSlot": 0, + "availPorts": 0, + "ccStatus": "NA", + "cfsSyslogStatus": 1, + "colDBId": 0, + "connUnitStatus": 0, + "consistencyState": false, + "contact": null, + "cpuUsage": 0, + "deviceType": "External", + "displayHdrs": null, + "displayValues": null, + "domain": null, + "domainID": 0, + "elementType": null, + "fabricId": 3, + "fabricName": "FOO", + "fabricTechnology": "LANClassic", + "fcoeEnabled": false, + "fex": false, + "fexMap": {}, + "fid": 0, + "freezeMode": null, + "health": -1, + "hostName": "cvd-1314-leaf", + "index": 0, + "intentedpeerName": "", + "interfaces": null, + "ipAddress": "192.168.1.1", + "ipDomain": "", + "isEchSupport": false, + "isLan": false, + "isNonNexus": false, + "isPmCollect": false, + "isTrapDelayed": false, + "isVpcConfigured": false, + "is_smlic_enabled": false, + "keepAliveState": null, + "lastScanTime": 0, + "licenseDetail": null, + "licenseViolation": false, + "linkName": null, + "location": null, + "logicalName": "cvd-1314-leaf", + "managable": true, + "mds": false, + "membership": null, + "memoryUsage": 0, + "mgmtAddress": null, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "moduleIndexOffset": 9999, + "modules": null, + "monitorMode": true, + "name": null, + "network": null, + "nonMdsModel": null, + "npvEnabled": false, + "numberOfPorts": 0, + "operMode": null, + "operStatus": "Minor", + "peer": null, + "peerSerialNumber": null, + "peerSwitchDbId": 0, + "peerlinkState": null, + "ports": 0, + "present": true, + "primaryIP": "", + "primarySwitchDbID": 0, + "principal": null, + "protoDiscSettings": null, + "recvIntf": null, + "release": "10.2(5)", + "role": null, + "sanAnalyticsCapable": false, + "scope": null, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "sendIntf": null, + "serialNumber": "FDO211218FV", + "sourceInterface": "mgmt0", + "sourceVrf": "management", + "standbySupState": 0, + "status": "ok", + "swType": null, + "swUUID": "DCNM-UUID-132770", + "swUUIDId": 132770, + "swWwn": null, + "swWwnName": null, + "switchDbID": 502030, + "switchRole": "leaf", + "switchRoleEnum": "Leaf", + "sysDescr": "", + "systemMode": "Normal", + "uid": 0, + "unmanagableCause": "", + "upTime": 0, + "upTimeNumber": 0, + "upTimeStr": "98 days, 21:55:52", + "usedPorts": 0, + "username": null, + "vdcId": 0, + "vdcMac": null, + "vdcName": "", + "vendor": "Cisco", + "version": null, + "vpcDomain": 0, + "vrf": "management", + "vsanWwn": null, + "vsanWwnName": null, + "waitForSwitchModeChg": false, + "wwn": null + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00400a": { + "TEST_NOTES": [ + "DATA contains switch with ip address 192.168.1.2" + ], + "DATA": [ + { + "activeSupSlot": 0, + "availPorts": 0, + "ccStatus": "NA", + "cfsSyslogStatus": 1, + "colDBId": 0, + "connUnitStatus": 0, + "consistencyState": false, + "contact": null, + "cpuUsage": 0, + "deviceType": "External", + "displayHdrs": null, + "displayValues": null, + "domain": null, + "domainID": 0, + "elementType": null, + "fabricId": 3, + "fabricName": "FOO", + "fabricTechnology": "LANClassic", + "fcoeEnabled": false, + "fex": false, + "fexMap": {}, + "fid": 0, + "freezeMode": null, + "health": -1, + "hostName": "cvd-1314-leaf", + "index": 0, + "intentedpeerName": "", + "interfaces": null, + "ipAddress": "192.168.1.2", + "ipDomain": "", + "isEchSupport": false, + "isLan": false, + "isNonNexus": false, + "isPmCollect": false, + "isTrapDelayed": false, + "isVpcConfigured": false, + "is_smlic_enabled": false, + "keepAliveState": null, + "lastScanTime": 0, + "licenseDetail": null, + "licenseViolation": false, + "linkName": null, + "location": null, + "logicalName": "cvd-1314-leaf", + "managable": true, + "mds": false, + "membership": null, + "memoryUsage": 0, + "mgmtAddress": null, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "moduleIndexOffset": 9999, + "modules": null, + "monitorMode": true, + "name": null, + "network": null, + "nonMdsModel": null, + "npvEnabled": false, + "numberOfPorts": 0, + "operMode": null, + "operStatus": "Minor", + "peer": null, + "peerSerialNumber": null, + "peerSwitchDbId": 0, + "peerlinkState": null, + "ports": 0, + "present": true, + "primaryIP": "", + "primarySwitchDbID": 0, + "principal": null, + "protoDiscSettings": null, + "recvIntf": null, + "release": "10.2(5)", + "role": null, + "sanAnalyticsCapable": false, + "scope": null, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "sendIntf": null, + "serialNumber": "FDO211218FV", + "sourceInterface": "mgmt0", + "sourceVrf": "management", + "standbySupState": 0, + "status": "ok", + "swType": null, + "swUUID": "DCNM-UUID-132770", + "swUUIDId": 132770, + "swWwn": null, + "swWwnName": null, + "switchDbID": 502030, + "switchRole": "leaf", + "switchRoleEnum": "Leaf", + "sysDescr": "", + "systemMode": "Normal", + "uid": 0, + "unmanagableCause": "", + "upTime": 0, + "upTimeNumber": 0, + "upTimeStr": "98 days, 21:55:52", + "usedPorts": 0, + "username": null, + "vdcId": 0, + "vdcMac": null, + "vdcName": "", + "vendor": "Cisco", + "version": null, + "vpcDomain": 0, + "vrf": "management", + "vsanWwn": null, + "vsanWwnName": null, + "waitForSwitchModeChg": false, + "wwn": null + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_maintenance_mode_info.py b/tests/unit/module_utils/common/test_maintenance_mode_info.py index 96609c48f..679b09e6d 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode_info.py +++ b/tests/unit/module_utils/common/test_maintenance_mode_info.py @@ -193,31 +193,45 @@ def test_maintenance_mode_info_00120(maintenance_mode_info) -> None: [ ( "FabricDetailsByName", - "results", + "refresh", + ControllerResponseError, + ValueError, + "Bad controller response: fabric_details.refresh", + ), + ( + "FabricDetailsByName", + "results.setter", TypeError, ValueError, - "Bad type: fabric_details.results", + "Bad type: fabric_details.results.setter", ), ( "FabricDetailsByName", - "rest_send", + "rest_send.setter", TypeError, ValueError, - "Bad type: fabric_details.rest_send", + "Bad type: fabric_details.rest_send.setter", ), ( "SwitchDetails", - "results", + "refresh", + ControllerResponseError, + ValueError, + "Bad controller response: switch_details.refresh", + ), + ( + "SwitchDetails", + "results.setter", TypeError, ValueError, - "Bad type: switch_details.results", + "Bad type: switch_details.results.setter", ), ( "SwitchDetails", - "rest_send", + "rest_send.setter", TypeError, ValueError, - "Bad type: switch_details.rest_send", + "Bad type: switch_details.rest_send.setter", ), ], ) @@ -249,7 +263,7 @@ def test_maintenance_mode_info_00200( - ``SwitchDetails()`` is mocked to conditionally raise ``TypeError``. ### Code Flow - Test - - MaintenanceModeInfo().refresh() is called for each condition. + - ``refresh()`` is called. ### Expected Result - ``ValueError`` is raised. @@ -294,6 +308,235 @@ def responses(): instance.refresh() +@pytest.mark.parametrize( + "mock_class, mock_property, mock_exception, expected_exception, mock_message", + [ + ( + "SwitchDetails", + "serial_number.getter", + ValueError, + ValueError, + "serial_number.getter: ValueError", + ) + ], +) +def test_maintenance_mode_info_00210( + monkeypatch, + mock_class, + mock_property, + mock_exception, + expected_exception, + mock_message, +) -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + + ### Summary + - Verify ``refresh()`` raises ``ValueError`` when: + - ``switch_details.serial_number`` raises ``ValueError``. + + ### Code Flow - Setup + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + - ``FabricDetails()`` is mocked not to raise any exceptions. + - ``SwitchDetails()`` is mocked to conditionally raise ``ValueError``. + in the ``serial_number.getter`` property. + + ### Code Flow - Test + - ``refresh()`` is called. + + ### Expected Result + - ``ValueError`` is raised. + - Exception message matches expected. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = mock_sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + mock_fabric_details = MockFabricDetailsByName() + mock_fabric_details.mock_class = mock_class + mock_fabric_details.mock_exception = mock_exception + mock_fabric_details.mock_message = mock_message + mock_fabric_details.mock_property = mock_property + + mock_switch_details = MockSwitchDetails() + mock_switch_details.mock_class = mock_class + mock_switch_details.mock_exception = mock_exception + mock_switch_details.mock_message = mock_message + mock_switch_details.mock_property = mock_property + + monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) + monkeypatch.setattr(instance, "switch_details", mock_switch_details) + + with does_not_raise(): + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + + with pytest.raises(expected_exception, match=mock_message): + instance.refresh() + + +def test_maintenance_mode_info_00300( + monkeypatch, +) -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + + ### Summary + - Verify ``refresh()`` raises ``ValueError`` when: + ``switch_details.serial_number`` is ``None``. This happens + when the switch does not exist on the controller. + + ### Code Flow - Setup + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + - ``FabricDetails()`` is mocked not to raise any exceptions. + - ``SwitchDetails()`` is mocked not to raise any exceptions. + - ``responses_SwitchDetails.json`` contains a 200 response that + does not contain the switch ip address in CONFIG (192.168.1.2) + + ### Code Flow - Test + - ``refresh()`` is called. + + ### Expected Result + - ``ValueError`` is raised. + - Exception message matches expected. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + pass + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = mock_sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + mock_fabric_details = MockFabricDetailsByName() + mock_switch_details = MockSwitchDetails() + mock_switch_details.filter = CONFIG[0] + mock_switch_details.mock_response_key = key + + monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) + monkeypatch.setattr(instance, "switch_details", mock_switch_details) + + with does_not_raise(): + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + + match = r"MaintenanceModeInfo\.refresh:\s+" + match += r"Switch with ip_address 192\.168\.1\.2\s+" + match += r"does not exist on the controller\." + with pytest.raises(ValueError, match=match): + instance.refresh() + + +@pytest.mark.parametrize( + "mock_class, mock_property, mock_exception, expected_exception, mock_message", + [ + ( + "FabricDetailsByName", + "filter.setter", + ValueError, + ValueError, + "fabric_details.filter.setter: ValueError", + ) + ], +) +def test_maintenance_mode_info_00400( + monkeypatch, + mock_class, + mock_property, + mock_exception, + expected_exception, + mock_message, +) -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + + ### Summary + - Verify ``refresh()`` raises ``ValueError`` when: + - ``fabric_details.filter`` raises ``ValueError``. + + ### Code Flow - Setup + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + - ``FabricDetails().filter`` is mocked to conditionally raise ``ValueError``. + - ``SwitchDetails()`` is mocked not to raise any exceptions. + - ``responses_SwitchDetails.json`` contains a 200 response that + contains the switch ip address in CONFIG (192.168.1.2) + + ### Code Flow - Test + - ``refresh()`` is called. + + ### Expected Result + - ``ValueError`` is raised. + - Exception message matches expected. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + pass + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = mock_sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + mock_fabric_details = MockFabricDetailsByName() + mock_fabric_details.mock_class = mock_class + mock_fabric_details.mock_exception = mock_exception + mock_fabric_details.mock_message = mock_message + mock_fabric_details.mock_property = mock_property + + mock_switch_details = MockSwitchDetails() + mock_switch_details.filter = CONFIG[0] + mock_switch_details.mock_response_key = key + + monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) + monkeypatch.setattr(instance, "switch_details", mock_switch_details) + + with does_not_raise(): + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + + with pytest.raises(expected_exception, match=mock_message): + instance.refresh() + + # @pytest.mark.parametrize( # "mock_exception, expected_exception, mock_message", # [ From 588310939c6e11e69d6eda175ceee6dfdb705206 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 5 Jun 2024 19:11:12 -1000 Subject: [PATCH 132/230] MaintenanceModeInfo: 81% unit test coverage. MockFabricDetailsByName(): call from refresh() if mock_response_key isn't None. MockFabricDetailsByName(): add _get() method and mock_response_key property. MockSwitchDetails(): call from refresh() if mock_response_key isn't None. MockSwitchDetails().populate_mocked_properties(): remove. Replace with _get(). MockSwitchDetails() add ability to throw exception from any property. --- .../common/maintenance_mode_info.py | 2 +- .../unit/mocks/mock_fabric_details_by_name.py | 46 +- tests/unit/mocks/mock_switch_details.py | 83 +- .../unit/module_utils/common/common_utils.py | 10 + .../responses_FabricDetailsByName.json | 948 ++++++++++++++++++ .../fixtures/responses_SwitchDetails.json | 292 ++++++ .../common/test_maintenance_mode_info.py | 212 +++- 7 files changed, 1557 insertions(+), 36 deletions(-) create mode 100644 tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json diff --git a/plugins/module_utils/common/maintenance_mode_info.py b/plugins/module_utils/common/maintenance_mode_info.py index 4262c541e..af1656dca 100644 --- a/plugins/module_utils/common/maintenance_mode_info.py +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -325,7 +325,7 @@ def _get(self, item): def filter(self): """ ### Summary - Set the query filter. + Set the query filter (switch IP address) ### Raises None. However, if ``filter`` is not set, or ``filter`` is set to diff --git a/tests/unit/mocks/mock_fabric_details_by_name.py b/tests/unit/mocks/mock_fabric_details_by_name.py index 9fffcf983..afd136332 100644 --- a/tests/unit/mocks/mock_fabric_details_by_name.py +++ b/tests/unit/mocks/mock_fabric_details_by_name.py @@ -19,6 +19,9 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + responses_fabric_details_by_name + class MockFabricDetailsByName: """ @@ -35,17 +38,46 @@ def null_mock_exception(): self._mock_exception = null_mock_exception self._mock_message = None self._mock_property = None + self._mock_response_key = None + self._filter = None + self._info = {} + self.data_subclass = {} + self.response = None + self.response_data = None self._rest_send = None self._results = None self._is_read_only = None + def _get(self, key): + """ + Get the value of the key from the info dict. + """ + return self.data_subclass.get(self.filter, {}).get(key, None) + def refresh(self): """ Mocked refresh method """ if self.mock_class == self.class_name and self.mock_property == "refresh": raise self.mock_exception(self.mock_message) + if self.mock_response_key is None: + return + self.populate_info() + + def populate_info(self): + """ + Populate the info dict. + """ + self._info = {} + self.data_subclass = {} + self.response = responses_fabric_details_by_name(self.mock_response_key) + self.response_data = self.response.get("DATA", []) + for fabric in self.response_data: + nv_pairs = fabric.get("nvPairs", {}) + fabric_name = nv_pairs.get("FABRIC_NAME", None) + self._info[fabric_name] = nv_pairs + self.data_subclass[fabric_name] = nv_pairs @property def mock_class(self): @@ -91,6 +123,18 @@ def mock_property(self): def mock_property(self, value): self._mock_property = value + @property + def mock_response_key(self): + """ + The key used to extract controller response from the mocked response + in ``responses_FabricDetails.json``. + """ + return self._mock_response_key + + @mock_response_key.setter + def mock_response_key(self, value): + self._mock_response_key = value + @property def filter(self): """ @@ -153,4 +197,4 @@ def is_read_only(self): """ Mocked is_read_only property """ - return self._is_read_only + return self._get("IS_READ_ONLY") diff --git a/tests/unit/mocks/mock_switch_details.py b/tests/unit/mocks/mock_switch_details.py index f5768c366..72945b913 100644 --- a/tests/unit/mocks/mock_switch_details.py +++ b/tests/unit/mocks/mock_switch_details.py @@ -61,6 +61,9 @@ def refresh(self): """ if self.mock_class == self.class_name and self.mock_property == "refresh": raise self.mock_exception(self.mock_message) + if self.mock_response_key is None: + return + self.populate_info() def populate_info(self): """ @@ -72,29 +75,11 @@ def populate_info(self): for switch in self.response_data: self._info[switch["ipAddress"]] = switch - def populate_mocked_properties(self): + def _get(self, key): """ - Set the mocked property values from the contents of the mocked response. + Get the value of the key from the info dict. """ - if self.mock_response_key: - self.populate_info() - if self.filter is None: - raise ValueError( - "filter must be set before calling populate_mocked_properties()" - ) - - self.serial_number = self._info.get(self.filter, {}).get("serialNumber") - self.fabric_name = self._info.get(self.filter, {}).get("fabricName") - self.freeze_mode = self._info.get(self.filter, {}).get("freezeMode") - self.mode = self._info.get(self.filter, {}).get("mode") - self.system_mode = self._info.get(self.filter, {}).get("systemMode") - - if str(self.mode).lower() == "migration": - self.maintenance_mode = "migration" - elif str(self.mode).lower() != str(self.system_mode).lower(): - self.maintenance_mode = "inconsistent" - else: - self.maintenance_mode = self.mode + return self._info.get(self.filter, {}).get(key, None) @property def mock_class(self): @@ -169,7 +154,6 @@ def mock_response_key(self): @mock_response_key.setter def mock_response_key(self, value): self._mock_response_key = value - self.populate_mocked_properties() @property def rest_send(self): @@ -218,7 +202,7 @@ def fabric_name(self): """ Mocked fabric_name property """ - return self._fabric_name + return self._get("fabricName") @fabric_name.setter def fabric_name(self, value): @@ -229,7 +213,7 @@ def freeze_mode(self): """ Mocked freeze_mode property """ - return self._freeze_mode + return self._get("freezeMode") @freeze_mode.setter def freeze_mode(self, value): @@ -240,10 +224,27 @@ def maintenance_mode(self): """ Mocked maintenance_mode property """ - return self._maintenance_mode + if ( + self.mock_class == self.class_name + and self.mock_property == "maintenance_mode.getter" + ): + raise self.mock_exception(self.mock_message) + + mode = str(self._get("mode")).lower() + system_mode = str(self._get("systemMode")).lower() + if mode == "migration": + return "migration" + if mode != system_mode: + return "inconsistent" + return mode @maintenance_mode.setter def maintenance_mode(self, value): + if ( + self.mock_class == self.class_name + and self.mock_property == "maintenance_mode.setter" + ): + raise self.mock_exception(self.mock_message) self._maintenance_mode = value @property @@ -251,10 +252,14 @@ def mode(self): """ Mocked mode property """ - return self._mode + if self.mock_class == self.class_name and self.mock_property == "mode.getter": + raise self.mock_exception(self.mock_message) + return self._get("mode") @mode.setter def mode(self, value): + if self.mock_class == self.class_name and self.mock_property == "mode.setter": + raise self.mock_exception(self.mock_message) self._mode = value @property @@ -267,7 +272,7 @@ def serial_number(self): and self.mock_property == "serial_number.getter" ): raise self.mock_exception(self.mock_message) - return self._serial_number + return self._get("serialNumber") @serial_number.setter def serial_number(self, value): @@ -283,10 +288,20 @@ def switch_role(self): """ Mocked switch_role property """ - return self._switch_role + if ( + self.mock_class == self.class_name + and self.mock_property == "switch_role.getter" + ): + raise self.mock_exception(self.mock_message) + return self._get("switchRole") @switch_role.setter def switch_role(self, value): + if ( + self.mock_class == self.class_name + and self.mock_property == "switch_role.setter" + ): + raise self.mock_exception(self.mock_message) self._switch_role = value @property @@ -294,8 +309,18 @@ def system_mode(self): """ Mocked switch_role property """ - return self._system_mode + if ( + self.mock_class == self.class_name + and self.mock_property == "system_mode.getter" + ): + raise self.mock_exception(self.mock_message) + return self._get("systemMode") @system_mode.setter def system_mode(self, value): + if ( + self.mock_class == self.class_name + and self.mock_property == "system_mode.setter" + ): + raise self.mock_exception(self.mock_message) self._system_mode = value diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index 5a3df0cef..940e3c2c4 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -333,6 +333,16 @@ def responses_controller_version(key: str) -> Dict[str, str]: return response +def responses_fabric_details_by_name(key: str) -> Dict[str, str]: + """ + Return data in responses_FabricDetailsByName.json + """ + response_file = "responses_FabricDetailsByName" + response = load_fixture(response_file).get(key) + print(f"responses_fabric_details_by_name: {key} : {response}") + return response + + def responses_maintenance_mode(key: str) -> Dict[str, str]: """ Return data in responses_MaintenanceMode.json diff --git a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json new file mode 100644 index 000000000..d2de04bc8 --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json @@ -0,0 +1,948 @@ +{ + "test_maintenance_mode_info_00500a": { + "DATA": [ + { + "asn": "65000", + "createdOn": 1716345062044, + "deviceType": "n9k", + "fabricId": "FABRIC-2", + "fabricName": "VXLAN_Fabric", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN EVPN", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", + "id": 2, + "modifiedOn": 1716952430067, + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplate": "Default_Network_Universal", + "nvPairs": { + "AAA_REMOTE_IP_ENABLED": "false", + "AAA_SERVER_CONF": "", + "ACTIVE_MIGRATION": "false", + "ADVERTISE_PIP_BGP": "false", + "ADVERTISE_PIP_ON_BORDER": "true", + "AGENT_INTF": "eth0", + "AI_ML_QOS_POLICY": "AI_Fabric_QOS_400G", + "ALLOW_NXC": "true", + "ALLOW_NXC_PREV": "true", + "ANYCAST_BGW_ADVERTISE_PIP": "false", + "ANYCAST_GW_MAC": "2020.0000.00aa", + "ANYCAST_LB_ID": "", + "ANYCAST_RP_IP_RANGE": "10.254.254.0/24", + "ANYCAST_RP_IP_RANGE_INTERNAL": "10.254.254.0/24", + "AUTO_SYMMETRIC_DEFAULT_VRF": "false", + "AUTO_SYMMETRIC_VRF_LITE": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", + "AUTO_VRFLITE_IFC_DEFAULT_VRF": "false", + "BANNER": "", + "BFD_AUTH_ENABLE": "false", + "BFD_AUTH_KEY": "", + "BFD_AUTH_KEY_ID": "", + "BFD_ENABLE": "false", + "BFD_ENABLE_PREV": "false", + "BFD_IBGP_ENABLE": "false", + "BFD_ISIS_ENABLE": "false", + "BFD_OSPF_ENABLE": "false", + "BFD_PIM_ENABLE": "false", + "BGP_AS": "65000", + "BGP_AS_PREV": "65000", + "BGP_AUTH_ENABLE": "false", + "BGP_AUTH_KEY": "", + "BGP_AUTH_KEY_TYPE": "3", + "BGP_LB_ID": "0", + "BOOTSTRAP_CONF": "", + "BOOTSTRAP_ENABLE": "false", + "BOOTSTRAP_ENABLE_PREV": "false", + "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", + "BOOTSTRAP_MULTISUBNET_INTERNAL": "", + "BRFIELD_DEBUG_FLAG": "Disable", + "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", + "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", + "CDP_ENABLE": "false", + "COPP_POLICY": "strict", + "DCI_SUBNET_RANGE": "10.33.0.0/16", + "DCI_SUBNET_TARGET_MASK": "30", + "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", + "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", + "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", + "DEFAULT_VRF_REDIS_BGP_RMAP": "", + "DEPLOYMENT_FREEZE": "false", + "DHCP_ENABLE": "false", + "DHCP_END": "", + "DHCP_END_INTERNAL": "", + "DHCP_IPV6_ENABLE": "", + "DHCP_IPV6_ENABLE_INTERNAL": "", + "DHCP_START": "", + "DHCP_START_INTERNAL": "", + "DNS_SERVER_IP_LIST": "", + "DNS_SERVER_VRF": "", + "DOMAIN_NAME_INTERNAL": "", + "ENABLE_AAA": "false", + "ENABLE_AGENT": "false", + "ENABLE_AI_ML_QOS_POLICY": "false", + "ENABLE_AI_ML_QOS_POLICY_FLAP": "false", + "ENABLE_DEFAULT_QUEUING_POLICY": "false", + "ENABLE_EVPN": "true", + "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", + "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", + "ENABLE_L3VNI_NO_VLAN": "false", + "ENABLE_MACSEC": "false", + "ENABLE_NETFLOW": "false", + "ENABLE_NETFLOW_PREV": "false", + "ENABLE_NGOAM": "true", + "ENABLE_NXAPI": "true", + "ENABLE_NXAPI_HTTP": "true", + "ENABLE_PBR": "false", + "ENABLE_PVLAN": "false", + "ENABLE_PVLAN_PREV": "false", + "ENABLE_SGT": "false", + "ENABLE_SGT_PREV": "false", + "ENABLE_TENANT_DHCP": "true", + "ENABLE_TRM": "false", + "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", + "ESR_OPTION": "PBR", + "EXTRA_CONF_INTRA_LINKS": "", + "EXTRA_CONF_LEAF": "", + "EXTRA_CONF_SPINE": "", + "EXTRA_CONF_TOR": "", + "EXT_FABRIC_TYPE": "", + "FABRIC_INTERFACE_TYPE": "p2p", + "FABRIC_MTU": "9216", + "FABRIC_MTU_PREV": "9216", + "FABRIC_NAME": "VXLAN_Fabric", + "FABRIC_TYPE": "Switch_Fabric", + "FABRIC_VPC_DOMAIN_ID": "", + "FABRIC_VPC_DOMAIN_ID_PREV": "", + "FABRIC_VPC_QOS": "false", + "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", + "FEATURE_PTP": "false", + "FEATURE_PTP_INTERNAL": "false", + "FF": "Easy_Fabric", + "GRFIELD_DEBUG_FLAG": "Disable", + "HD_TIME": "180", + "HOST_INTF_ADMIN_STATE": "true", + "IBGP_PEER_TEMPLATE": "", + "IBGP_PEER_TEMPLATE_LEAF": "", + "INBAND_DHCP_SERVERS": "", + "INBAND_MGMT": "false", + "INBAND_MGMT_PREV": "false", + "ISIS_AREA_NUM": "0001", + "ISIS_AREA_NUM_PREV": "", + "ISIS_AUTH_ENABLE": "false", + "ISIS_AUTH_KEY": "", + "ISIS_AUTH_KEYCHAIN_KEY_ID": "", + "ISIS_AUTH_KEYCHAIN_NAME": "", + "ISIS_LEVEL": "level-2", + "ISIS_OVERLOAD_ELAPSE_TIME": "", + "ISIS_OVERLOAD_ENABLE": "false", + "ISIS_P2P_ENABLE": "false", + "L2_HOST_INTF_MTU": "9216", + "L2_HOST_INTF_MTU_PREV": "9216", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "L3VNI_MCAST_GROUP": "", + "L3_PARTITION_ID_RANGE": "50000-59000", + "LINK_STATE_ROUTING": "ospf", + "LINK_STATE_ROUTING_TAG": "UNDERLAY", + "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", + "LOOPBACK0_IPV6_RANGE": "", + "LOOPBACK0_IP_RANGE": "10.2.0.0/22", + "LOOPBACK1_IPV6_RANGE": "", + "LOOPBACK1_IP_RANGE": "10.3.0.0/22", + "MACSEC_ALGORITHM": "", + "MACSEC_CIPHER_SUITE": "", + "MACSEC_FALLBACK_ALGORITHM": "", + "MACSEC_FALLBACK_KEY_STRING": "", + "MACSEC_KEY_STRING": "", + "MACSEC_REPORT_TIMER": "", + "MGMT_GW": "", + "MGMT_GW_INTERNAL": "", + "MGMT_PREFIX": "", + "MGMT_PREFIX_INTERNAL": "", + "MGMT_V6PREFIX": "", + "MGMT_V6PREFIX_INTERNAL": "", + "MPLS_HANDOFF": "false", + "MPLS_ISIS_AREA_NUM": "0001", + "MPLS_ISIS_AREA_NUM_PREV": "", + "MPLS_LB_ID": "", + "MPLS_LOOPBACK_IP_RANGE": "", + "MSO_CONNECTIVITY_DEPLOYED": "", + "MSO_CONTROLER_ID": "", + "MSO_SITE_GROUP_NAME": "", + "MSO_SITE_ID": "", + "MST_INSTANCE_RANGE": "", + "MULTICAST_GROUP_SUBNET": "239.1.1.0/25", + "NETFLOW_EXPORTER_LIST": "", + "NETFLOW_MONITOR_LIST": "", + "NETFLOW_RECORD_LIST": "", + "NETWORK_VLAN_RANGE": "2300-2999", + "NTP_SERVER_IP_LIST": "", + "NTP_SERVER_VRF": "", + "NVE_LB_ID": "1", + "NXAPI_HTTPS_PORT": "443", + "NXAPI_HTTP_PORT": "80", + "NXC_DEST_VRF": "management", + "NXC_PROXY_PORT": "8080", + "NXC_PROXY_SERVER": "", + "NXC_SRC_INTF": "", + "OBJECT_TRACKING_NUMBER_RANGE": "100-299", + "OSPF_AREA_ID": "0.0.0.0", + "OSPF_AUTH_ENABLE": "false", + "OSPF_AUTH_KEY": "", + "OSPF_AUTH_KEY_ID": "", + "OVERLAY_MODE": "cli", + "OVERLAY_MODE_PREV": "cli", + "OVERWRITE_GLOBAL_NXC": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV": "false", + "PER_VRF_LOOPBACK_IP_RANGE": "", + "PER_VRF_LOOPBACK_IP_RANGE_V6": "fd00::a05:0/112", + "PHANTOM_RP_LB_ID1": "", + "PHANTOM_RP_LB_ID2": "", + "PHANTOM_RP_LB_ID3": "", + "PHANTOM_RP_LB_ID4": "", + "PIM_HELLO_AUTH_ENABLE": "false", + "PIM_HELLO_AUTH_KEY": "", + "PM_ENABLE": "false", + "PM_ENABLE_PREV": "false", + "POWER_REDUNDANCY_MODE": "ps-redundant", + "PREMSO_PARENT_FABRIC": "", + "PTP_DOMAIN_ID": "", + "PTP_LB_ID": "", + "REPLICATION_MODE": "Multicast", + "ROUTER_ID_RANGE": "", + "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", + "RP_COUNT": "2", + "RP_LB_ID": "254", + "RP_MODE": "asm", + "RR_COUNT": "2", + "SEED_SWITCH_CORE_INTERFACES": "", + "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", + "SGT_ID_RANGE": "", + "SGT_NAME_PREFIX": "", + "SGT_PREPROVISION": "false", + "SITE_ID": "65000", + "SLA_ID_RANGE": "10000-19999", + "SNMP_SERVER_HOST_TRAP": "true", + "SPINE_COUNT": "1", + "SPINE_SWITCH_CORE_INTERFACES": "", + "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", + "SSPINE_COUNT": "0", + "STATIC_UNDERLAY_IP_ALLOC": "false", + "STP_BRIDGE_PRIORITY": "", + "STP_ROOT_OPTION": "unmanaged", + "STP_VLAN_RANGE": "", + "STRICT_CC_MODE": "false", + "SUBINTERFACE_RANGE": "2-511", + "SUBNET_RANGE": "10.4.0.0/16", + "SUBNET_TARGET_MASK": "30", + "SYSLOG_SERVER_IP_LIST": "", + "SYSLOG_SERVER_VRF": "", + "SYSLOG_SEV": "", + "TCAM_ALLOCATION": "true", + "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", + "UNDERLAY_IS_V6": "false", + "UNNUM_BOOTSTRAP_LB_ID": "", + "UNNUM_DHCP_END": "", + "UNNUM_DHCP_END_INTERNAL": "", + "UNNUM_DHCP_START": "", + "UNNUM_DHCP_START_INTERNAL": "", + "UPGRADE_FROM_VERSION": "", + "USE_LINK_LOCAL": "false", + "V6_SUBNET_RANGE": "", + "V6_SUBNET_TARGET_MASK": "126", + "VPC_AUTO_RECOVERY_TIME": "360", + "VPC_DELAY_RESTORE": "150", + "VPC_DELAY_RESTORE_TIME": "60", + "VPC_DOMAIN_ID_RANGE": "1-1000", + "VPC_ENABLE_IPv6_ND_SYNC": "true", + "VPC_PEER_KEEP_ALIVE_OPTION": "management", + "VPC_PEER_LINK_PO": "500", + "VPC_PEER_LINK_VLAN": "3600", + "VRF_LITE_AUTOCONFIG": "Manual", + "VRF_VLAN_RANGE": "2000-2299", + "abstract_anycast_rp": "anycast_rp", + "abstract_bgp": "base_bgp", + "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", + "abstract_bgp_rr": "evpn_bgp_rr", + "abstract_dhcp": "base_dhcp", + "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", + "abstract_extra_config_leaf": "extra_config_leaf", + "abstract_extra_config_spine": "extra_config_spine", + "abstract_extra_config_tor": "extra_config_tor", + "abstract_feature_leaf": "base_feature_leaf_upg", + "abstract_feature_spine": "base_feature_spine_upg", + "abstract_isis": "base_isis_level2", + "abstract_isis_interface": "isis_interface", + "abstract_loopback_interface": "int_fabric_loopback_11_1", + "abstract_multicast": "base_multicast_11_1", + "abstract_ospf": "base_ospf", + "abstract_ospf_interface": "ospf_interface_11_1", + "abstract_pim_interface": "pim_interface", + "abstract_route_map": "route_map", + "abstract_routed_host": "int_routed_host", + "abstract_trunk_host": "int_trunk_host", + "abstract_vlan_interface": "int_fabric_vlan_11_1", + "abstract_vpc_domain": "base_vpc_domain_11_1", + "dcnmUser": "admin", + "default_network": "Default_Network_Universal", + "default_pvlan_sec_network": "", + "default_vrf": "Default_VRF_Universal", + "enableRealTimeBackup": "", + "enableScheduledBackup": "", + "network_extension_template": "Default_Network_Extension_Universal", + "scheduledTime": "", + "temp_anycast_gateway": "anycast_gateway", + "temp_vpc_domain_mgmt": "vpc_domain_mgmt", + "temp_vpc_peer_link": "int_vpc_peer_link_po", + "vrf_extension_template": "Default_VRF_Extension_Universal" + }, + "operStatus": "CRITICAL", + "provisionMode": "DCNMTopDown", + "replicationMode": "Multicast", + "siteId": "65000", + "templateName": "Easy_Fabric", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplate": "Default_VRF_Universal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00510a": { + "DATA": [ + { + "asn": "65000", + "createdOn": 1716345062044, + "deviceType": "n9k", + "fabricId": "FABRIC-2", + "fabricName": "VXLAN_Fabric", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN EVPN", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", + "id": 2, + "modifiedOn": 1716952430067, + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplate": "Default_Network_Universal", + "nvPairs": { + "AAA_REMOTE_IP_ENABLED": "false", + "AAA_SERVER_CONF": "", + "ACTIVE_MIGRATION": "false", + "ADVERTISE_PIP_BGP": "false", + "ADVERTISE_PIP_ON_BORDER": "true", + "AGENT_INTF": "eth0", + "AI_ML_QOS_POLICY": "AI_Fabric_QOS_400G", + "ALLOW_NXC": "true", + "ALLOW_NXC_PREV": "true", + "ANYCAST_BGW_ADVERTISE_PIP": "false", + "ANYCAST_GW_MAC": "2020.0000.00aa", + "ANYCAST_LB_ID": "", + "ANYCAST_RP_IP_RANGE": "10.254.254.0/24", + "ANYCAST_RP_IP_RANGE_INTERNAL": "10.254.254.0/24", + "AUTO_SYMMETRIC_DEFAULT_VRF": "false", + "AUTO_SYMMETRIC_VRF_LITE": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", + "AUTO_VRFLITE_IFC_DEFAULT_VRF": "false", + "BANNER": "", + "BFD_AUTH_ENABLE": "false", + "BFD_AUTH_KEY": "", + "BFD_AUTH_KEY_ID": "", + "BFD_ENABLE": "false", + "BFD_ENABLE_PREV": "false", + "BFD_IBGP_ENABLE": "false", + "BFD_ISIS_ENABLE": "false", + "BFD_OSPF_ENABLE": "false", + "BFD_PIM_ENABLE": "false", + "BGP_AS": "65000", + "BGP_AS_PREV": "65000", + "BGP_AUTH_ENABLE": "false", + "BGP_AUTH_KEY": "", + "BGP_AUTH_KEY_TYPE": "3", + "BGP_LB_ID": "0", + "BOOTSTRAP_CONF": "", + "BOOTSTRAP_ENABLE": "false", + "BOOTSTRAP_ENABLE_PREV": "false", + "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", + "BOOTSTRAP_MULTISUBNET_INTERNAL": "", + "BRFIELD_DEBUG_FLAG": "Disable", + "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", + "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", + "CDP_ENABLE": "false", + "COPP_POLICY": "strict", + "DCI_SUBNET_RANGE": "10.33.0.0/16", + "DCI_SUBNET_TARGET_MASK": "30", + "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", + "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", + "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", + "DEFAULT_VRF_REDIS_BGP_RMAP": "", + "DEPLOYMENT_FREEZE": "false", + "DHCP_ENABLE": "false", + "DHCP_END": "", + "DHCP_END_INTERNAL": "", + "DHCP_IPV6_ENABLE": "", + "DHCP_IPV6_ENABLE_INTERNAL": "", + "DHCP_START": "", + "DHCP_START_INTERNAL": "", + "DNS_SERVER_IP_LIST": "", + "DNS_SERVER_VRF": "", + "DOMAIN_NAME_INTERNAL": "", + "ENABLE_AAA": "false", + "ENABLE_AGENT": "false", + "ENABLE_AI_ML_QOS_POLICY": "false", + "ENABLE_AI_ML_QOS_POLICY_FLAP": "false", + "ENABLE_DEFAULT_QUEUING_POLICY": "false", + "ENABLE_EVPN": "true", + "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", + "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", + "ENABLE_L3VNI_NO_VLAN": "false", + "ENABLE_MACSEC": "false", + "ENABLE_NETFLOW": "false", + "ENABLE_NETFLOW_PREV": "false", + "ENABLE_NGOAM": "true", + "ENABLE_NXAPI": "true", + "ENABLE_NXAPI_HTTP": "true", + "ENABLE_PBR": "false", + "ENABLE_PVLAN": "false", + "ENABLE_PVLAN_PREV": "false", + "ENABLE_SGT": "false", + "ENABLE_SGT_PREV": "false", + "ENABLE_TENANT_DHCP": "true", + "ENABLE_TRM": "false", + "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", + "ESR_OPTION": "PBR", + "EXTRA_CONF_INTRA_LINKS": "", + "EXTRA_CONF_LEAF": "", + "EXTRA_CONF_SPINE": "", + "EXTRA_CONF_TOR": "", + "EXT_FABRIC_TYPE": "", + "FABRIC_INTERFACE_TYPE": "p2p", + "FABRIC_MTU": "9216", + "FABRIC_MTU_PREV": "9216", + "FABRIC_NAME": "VXLAN_Fabric", + "FABRIC_TYPE": "Switch_Fabric", + "FABRIC_VPC_DOMAIN_ID": "", + "FABRIC_VPC_DOMAIN_ID_PREV": "", + "FABRIC_VPC_QOS": "false", + "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", + "FEATURE_PTP": "false", + "FEATURE_PTP_INTERNAL": "false", + "FF": "Easy_Fabric", + "GRFIELD_DEBUG_FLAG": "Disable", + "HD_TIME": "180", + "HOST_INTF_ADMIN_STATE": "true", + "IBGP_PEER_TEMPLATE": "", + "IBGP_PEER_TEMPLATE_LEAF": "", + "INBAND_DHCP_SERVERS": "", + "INBAND_MGMT": "false", + "INBAND_MGMT_PREV": "false", + "ISIS_AREA_NUM": "0001", + "ISIS_AREA_NUM_PREV": "", + "ISIS_AUTH_ENABLE": "false", + "ISIS_AUTH_KEY": "", + "ISIS_AUTH_KEYCHAIN_KEY_ID": "", + "ISIS_AUTH_KEYCHAIN_NAME": "", + "ISIS_LEVEL": "level-2", + "ISIS_OVERLOAD_ELAPSE_TIME": "", + "ISIS_OVERLOAD_ENABLE": "false", + "ISIS_P2P_ENABLE": "false", + "L2_HOST_INTF_MTU": "9216", + "L2_HOST_INTF_MTU_PREV": "9216", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "L3VNI_MCAST_GROUP": "", + "L3_PARTITION_ID_RANGE": "50000-59000", + "LINK_STATE_ROUTING": "ospf", + "LINK_STATE_ROUTING_TAG": "UNDERLAY", + "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", + "LOOPBACK0_IPV6_RANGE": "", + "LOOPBACK0_IP_RANGE": "10.2.0.0/22", + "LOOPBACK1_IPV6_RANGE": "", + "LOOPBACK1_IP_RANGE": "10.3.0.0/22", + "MACSEC_ALGORITHM": "", + "MACSEC_CIPHER_SUITE": "", + "MACSEC_FALLBACK_ALGORITHM": "", + "MACSEC_FALLBACK_KEY_STRING": "", + "MACSEC_KEY_STRING": "", + "MACSEC_REPORT_TIMER": "", + "MGMT_GW": "", + "MGMT_GW_INTERNAL": "", + "MGMT_PREFIX": "", + "MGMT_PREFIX_INTERNAL": "", + "MGMT_V6PREFIX": "", + "MGMT_V6PREFIX_INTERNAL": "", + "MPLS_HANDOFF": "false", + "MPLS_ISIS_AREA_NUM": "0001", + "MPLS_ISIS_AREA_NUM_PREV": "", + "MPLS_LB_ID": "", + "MPLS_LOOPBACK_IP_RANGE": "", + "MSO_CONNECTIVITY_DEPLOYED": "", + "MSO_CONTROLER_ID": "", + "MSO_SITE_GROUP_NAME": "", + "MSO_SITE_ID": "", + "MST_INSTANCE_RANGE": "", + "MULTICAST_GROUP_SUBNET": "239.1.1.0/25", + "NETFLOW_EXPORTER_LIST": "", + "NETFLOW_MONITOR_LIST": "", + "NETFLOW_RECORD_LIST": "", + "NETWORK_VLAN_RANGE": "2300-2999", + "NTP_SERVER_IP_LIST": "", + "NTP_SERVER_VRF": "", + "NVE_LB_ID": "1", + "NXAPI_HTTPS_PORT": "443", + "NXAPI_HTTP_PORT": "80", + "NXC_DEST_VRF": "management", + "NXC_PROXY_PORT": "8080", + "NXC_PROXY_SERVER": "", + "NXC_SRC_INTF": "", + "OBJECT_TRACKING_NUMBER_RANGE": "100-299", + "OSPF_AREA_ID": "0.0.0.0", + "OSPF_AUTH_ENABLE": "false", + "OSPF_AUTH_KEY": "", + "OSPF_AUTH_KEY_ID": "", + "OVERLAY_MODE": "cli", + "OVERLAY_MODE_PREV": "cli", + "OVERWRITE_GLOBAL_NXC": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV": "false", + "PER_VRF_LOOPBACK_IP_RANGE": "", + "PER_VRF_LOOPBACK_IP_RANGE_V6": "fd00::a05:0/112", + "PHANTOM_RP_LB_ID1": "", + "PHANTOM_RP_LB_ID2": "", + "PHANTOM_RP_LB_ID3": "", + "PHANTOM_RP_LB_ID4": "", + "PIM_HELLO_AUTH_ENABLE": "false", + "PIM_HELLO_AUTH_KEY": "", + "PM_ENABLE": "false", + "PM_ENABLE_PREV": "false", + "POWER_REDUNDANCY_MODE": "ps-redundant", + "PREMSO_PARENT_FABRIC": "", + "PTP_DOMAIN_ID": "", + "PTP_LB_ID": "", + "REPLICATION_MODE": "Multicast", + "ROUTER_ID_RANGE": "", + "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", + "RP_COUNT": "2", + "RP_LB_ID": "254", + "RP_MODE": "asm", + "RR_COUNT": "2", + "SEED_SWITCH_CORE_INTERFACES": "", + "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", + "SGT_ID_RANGE": "", + "SGT_NAME_PREFIX": "", + "SGT_PREPROVISION": "false", + "SITE_ID": "65000", + "SLA_ID_RANGE": "10000-19999", + "SNMP_SERVER_HOST_TRAP": "true", + "SPINE_COUNT": "1", + "SPINE_SWITCH_CORE_INTERFACES": "", + "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", + "SSPINE_COUNT": "0", + "STATIC_UNDERLAY_IP_ALLOC": "false", + "STP_BRIDGE_PRIORITY": "", + "STP_ROOT_OPTION": "unmanaged", + "STP_VLAN_RANGE": "", + "STRICT_CC_MODE": "false", + "SUBINTERFACE_RANGE": "2-511", + "SUBNET_RANGE": "10.4.0.0/16", + "SUBNET_TARGET_MASK": "30", + "SYSLOG_SERVER_IP_LIST": "", + "SYSLOG_SERVER_VRF": "", + "SYSLOG_SEV": "", + "TCAM_ALLOCATION": "true", + "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", + "UNDERLAY_IS_V6": "false", + "UNNUM_BOOTSTRAP_LB_ID": "", + "UNNUM_DHCP_END": "", + "UNNUM_DHCP_END_INTERNAL": "", + "UNNUM_DHCP_START": "", + "UNNUM_DHCP_START_INTERNAL": "", + "UPGRADE_FROM_VERSION": "", + "USE_LINK_LOCAL": "false", + "V6_SUBNET_RANGE": "", + "V6_SUBNET_TARGET_MASK": "126", + "VPC_AUTO_RECOVERY_TIME": "360", + "VPC_DELAY_RESTORE": "150", + "VPC_DELAY_RESTORE_TIME": "60", + "VPC_DOMAIN_ID_RANGE": "1-1000", + "VPC_ENABLE_IPv6_ND_SYNC": "true", + "VPC_PEER_KEEP_ALIVE_OPTION": "management", + "VPC_PEER_LINK_PO": "500", + "VPC_PEER_LINK_VLAN": "3600", + "VRF_LITE_AUTOCONFIG": "Manual", + "VRF_VLAN_RANGE": "2000-2299", + "abstract_anycast_rp": "anycast_rp", + "abstract_bgp": "base_bgp", + "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", + "abstract_bgp_rr": "evpn_bgp_rr", + "abstract_dhcp": "base_dhcp", + "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", + "abstract_extra_config_leaf": "extra_config_leaf", + "abstract_extra_config_spine": "extra_config_spine", + "abstract_extra_config_tor": "extra_config_tor", + "abstract_feature_leaf": "base_feature_leaf_upg", + "abstract_feature_spine": "base_feature_spine_upg", + "abstract_isis": "base_isis_level2", + "abstract_isis_interface": "isis_interface", + "abstract_loopback_interface": "int_fabric_loopback_11_1", + "abstract_multicast": "base_multicast_11_1", + "abstract_ospf": "base_ospf", + "abstract_ospf_interface": "ospf_interface_11_1", + "abstract_pim_interface": "pim_interface", + "abstract_route_map": "route_map", + "abstract_routed_host": "int_routed_host", + "abstract_trunk_host": "int_trunk_host", + "abstract_vlan_interface": "int_fabric_vlan_11_1", + "abstract_vpc_domain": "base_vpc_domain_11_1", + "dcnmUser": "admin", + "default_network": "Default_Network_Universal", + "default_pvlan_sec_network": "", + "default_vrf": "Default_VRF_Universal", + "enableRealTimeBackup": "", + "enableScheduledBackup": "", + "network_extension_template": "Default_Network_Extension_Universal", + "scheduledTime": "", + "temp_anycast_gateway": "anycast_gateway", + "temp_vpc_domain_mgmt": "vpc_domain_mgmt", + "temp_vpc_peer_link": "int_vpc_peer_link_po", + "vrf_extension_template": "Default_VRF_Extension_Universal" + }, + "operStatus": "CRITICAL", + "provisionMode": "DCNMTopDown", + "replicationMode": "Multicast", + "siteId": "65000", + "templateName": "Easy_Fabric", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplate": "Default_VRF_Universal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00520a": { + "TEST_NOTES": [ + "IS_READ_ONLY is True" + ], + "DATA": [ + { + "asn": "65000", + "createdOn": 1716345062044, + "deviceType": "n9k", + "fabricId": "FABRIC-2", + "fabricName": "VXLAN_Fabric", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN EVPN", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", + "id": 2, + "modifiedOn": 1716952430067, + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplate": "Default_Network_Universal", + "nvPairs": { + "AAA_REMOTE_IP_ENABLED": "false", + "AAA_SERVER_CONF": "", + "ACTIVE_MIGRATION": "false", + "ADVERTISE_PIP_BGP": "false", + "ADVERTISE_PIP_ON_BORDER": "true", + "AGENT_INTF": "eth0", + "AI_ML_QOS_POLICY": "AI_Fabric_QOS_400G", + "ALLOW_NXC": "true", + "ALLOW_NXC_PREV": "true", + "ANYCAST_BGW_ADVERTISE_PIP": "false", + "ANYCAST_GW_MAC": "2020.0000.00aa", + "ANYCAST_LB_ID": "", + "ANYCAST_RP_IP_RANGE": "10.254.254.0/24", + "ANYCAST_RP_IP_RANGE_INTERNAL": "10.254.254.0/24", + "AUTO_SYMMETRIC_DEFAULT_VRF": "false", + "AUTO_SYMMETRIC_VRF_LITE": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", + "AUTO_VRFLITE_IFC_DEFAULT_VRF": "false", + "BANNER": "", + "BFD_AUTH_ENABLE": "false", + "BFD_AUTH_KEY": "", + "BFD_AUTH_KEY_ID": "", + "BFD_ENABLE": "false", + "BFD_ENABLE_PREV": "false", + "BFD_IBGP_ENABLE": "false", + "BFD_ISIS_ENABLE": "false", + "BFD_OSPF_ENABLE": "false", + "BFD_PIM_ENABLE": "false", + "BGP_AS": "65000", + "BGP_AS_PREV": "65000", + "BGP_AUTH_ENABLE": "false", + "BGP_AUTH_KEY": "", + "BGP_AUTH_KEY_TYPE": "3", + "BGP_LB_ID": "0", + "BOOTSTRAP_CONF": "", + "BOOTSTRAP_ENABLE": "false", + "BOOTSTRAP_ENABLE_PREV": "false", + "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", + "BOOTSTRAP_MULTISUBNET_INTERNAL": "", + "BRFIELD_DEBUG_FLAG": "Disable", + "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", + "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", + "CDP_ENABLE": "false", + "COPP_POLICY": "strict", + "DCI_SUBNET_RANGE": "10.33.0.0/16", + "DCI_SUBNET_TARGET_MASK": "30", + "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", + "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", + "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", + "DEFAULT_VRF_REDIS_BGP_RMAP": "", + "DEPLOYMENT_FREEZE": "true", + "DHCP_ENABLE": "false", + "DHCP_END": "", + "DHCP_END_INTERNAL": "", + "DHCP_IPV6_ENABLE": "", + "DHCP_IPV6_ENABLE_INTERNAL": "", + "DHCP_START": "", + "DHCP_START_INTERNAL": "", + "DNS_SERVER_IP_LIST": "", + "DNS_SERVER_VRF": "", + "DOMAIN_NAME_INTERNAL": "", + "ENABLE_AAA": "false", + "ENABLE_AGENT": "false", + "ENABLE_AI_ML_QOS_POLICY": "false", + "ENABLE_AI_ML_QOS_POLICY_FLAP": "false", + "ENABLE_DEFAULT_QUEUING_POLICY": "false", + "ENABLE_EVPN": "true", + "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", + "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", + "ENABLE_L3VNI_NO_VLAN": "false", + "ENABLE_MACSEC": "false", + "ENABLE_NETFLOW": "false", + "ENABLE_NETFLOW_PREV": "false", + "ENABLE_NGOAM": "true", + "ENABLE_NXAPI": "true", + "ENABLE_NXAPI_HTTP": "true", + "ENABLE_PBR": "false", + "ENABLE_PVLAN": "false", + "ENABLE_PVLAN_PREV": "false", + "ENABLE_SGT": "false", + "ENABLE_SGT_PREV": "false", + "ENABLE_TENANT_DHCP": "true", + "ENABLE_TRM": "false", + "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", + "ESR_OPTION": "PBR", + "EXTRA_CONF_INTRA_LINKS": "", + "EXTRA_CONF_LEAF": "", + "EXTRA_CONF_SPINE": "", + "EXTRA_CONF_TOR": "", + "EXT_FABRIC_TYPE": "", + "FABRIC_INTERFACE_TYPE": "p2p", + "FABRIC_MTU": "9216", + "FABRIC_MTU_PREV": "9216", + "FABRIC_NAME": "VXLAN_Fabric", + "FABRIC_TYPE": "Switch_Fabric", + "FABRIC_VPC_DOMAIN_ID": "", + "FABRIC_VPC_DOMAIN_ID_PREV": "", + "FABRIC_VPC_QOS": "false", + "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", + "FEATURE_PTP": "false", + "FEATURE_PTP_INTERNAL": "false", + "FF": "Easy_Fabric", + "GRFIELD_DEBUG_FLAG": "Disable", + "HD_TIME": "180", + "HOST_INTF_ADMIN_STATE": "true", + "IBGP_PEER_TEMPLATE": "", + "IBGP_PEER_TEMPLATE_LEAF": "", + "INBAND_DHCP_SERVERS": "", + "INBAND_MGMT": "false", + "INBAND_MGMT_PREV": "false", + "IS_READ_ONLY": "true", + "ISIS_AREA_NUM": "0001", + "ISIS_AREA_NUM_PREV": "", + "ISIS_AUTH_ENABLE": "false", + "ISIS_AUTH_KEY": "", + "ISIS_AUTH_KEYCHAIN_KEY_ID": "", + "ISIS_AUTH_KEYCHAIN_NAME": "", + "ISIS_LEVEL": "level-2", + "ISIS_OVERLOAD_ELAPSE_TIME": "", + "ISIS_OVERLOAD_ENABLE": "false", + "ISIS_P2P_ENABLE": "false", + "L2_HOST_INTF_MTU": "9216", + "L2_HOST_INTF_MTU_PREV": "9216", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "L3VNI_MCAST_GROUP": "", + "L3_PARTITION_ID_RANGE": "50000-59000", + "LINK_STATE_ROUTING": "ospf", + "LINK_STATE_ROUTING_TAG": "UNDERLAY", + "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", + "LOOPBACK0_IPV6_RANGE": "", + "LOOPBACK0_IP_RANGE": "10.2.0.0/22", + "LOOPBACK1_IPV6_RANGE": "", + "LOOPBACK1_IP_RANGE": "10.3.0.0/22", + "MACSEC_ALGORITHM": "", + "MACSEC_CIPHER_SUITE": "", + "MACSEC_FALLBACK_ALGORITHM": "", + "MACSEC_FALLBACK_KEY_STRING": "", + "MACSEC_KEY_STRING": "", + "MACSEC_REPORT_TIMER": "", + "MGMT_GW": "", + "MGMT_GW_INTERNAL": "", + "MGMT_PREFIX": "", + "MGMT_PREFIX_INTERNAL": "", + "MGMT_V6PREFIX": "", + "MGMT_V6PREFIX_INTERNAL": "", + "MPLS_HANDOFF": "false", + "MPLS_ISIS_AREA_NUM": "0001", + "MPLS_ISIS_AREA_NUM_PREV": "", + "MPLS_LB_ID": "", + "MPLS_LOOPBACK_IP_RANGE": "", + "MSO_CONNECTIVITY_DEPLOYED": "", + "MSO_CONTROLER_ID": "", + "MSO_SITE_GROUP_NAME": "", + "MSO_SITE_ID": "", + "MST_INSTANCE_RANGE": "", + "MULTICAST_GROUP_SUBNET": "239.1.1.0/25", + "NETFLOW_EXPORTER_LIST": "", + "NETFLOW_MONITOR_LIST": "", + "NETFLOW_RECORD_LIST": "", + "NETWORK_VLAN_RANGE": "2300-2999", + "NTP_SERVER_IP_LIST": "", + "NTP_SERVER_VRF": "", + "NVE_LB_ID": "1", + "NXAPI_HTTPS_PORT": "443", + "NXAPI_HTTP_PORT": "80", + "NXC_DEST_VRF": "management", + "NXC_PROXY_PORT": "8080", + "NXC_PROXY_SERVER": "", + "NXC_SRC_INTF": "", + "OBJECT_TRACKING_NUMBER_RANGE": "100-299", + "OSPF_AREA_ID": "0.0.0.0", + "OSPF_AUTH_ENABLE": "false", + "OSPF_AUTH_KEY": "", + "OSPF_AUTH_KEY_ID": "", + "OVERLAY_MODE": "cli", + "OVERLAY_MODE_PREV": "cli", + "OVERWRITE_GLOBAL_NXC": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV": "false", + "PER_VRF_LOOPBACK_IP_RANGE": "", + "PER_VRF_LOOPBACK_IP_RANGE_V6": "fd00::a05:0/112", + "PHANTOM_RP_LB_ID1": "", + "PHANTOM_RP_LB_ID2": "", + "PHANTOM_RP_LB_ID3": "", + "PHANTOM_RP_LB_ID4": "", + "PIM_HELLO_AUTH_ENABLE": "false", + "PIM_HELLO_AUTH_KEY": "", + "PM_ENABLE": "false", + "PM_ENABLE_PREV": "false", + "POWER_REDUNDANCY_MODE": "ps-redundant", + "PREMSO_PARENT_FABRIC": "", + "PTP_DOMAIN_ID": "", + "PTP_LB_ID": "", + "REPLICATION_MODE": "Multicast", + "ROUTER_ID_RANGE": "", + "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", + "RP_COUNT": "2", + "RP_LB_ID": "254", + "RP_MODE": "asm", + "RR_COUNT": "2", + "SEED_SWITCH_CORE_INTERFACES": "", + "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", + "SGT_ID_RANGE": "", + "SGT_NAME_PREFIX": "", + "SGT_PREPROVISION": "false", + "SITE_ID": "65000", + "SLA_ID_RANGE": "10000-19999", + "SNMP_SERVER_HOST_TRAP": "true", + "SPINE_COUNT": "1", + "SPINE_SWITCH_CORE_INTERFACES": "", + "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", + "SSPINE_COUNT": "0", + "STATIC_UNDERLAY_IP_ALLOC": "false", + "STP_BRIDGE_PRIORITY": "", + "STP_ROOT_OPTION": "unmanaged", + "STP_VLAN_RANGE": "", + "STRICT_CC_MODE": "false", + "SUBINTERFACE_RANGE": "2-511", + "SUBNET_RANGE": "10.4.0.0/16", + "SUBNET_TARGET_MASK": "30", + "SYSLOG_SERVER_IP_LIST": "", + "SYSLOG_SERVER_VRF": "", + "SYSLOG_SEV": "", + "TCAM_ALLOCATION": "true", + "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", + "UNDERLAY_IS_V6": "false", + "UNNUM_BOOTSTRAP_LB_ID": "", + "UNNUM_DHCP_END": "", + "UNNUM_DHCP_END_INTERNAL": "", + "UNNUM_DHCP_START": "", + "UNNUM_DHCP_START_INTERNAL": "", + "UPGRADE_FROM_VERSION": "", + "USE_LINK_LOCAL": "false", + "V6_SUBNET_RANGE": "", + "V6_SUBNET_TARGET_MASK": "126", + "VPC_AUTO_RECOVERY_TIME": "360", + "VPC_DELAY_RESTORE": "150", + "VPC_DELAY_RESTORE_TIME": "60", + "VPC_DOMAIN_ID_RANGE": "1-1000", + "VPC_ENABLE_IPv6_ND_SYNC": "true", + "VPC_PEER_KEEP_ALIVE_OPTION": "management", + "VPC_PEER_LINK_PO": "500", + "VPC_PEER_LINK_VLAN": "3600", + "VRF_LITE_AUTOCONFIG": "Manual", + "VRF_VLAN_RANGE": "2000-2299", + "abstract_anycast_rp": "anycast_rp", + "abstract_bgp": "base_bgp", + "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", + "abstract_bgp_rr": "evpn_bgp_rr", + "abstract_dhcp": "base_dhcp", + "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", + "abstract_extra_config_leaf": "extra_config_leaf", + "abstract_extra_config_spine": "extra_config_spine", + "abstract_extra_config_tor": "extra_config_tor", + "abstract_feature_leaf": "base_feature_leaf_upg", + "abstract_feature_spine": "base_feature_spine_upg", + "abstract_isis": "base_isis_level2", + "abstract_isis_interface": "isis_interface", + "abstract_loopback_interface": "int_fabric_loopback_11_1", + "abstract_multicast": "base_multicast_11_1", + "abstract_ospf": "base_ospf", + "abstract_ospf_interface": "ospf_interface_11_1", + "abstract_pim_interface": "pim_interface", + "abstract_route_map": "route_map", + "abstract_routed_host": "int_routed_host", + "abstract_trunk_host": "int_trunk_host", + "abstract_vlan_interface": "int_fabric_vlan_11_1", + "abstract_vpc_domain": "base_vpc_domain_11_1", + "dcnmUser": "admin", + "default_network": "Default_Network_Universal", + "default_pvlan_sec_network": "", + "default_vrf": "Default_VRF_Universal", + "enableRealTimeBackup": "", + "enableScheduledBackup": "", + "network_extension_template": "Default_Network_Extension_Universal", + "scheduledTime": "", + "temp_anycast_gateway": "anycast_gateway", + "temp_vpc_domain_mgmt": "vpc_domain_mgmt", + "temp_vpc_peer_link": "int_vpc_peer_link_po", + "vrf_extension_template": "Default_VRF_Extension_Universal" + }, + "operStatus": "CRITICAL", + "provisionMode": "DCNMTopDown", + "replicationMode": "Multicast", + "siteId": "65000", + "templateName": "Easy_Fabric", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplate": "Default_VRF_Universal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + } +} \ No newline at end of file diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json index 2dbaf014e..5e7ae971a 100644 --- a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -373,5 +373,297 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00500a": { + "TEST_NOTES": [ + "DATA contains switch with ip address 192.168.1.2", + "fabricName is VXLAN_Fabric", + "freezeMode is null", + "switchRole is leaf", + "serialNumber is FDO211218FV" + ], + "DATA": [ + { + "activeSupSlot": 0, + "availPorts": 0, + "ccStatus": "NA", + "cfsSyslogStatus": 1, + "colDBId": 0, + "connUnitStatus": 0, + "consistencyState": false, + "contact": null, + "cpuUsage": 0, + "deviceType": "External", + "displayHdrs": null, + "displayValues": null, + "domain": null, + "domainID": 0, + "elementType": null, + "fabricId": 3, + "fabricName": "VXLAN_Fabric", + "fabricTechnology": "VXLANFabric", + "fcoeEnabled": false, + "fex": false, + "fexMap": {}, + "fid": 0, + "freezeMode": null, + "health": -1, + "hostName": "cvd-1314-leaf", + "index": 0, + "intentedpeerName": "", + "interfaces": null, + "ipAddress": "192.168.1.2", + "ipDomain": "", + "isEchSupport": false, + "isLan": false, + "isNonNexus": false, + "isPmCollect": false, + "isTrapDelayed": false, + "isVpcConfigured": false, + "is_smlic_enabled": false, + "keepAliveState": null, + "lastScanTime": 0, + "licenseDetail": null, + "licenseViolation": false, + "linkName": null, + "location": null, + "logicalName": "cvd-1314-leaf", + "managable": true, + "mds": false, + "membership": null, + "memoryUsage": 0, + "mgmtAddress": null, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "moduleIndexOffset": 9999, + "modules": null, + "monitorMode": true, + "name": null, + "network": null, + "nonMdsModel": null, + "npvEnabled": false, + "numberOfPorts": 0, + "operMode": null, + "operStatus": "Minor", + "peer": null, + "peerSerialNumber": null, + "peerSwitchDbId": 0, + "peerlinkState": null, + "ports": 0, + "present": true, + "primaryIP": "", + "primarySwitchDbID": 0, + "principal": null, + "protoDiscSettings": null, + "recvIntf": null, + "release": "10.2(5)", + "role": null, + "sanAnalyticsCapable": false, + "scope": null, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "sendIntf": null, + "serialNumber": "FDO123456FV", + "sourceInterface": "mgmt0", + "sourceVrf": "management", + "standbySupState": 0, + "status": "ok", + "swType": null, + "swUUID": "DCNM-UUID-132770", + "swUUIDId": 132770, + "swWwn": null, + "swWwnName": null, + "switchDbID": 502030, + "switchRole": "leaf", + "switchRoleEnum": "Leaf", + "sysDescr": "", + "systemMode": "Normal", + "uid": 0, + "unmanagableCause": "", + "upTime": 0, + "upTimeNumber": 0, + "upTimeStr": "98 days, 21:55:52", + "usedPorts": 0, + "username": null, + "vdcId": 0, + "vdcMac": null, + "vdcName": "", + "vendor": "Cisco", + "version": null, + "vpcDomain": 0, + "vrf": "management", + "vsanWwn": null, + "vsanWwnName": null, + "waitForSwitchModeChg": false, + "wwn": null + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00510a": { + "TEST_NOTES": [ + "DATA contains switch with ip address 192.168.1.2", + "fabricName is VXLAN_Fabric", + "freezeMode is true", + "switchRole is leaf", + "serialNumber is FDO211218FV" + ], + "DATA": [ + { + "activeSupSlot": 0, + "availPorts": 0, + "ccStatus": "NA", + "cfsSyslogStatus": 1, + "colDBId": 0, + "connUnitStatus": 0, + "consistencyState": false, + "contact": null, + "cpuUsage": 0, + "deviceType": "External", + "displayHdrs": null, + "displayValues": null, + "domain": null, + "domainID": 0, + "elementType": null, + "fabricId": 3, + "fabricName": "VXLAN_Fabric", + "fabricTechnology": "VXLANFabric", + "fcoeEnabled": false, + "fex": false, + "fexMap": {}, + "fid": 0, + "freezeMode": true, + "health": -1, + "hostName": "cvd-1314-leaf", + "index": 0, + "intentedpeerName": "", + "interfaces": null, + "ipAddress": "192.168.1.2", + "ipDomain": "", + "isEchSupport": false, + "isLan": false, + "isNonNexus": false, + "isPmCollect": false, + "isTrapDelayed": false, + "isVpcConfigured": false, + "is_smlic_enabled": false, + "keepAliveState": null, + "lastScanTime": 0, + "licenseDetail": null, + "licenseViolation": false, + "linkName": null, + "location": null, + "logicalName": "cvd-1314-leaf", + "managable": true, + "mds": false, + "membership": null, + "memoryUsage": 0, + "mgmtAddress": null, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "modelType": 0, + "moduleIndexOffset": 9999, + "modules": null, + "monitorMode": true, + "name": null, + "network": null, + "nonMdsModel": null, + "npvEnabled": false, + "numberOfPorts": 0, + "operMode": null, + "operStatus": "Minor", + "peer": null, + "peerSerialNumber": null, + "peerSwitchDbId": 0, + "peerlinkState": null, + "ports": 0, + "present": true, + "primaryIP": "", + "primarySwitchDbID": 0, + "principal": null, + "protoDiscSettings": null, + "recvIntf": null, + "release": "10.2(5)", + "role": null, + "sanAnalyticsCapable": false, + "scope": null, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "sendIntf": null, + "serialNumber": "FDO123456FV", + "sourceInterface": "mgmt0", + "sourceVrf": "management", + "standbySupState": 0, + "status": "ok", + "swType": null, + "swUUID": "DCNM-UUID-132770", + "swUUIDId": 132770, + "swWwn": null, + "swWwnName": null, + "switchDbID": 502030, + "switchRole": "leaf", + "switchRoleEnum": "Leaf", + "sysDescr": "", + "systemMode": "Normal", + "uid": 0, + "unmanagableCause": "", + "upTime": 0, + "upTimeNumber": 0, + "upTimeStr": "98 days, 21:55:52", + "usedPorts": 0, + "username": null, + "vdcId": 0, + "vdcMac": null, + "vdcName": "", + "vendor": "Cisco", + "version": null, + "vpcDomain": 0, + "vrf": "management", + "vsanWwn": null, + "vsanWwnName": null, + "waitForSwitchModeChg": false, + "wwn": null + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00520a": { + "TEST_NOTES": [ + "DATA contains switch with ip address 192.168.1.2", + "fabricName is VXLAN_Fabric", + "freezeMode is true", + "switchRole is leaf", + "serialNumber is FDO211218FV" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "freezeMode": true, + "hostName": "cvd-1314-leaf", + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1314-leaf", + "managable": true, + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "operStatus": "Minor", + "present": true, + "release": "10.2(5)", + "role": null, + "serialNumber": "FDO123456FV", + "status": "ok", + "switchRole": "leaf" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_maintenance_mode_info.py b/tests/unit/module_utils/common/test_maintenance_mode_info.py index 679b09e6d..6e7bd92d8 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode_info.py +++ b/tests/unit/module_utils/common/test_maintenance_mode_info.py @@ -50,7 +50,8 @@ MockSwitchDetails from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( MockSender, ResponseGenerator, does_not_raise, - maintenance_mode_info_fixture, responses_switch_details) + maintenance_mode_info_fixture, responses_fabric_details_by_name, + responses_switch_details) FABRIC_NAME = "VXLAN_Fabric" CONFIG = ["192.168.1.2"] @@ -368,10 +369,6 @@ def responses(): instance = MaintenanceModeInfo(PARAMS) mock_fabric_details = MockFabricDetailsByName() - mock_fabric_details.mock_class = mock_class - mock_fabric_details.mock_exception = mock_exception - mock_fabric_details.mock_message = mock_message - mock_fabric_details.mock_property = mock_property mock_switch_details = MockSwitchDetails() mock_switch_details.mock_class = mock_class @@ -537,6 +534,211 @@ def responses(): instance.refresh() +def test_maintenance_mode_info_00500(monkeypatch) -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + + ### Summary + - Verify happy path with freezeMode == False + + ### Code Flow - Setup + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + - ``FabricDetails()`` is mocked not to raise any exceptions. + - ``SwitchDetails()`` is mocked not to raise any exceptions. + - ``responses_SwitchDetails.json`` contains a 200 response that + contains the switch ip address in CONFIG (192.168.1.2) + - ``responses_FabricDetailsByName.json`` contains a 200 response that + contains FABRIC_NAME. + + ### Code Flow - Test + - ``refresh()`` is called. + + ### Expected Result + - Exception is not raised. + - ``MaintenanceModeInfo().results`` contains expected data. + + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_by_name(key) + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = mock_sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + mock_fabric_details = MockFabricDetailsByName() + mock_switch_details = MockSwitchDetails() + mock_switch_details.filter = CONFIG[0] + mock_switch_details.mock_response_key = key + + monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) + monkeypatch.setattr(instance, "switch_details", mock_switch_details) + + with does_not_raise(): + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = CONFIG[0] + assert instance.fabric_name == FABRIC_NAME + assert instance.fabric_freeze_mode is False + assert instance.fabric_read_only is False + assert instance.fabric_deployment_disabled is False + assert instance.mode == "normal" + assert instance.role == "leaf" + + +def test_maintenance_mode_info_00510(monkeypatch) -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + + ### Summary + - Verify happy path with: + - switch_details: freezeMode is True + - fabric_details: IS_READ_ONLY not present + + ### Code Flow - Setup + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + - ``FabricDetails()`` is mocked not to raise any exceptions. + - ``SwitchDetails()`` is mocked not to raise any exceptions. + - ``responses_SwitchDetails.json`` contains a 200 response that + contains the switch ip address in CONFIG (192.168.1.2) + - ``responses_FabricDetailsByName.json`` contains a 200 response that + contains FABRIC_NAME. + + ### Code Flow - Test + - ``refresh()`` is called. + + ### Expected Result + - Exception is not raised. + - ``MaintenanceModeInfo().results`` contains expected data. + + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + pass + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = mock_sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + mock_fabric_details = MockFabricDetailsByName() + mock_switch_details = MockSwitchDetails() + mock_switch_details.filter = CONFIG[0] + mock_switch_details.mock_response_key = key + + monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) + monkeypatch.setattr(instance, "switch_details", mock_switch_details) + + with does_not_raise(): + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = CONFIG[0] + assert instance.fabric_name == FABRIC_NAME + assert instance.fabric_freeze_mode is True + assert instance.fabric_read_only is True + assert instance.fabric_deployment_disabled is True + assert instance.mode == "normal" + assert instance.role == "leaf" + + +def test_maintenance_mode_info_00520(monkeypatch) -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + + ### Summary + - Verify happy path with: + - switch_details: freezeMode is True + - switch_details: mode is Normal + - fabric_details: IS_READ_ONLY present and True + - fabric_details: DEPLOYMENT_FREEZE present and True + + ### Code Flow - Setup + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + - ``FabricDetails()`` is mocked not to raise any exceptions. + - ``SwitchDetails()`` is mocked not to raise any exceptions. + - ``responses_SwitchDetails.json`` contains a 200 response that + contains the switch ip address in CONFIG (192.168.1.2) + - ``responses_FabricDetailsByName.json`` contains a 200 response that + contains FABRIC_NAME. + + ### Code Flow - Test + - ``refresh()`` is called. + + ### Expected Result + - Exception is not raised. + - ``MaintenanceModeInfo().results`` contains expected data. + - mode is "inconsistent" due to mode differing from freezeMode. + + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + pass + + mock_sender = MockSender() + mock_sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = mock_sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + mock_fabric_details = MockFabricDetailsByName() + mock_fabric_details.mock_response_key = key + mock_fabric_details.filter = "VXLAN_Fabric" + + mock_switch_details = MockSwitchDetails() + mock_switch_details.mock_response_key = key + mock_switch_details.filter = CONFIG[0] + + monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) + monkeypatch.setattr(instance, "switch_details", mock_switch_details) + + with does_not_raise(): + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = CONFIG[0] + assert instance.fabric_name == FABRIC_NAME + assert instance.fabric_freeze_mode is True + assert instance.fabric_read_only is True + assert instance.fabric_deployment_disabled is True + assert instance.mode == "inconsistent" + assert instance.role == "leaf" + + # @pytest.mark.parametrize( # "mock_exception, expected_exception, mock_message", # [ From 31907dc6f85378369fb3d7b8aaab2fd92bc90b80 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 6 Jun 2024 10:43:23 -1000 Subject: [PATCH 133/230] sender_file.py Sender(): New class to simulate controller responses DISCUSSION: Sender(), in sender_file.py, implements the sender interface. It is injected into RestSend() v2. RestSend() has no knowledge of how controller responses are retrieved, as long as the sender interface is conformed to. Hence, the rest of the code base (that uses RestSend) can be unit tested without having to mock anything. The actual code is tested, rather than mocks. 1. dcnm_sender.py - rename to sender_dcnm.py for consistency with sender_file.py. Future Sender() classes, e.g. that leverage Requests, would be named e.g. sender_requests.py, etc. 2. test_maintenance_mode_info.py: Converted all test cases to use the above. 3. MockSwitchDetails(): Modified to mock ONLY the exceptions raised by SwitchDetails. It no longer duplicates the functionality of Sender(). 4. dcnm_maintenance_mode.py: Changed import to use sender_dcnm. 5. module_utils/common/sender_file.py: New file 6. module_utils/common/sender_dcnm.py: Update docstring. 7. module_utils/common/rest_send_v2.py: Update docstring. 8. module_utils/common/maintenance_mode_info.py: Update docstring. --- .../common/maintenance_mode_info.py | 2 +- plugins/module_utils/common/rest_send_v2.py | 4 +- .../common/{dcnm_sender.py => sender_dcnm.py} | 2 +- plugins/module_utils/common/sender_file.py | 194 +++ plugins/modules/dcnm_maintenance_mode.py | 2 +- tests/unit/mocks/mock_switch_details.py | 182 ++- .../unit/module_utils/common/common_utils.py | 10 - .../responses_FabricDetailsByName.json | 650 +-------- .../fixtures/responses_SwitchDetails.json | 526 +------ .../common/test_maintenance_mode_info.py | 1249 +++-------------- 10 files changed, 643 insertions(+), 2178 deletions(-) rename plugins/module_utils/common/{dcnm_sender.py => sender_dcnm.py} (99%) create mode 100644 plugins/module_utils/common/sender_file.py diff --git a/plugins/module_utils/common/maintenance_mode_info.py b/plugins/module_utils/common/maintenance_mode_info.py index af1656dca..7fe2c7535 100644 --- a/plugins/module_utils/common/maintenance_mode_info.py +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -92,7 +92,7 @@ class MaintenanceModeInfo: - ``params`` is ``AnsibleModule.params`` - ``config`` is per the above example. - ``sender`` is an instance of a Sender() class. - See ``dcnm_sender.py`` for usage. + See ``sender_dcnm.py`` for usage. ```python ansible_module = AnsibleModule() diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 1c80a2d57..ab705167c 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -36,7 +36,7 @@ class RestSend: - Send REST requests to the controller with retries. - Accepts a ``Sender()`` class that implements the sender interface. - The sender interface is defined in - ``module_utils/common/dcnm_sender.py`` + ``module_utils/common/sender_dcnm.py`` - Accepts a ``ResponseHandler()`` class that implements the response handler interface. - The response handler interface is defined in @@ -68,7 +68,7 @@ class RestSend: - A Sender() class is used in the usage example below that requires an instance of ``AnsibleModule``, and uses ``dcnm_send()`` to send requests to the controller. - - See ``module_utils/common/dcnm_sender.py`` for details about + - See ``module_utils/common/sender_dcnm.py`` for details about implementing ``Sender()`` classes. - A ResponseHandler() class is used in the usage example below that abstracts controller response handling. It accepts a controller diff --git a/plugins/module_utils/common/dcnm_sender.py b/plugins/module_utils/common/sender_dcnm.py similarity index 99% rename from plugins/module_utils/common/dcnm_sender.py rename to plugins/module_utils/common/sender_dcnm.py index 12cb25396..5612f0c4b 100644 --- a/plugins/module_utils/common/dcnm_sender.py +++ b/plugins/module_utils/common/sender_dcnm.py @@ -31,7 +31,7 @@ class Sender: """ ### Summary An injected dependency for ``RestSend`` which implements the - ``sender`` interface using dcnm_send. + ``sender`` interface. Responses are retrieved using dcnm_send. ### Raises - ``ValueError`` if: diff --git a/plugins/module_utils/common/sender_file.py b/plugins/module_utils/common/sender_file.py new file mode 100644 index 000000000..1c3a8f27d --- /dev/null +++ b/plugins/module_utils/common/sender_file.py @@ -0,0 +1,194 @@ +# +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging + + +class Sender: + """ + ### Summary + An injected dependency for ``RestSend`` which implements the + ``sender`` interface. Responses are read from JSON files. + + ### Raises + - ``ValueError`` if: + - ``gen`` is not set. + - ``TypeError`` if: + - ``gen`` is not an instance of ResponseGenerator() + + ### Usage + ``responses()`` is a coroutine that yields controller responses. + In the example below, it yields to dictionaries. However, in + practice, it would yield responses read from JSON files. + + ```python + def responses(): + yield {"key1": "value1"} + yield {"key2": "value2"} + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + + try: + rest_send = RestSend() + rest_send.sender = sender + except (TypeError, ValueError) as error: + handle_error(error) + # etc... + # See rest_send_v2.py for RestSend() usage. + ``` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + self._ansible_module = None + self._gen = None + self._path = None + self._payload = None + self._response = None + self._verb = None + + msg = "ENTERED Sender(): " + self.log.debug(msg) + + def _verify_commit_parameters(self): + """ + ### Summary + Verify that required parameters are set prior to calling ``commit()`` + + ### Raises + - ``ValueError`` if ``verb`` is not set + - ``ValueError`` if ``path`` is not set + """ + if self.gen is None: + msg = f"{self.class_name}._verify_commit_parameters: " + msg += "gen must be set before calling commit()." + raise ValueError(msg) + + def commit(self): + """ + ### Summary + Dummy commit + + ### Raises + - ```ValueError`` if ``gen`` is not set. + """ + self._verify_commit_parameters() + + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"caller {caller}" + self.log.debug(msg) + + @property + def ansible_module(self): + """ + ### Summary + Dummy ansible_module + """ + return self._ansible_module + + @ansible_module.setter + def ansible_module(self, value): + self._ansible_module = value + + @property + def gen(self): + """ + - getter: Return the ``ResponseGenerator()`` instance. + - setter: Set the ``ResponseGenerator()`` instance that provides + simulated responses. + """ + return self._gen + + @gen.setter + def gen(self, value): + self._gen = value + + @property + def path(self): + """ + ### Summary + Dummy path. + + ### Raises + None + + ### Example + ``/appcenter/cisco/ndfc/api/v1/...etc...`` + """ + return self._path + + @path.setter + def path(self, value): + self._path = value + + @property + def payload(self): + """ + ### Summary + Dummy payload. + + ### Raises + - ``TypeError`` if value is not a ``dict``. + """ + return self._payload + + @payload.setter + def payload(self, value): + self._payload = value + + @property + def response(self): + """ + ### Summary + The simulated response from a file. + + ### Raises + - ``TypeError`` if value is not a ``dict``. + + - getter: Return a copy of ``response`` + - setter: Set ``response`` + """ + return self.gen.next + + @response.setter + def response(self, value): + self._response = value + + @property + def verb(self): + """ + ### Summary + Dummy Verb. + + ### Raises + None + """ + return self._verb + + @verb.setter + def verb(self, value): + self._verb = value diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 6fd236817..b6e227476 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -127,7 +127,7 @@ import logging from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.common.dcnm_sender import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import \ Sender from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import \ Log diff --git a/tests/unit/mocks/mock_switch_details.py b/tests/unit/mocks/mock_switch_details.py index 72945b913..93e4607aa 100644 --- a/tests/unit/mocks/mock_switch_details.py +++ b/tests/unit/mocks/mock_switch_details.py @@ -19,13 +19,106 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ - responses_switch_details - class MockSwitchDetails: """ - Mock the SwitchDetails class + ### Summary + Mock the exceptions raised by the methods and properties + in the ``SwitchDetails`` class. + + ### NOTES + - This class is used to test the exceptions raised by ``SwitchDetails`` + - This class does NOT simulate the behavior of ``SwitchDetails`` with + respect its interaction with the controller. For that, see the + ``Sender`` class within ``module_utils/common/sender_file.py``, + and the ``RestSend`` class within ``module_utils/common/rest_send.py``. + - Example usage for the ``Sender`` class can be found in + ``test_maintenance_mode_info_00500`` within + ``tests/unit/module_utils/common/test_maintenance_mode_info.py``. + + ### Example usage + ```python + @pytest.mark.parametrize( + "mock_class, mock_property, mock_exception, expected_exception, mock_message", + [ + ( + "FabricDetailsByName", + "refresh", + ControllerResponseError, + ValueError, + "Bad controller response: fabric_details.refresh", + ), + ( + "FabricDetailsByName", + "results.setter", + TypeError, + ValueError, + "Bad type: fabric_details.results.setter", + ), + ( + "FabricDetailsByName", + "rest_send.setter", + TypeError, + ValueError, + "Bad type: fabric_details.rest_send.setter", + ), + ( + "SwitchDetails", + "refresh", + ControllerResponseError, + ValueError, + "Bad controller response: switch_details.refresh", + ), + ( + "SwitchDetails", + "results.setter", + TypeError, + ValueError, + "Bad type: switch_details.results.setter", + ), + ( + "SwitchDetails", + "rest_send.setter", + TypeError, + ValueError, + "Bad type: switch_details.rest_send.setter", + ), + ], + ) + def test_maintenance_mode_info_00200( + monkeypatch, + mock_class, + mock_property, + mock_exception, + expected_exception, + mock_message, + ) -> None: + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + mock_fabric_details = MockFabricDetailsByName() + mock_fabric_details.mock_class = mock_class + mock_fabric_details.mock_exception = mock_exception + mock_fabric_details.mock_message = mock_message + mock_fabric_details.mock_property = mock_property + + mock_switch_details = MockSwitchDetails() + mock_switch_details.mock_class = mock_class + mock_switch_details.mock_exception = mock_exception + mock_switch_details.mock_message = mock_message + mock_switch_details.mock_property = mock_property + + monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) + monkeypatch.setattr(instance, "switch_details", mock_switch_details) + + with does_not_raise(): + instance.config = CONFIG + instance.rest_send = RestSend({"state": "query", "check_mode": False}) + instance.results = Results() + + with pytest.raises(expected_exception, match=mock_message): + instance.refresh() + ``` """ def __init__(self) -> None: @@ -38,10 +131,6 @@ def null_mock_exception(): self._mock_exception = null_mock_exception self._mock_message = None self._mock_property = None - self._mock_response_key = None - - self.response = None - self.response_data = None self._filter = None self._info = {} @@ -61,25 +150,6 @@ def refresh(self): """ if self.mock_class == self.class_name and self.mock_property == "refresh": raise self.mock_exception(self.mock_message) - if self.mock_response_key is None: - return - self.populate_info() - - def populate_info(self): - """ - Populate the info dict. - """ - self._info = {} - self.response = responses_switch_details(self.mock_response_key) - self.response_data = self.response.get("DATA", []) - for switch in self.response_data: - self._info[switch["ipAddress"]] = switch - - def _get(self, key): - """ - Get the value of the key from the info dict. - """ - return self._info.get(self.filter, {}).get(key, None) @property def mock_class(self): @@ -128,7 +198,7 @@ def mock_property(self, value): @property def filter(self): """ - IP Address of the switch with which to filter self._info() + Mocked filter """ if self.mock_class == self.class_name and self.mock_property == "filter.getter": raise self.mock_exception(self.mock_message) @@ -140,21 +210,6 @@ def filter(self, value): raise self.mock_exception(self.mock_message) self._filter = value - @property - def mock_response_key(self): - """ - The key used to extract controller response from the mocked response - in ``responses_SwitchDetails.json``. - - When setter is accessed, call ``populate_properties()`` to set the - mocked property values from the contents of the mocked response. - """ - return self._mock_response_key - - @mock_response_key.setter - def mock_response_key(self, value): - self._mock_response_key = value - @property def rest_send(self): """ @@ -202,10 +257,20 @@ def fabric_name(self): """ Mocked fabric_name property """ - return self._get("fabricName") + if ( + self.mock_class == self.class_name + and self.mock_property == "fabric_name.getter" + ): + raise self.mock_exception(self.mock_message) + return self._fabric_name @fabric_name.setter def fabric_name(self, value): + if ( + self.mock_class == self.class_name + and self.mock_property == "fabric_name.setter" + ): + raise self.mock_exception(self.mock_message) self._fabric_name = value @property @@ -213,10 +278,20 @@ def freeze_mode(self): """ Mocked freeze_mode property """ - return self._get("freezeMode") + if ( + self.mock_class == self.class_name + and self.mock_property == "freeze_mode.getter" + ): + raise self.mock_exception(self.mock_message) + return self._freeze_mode @freeze_mode.setter def freeze_mode(self, value): + if ( + self.mock_class == self.class_name + and self.mock_property == "freeze_mode.setter" + ): + raise self.mock_exception(self.mock_message) self._freeze_mode = value @property @@ -229,14 +304,7 @@ def maintenance_mode(self): and self.mock_property == "maintenance_mode.getter" ): raise self.mock_exception(self.mock_message) - - mode = str(self._get("mode")).lower() - system_mode = str(self._get("systemMode")).lower() - if mode == "migration": - return "migration" - if mode != system_mode: - return "inconsistent" - return mode + return self._maintenance_mode @maintenance_mode.setter def maintenance_mode(self, value): @@ -254,7 +322,7 @@ def mode(self): """ if self.mock_class == self.class_name and self.mock_property == "mode.getter": raise self.mock_exception(self.mock_message) - return self._get("mode") + return self._mode @mode.setter def mode(self, value): @@ -272,7 +340,7 @@ def serial_number(self): and self.mock_property == "serial_number.getter" ): raise self.mock_exception(self.mock_message) - return self._get("serialNumber") + return self.serial_number @serial_number.setter def serial_number(self, value): @@ -293,7 +361,7 @@ def switch_role(self): and self.mock_property == "switch_role.getter" ): raise self.mock_exception(self.mock_message) - return self._get("switchRole") + return self.switch_role @switch_role.setter def switch_role(self, value): @@ -314,7 +382,7 @@ def system_mode(self): and self.mock_property == "system_mode.getter" ): raise self.mock_exception(self.mock_message) - return self._get("systemMode") + return self.system_mode @system_mode.setter def system_mode(self, value): diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index 940e3c2c4..27dc9eea5 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -353,16 +353,6 @@ def responses_maintenance_mode(key: str) -> Dict[str, str]: return response -def responses_maintenance_mode_info(key: str) -> Dict[str, str]: - """ - Return data in responses_MaintenanceModeInfo.json - """ - response_file = "responses_MaintenanceModeInfo" - response = load_fixture(response_file).get(key) - print(f"responses_maintenance_mode_info: {key} : {response}") - return response - - def responses_switch_details(key: str) -> Dict[str, str]: """ Return data in responses_SwitchDetails.json diff --git a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json index d2de04bc8..50be76d7a 100644 --- a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json +++ b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json @@ -1,5 +1,5 @@ { - "test_maintenance_mode_info_00500a": { + "test_maintenance_mode_info_00210a": { "DATA": [ { "asn": "65000", @@ -313,633 +313,45 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", "RETURN_CODE": 200 }, - "test_maintenance_mode_info_00510a": { - "DATA": [ - { - "asn": "65000", - "createdOn": 1716345062044, - "deviceType": "n9k", - "fabricId": "FABRIC-2", - "fabricName": "VXLAN_Fabric", - "fabricTechnology": "VXLANFabric", - "fabricTechnologyFriendly": "VXLAN EVPN", - "fabricType": "Switch_Fabric", - "fabricTypeFriendly": "Switch Fabric", - "id": 2, - "modifiedOn": 1716952430067, - "networkExtensionTemplate": "Default_Network_Extension_Universal", - "networkTemplate": "Default_Network_Universal", - "nvPairs": { - "AAA_REMOTE_IP_ENABLED": "false", - "AAA_SERVER_CONF": "", - "ACTIVE_MIGRATION": "false", - "ADVERTISE_PIP_BGP": "false", - "ADVERTISE_PIP_ON_BORDER": "true", - "AGENT_INTF": "eth0", - "AI_ML_QOS_POLICY": "AI_Fabric_QOS_400G", - "ALLOW_NXC": "true", - "ALLOW_NXC_PREV": "true", - "ANYCAST_BGW_ADVERTISE_PIP": "false", - "ANYCAST_GW_MAC": "2020.0000.00aa", - "ANYCAST_LB_ID": "", - "ANYCAST_RP_IP_RANGE": "10.254.254.0/24", - "ANYCAST_RP_IP_RANGE_INTERNAL": "10.254.254.0/24", - "AUTO_SYMMETRIC_DEFAULT_VRF": "false", - "AUTO_SYMMETRIC_VRF_LITE": "false", - "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", - "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", - "AUTO_VRFLITE_IFC_DEFAULT_VRF": "false", - "BANNER": "", - "BFD_AUTH_ENABLE": "false", - "BFD_AUTH_KEY": "", - "BFD_AUTH_KEY_ID": "", - "BFD_ENABLE": "false", - "BFD_ENABLE_PREV": "false", - "BFD_IBGP_ENABLE": "false", - "BFD_ISIS_ENABLE": "false", - "BFD_OSPF_ENABLE": "false", - "BFD_PIM_ENABLE": "false", - "BGP_AS": "65000", - "BGP_AS_PREV": "65000", - "BGP_AUTH_ENABLE": "false", - "BGP_AUTH_KEY": "", - "BGP_AUTH_KEY_TYPE": "3", - "BGP_LB_ID": "0", - "BOOTSTRAP_CONF": "", - "BOOTSTRAP_ENABLE": "false", - "BOOTSTRAP_ENABLE_PREV": "false", - "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", - "BOOTSTRAP_MULTISUBNET_INTERNAL": "", - "BRFIELD_DEBUG_FLAG": "Disable", - "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", - "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", - "CDP_ENABLE": "false", - "COPP_POLICY": "strict", - "DCI_SUBNET_RANGE": "10.33.0.0/16", - "DCI_SUBNET_TARGET_MASK": "30", - "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", - "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", - "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", - "DEFAULT_VRF_REDIS_BGP_RMAP": "", - "DEPLOYMENT_FREEZE": "false", - "DHCP_ENABLE": "false", - "DHCP_END": "", - "DHCP_END_INTERNAL": "", - "DHCP_IPV6_ENABLE": "", - "DHCP_IPV6_ENABLE_INTERNAL": "", - "DHCP_START": "", - "DHCP_START_INTERNAL": "", - "DNS_SERVER_IP_LIST": "", - "DNS_SERVER_VRF": "", - "DOMAIN_NAME_INTERNAL": "", - "ENABLE_AAA": "false", - "ENABLE_AGENT": "false", - "ENABLE_AI_ML_QOS_POLICY": "false", - "ENABLE_AI_ML_QOS_POLICY_FLAP": "false", - "ENABLE_DEFAULT_QUEUING_POLICY": "false", - "ENABLE_EVPN": "true", - "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", - "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", - "ENABLE_L3VNI_NO_VLAN": "false", - "ENABLE_MACSEC": "false", - "ENABLE_NETFLOW": "false", - "ENABLE_NETFLOW_PREV": "false", - "ENABLE_NGOAM": "true", - "ENABLE_NXAPI": "true", - "ENABLE_NXAPI_HTTP": "true", - "ENABLE_PBR": "false", - "ENABLE_PVLAN": "false", - "ENABLE_PVLAN_PREV": "false", - "ENABLE_SGT": "false", - "ENABLE_SGT_PREV": "false", - "ENABLE_TENANT_DHCP": "true", - "ENABLE_TRM": "false", - "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", - "ESR_OPTION": "PBR", - "EXTRA_CONF_INTRA_LINKS": "", - "EXTRA_CONF_LEAF": "", - "EXTRA_CONF_SPINE": "", - "EXTRA_CONF_TOR": "", - "EXT_FABRIC_TYPE": "", - "FABRIC_INTERFACE_TYPE": "p2p", - "FABRIC_MTU": "9216", - "FABRIC_MTU_PREV": "9216", - "FABRIC_NAME": "VXLAN_Fabric", - "FABRIC_TYPE": "Switch_Fabric", - "FABRIC_VPC_DOMAIN_ID": "", - "FABRIC_VPC_DOMAIN_ID_PREV": "", - "FABRIC_VPC_QOS": "false", - "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", - "FEATURE_PTP": "false", - "FEATURE_PTP_INTERNAL": "false", - "FF": "Easy_Fabric", - "GRFIELD_DEBUG_FLAG": "Disable", - "HD_TIME": "180", - "HOST_INTF_ADMIN_STATE": "true", - "IBGP_PEER_TEMPLATE": "", - "IBGP_PEER_TEMPLATE_LEAF": "", - "INBAND_DHCP_SERVERS": "", - "INBAND_MGMT": "false", - "INBAND_MGMT_PREV": "false", - "ISIS_AREA_NUM": "0001", - "ISIS_AREA_NUM_PREV": "", - "ISIS_AUTH_ENABLE": "false", - "ISIS_AUTH_KEY": "", - "ISIS_AUTH_KEYCHAIN_KEY_ID": "", - "ISIS_AUTH_KEYCHAIN_NAME": "", - "ISIS_LEVEL": "level-2", - "ISIS_OVERLOAD_ELAPSE_TIME": "", - "ISIS_OVERLOAD_ENABLE": "false", - "ISIS_P2P_ENABLE": "false", - "L2_HOST_INTF_MTU": "9216", - "L2_HOST_INTF_MTU_PREV": "9216", - "L2_SEGMENT_ID_RANGE": "30000-49000", - "L3VNI_MCAST_GROUP": "", - "L3_PARTITION_ID_RANGE": "50000-59000", - "LINK_STATE_ROUTING": "ospf", - "LINK_STATE_ROUTING_TAG": "UNDERLAY", - "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", - "LOOPBACK0_IPV6_RANGE": "", - "LOOPBACK0_IP_RANGE": "10.2.0.0/22", - "LOOPBACK1_IPV6_RANGE": "", - "LOOPBACK1_IP_RANGE": "10.3.0.0/22", - "MACSEC_ALGORITHM": "", - "MACSEC_CIPHER_SUITE": "", - "MACSEC_FALLBACK_ALGORITHM": "", - "MACSEC_FALLBACK_KEY_STRING": "", - "MACSEC_KEY_STRING": "", - "MACSEC_REPORT_TIMER": "", - "MGMT_GW": "", - "MGMT_GW_INTERNAL": "", - "MGMT_PREFIX": "", - "MGMT_PREFIX_INTERNAL": "", - "MGMT_V6PREFIX": "", - "MGMT_V6PREFIX_INTERNAL": "", - "MPLS_HANDOFF": "false", - "MPLS_ISIS_AREA_NUM": "0001", - "MPLS_ISIS_AREA_NUM_PREV": "", - "MPLS_LB_ID": "", - "MPLS_LOOPBACK_IP_RANGE": "", - "MSO_CONNECTIVITY_DEPLOYED": "", - "MSO_CONTROLER_ID": "", - "MSO_SITE_GROUP_NAME": "", - "MSO_SITE_ID": "", - "MST_INSTANCE_RANGE": "", - "MULTICAST_GROUP_SUBNET": "239.1.1.0/25", - "NETFLOW_EXPORTER_LIST": "", - "NETFLOW_MONITOR_LIST": "", - "NETFLOW_RECORD_LIST": "", - "NETWORK_VLAN_RANGE": "2300-2999", - "NTP_SERVER_IP_LIST": "", - "NTP_SERVER_VRF": "", - "NVE_LB_ID": "1", - "NXAPI_HTTPS_PORT": "443", - "NXAPI_HTTP_PORT": "80", - "NXC_DEST_VRF": "management", - "NXC_PROXY_PORT": "8080", - "NXC_PROXY_SERVER": "", - "NXC_SRC_INTF": "", - "OBJECT_TRACKING_NUMBER_RANGE": "100-299", - "OSPF_AREA_ID": "0.0.0.0", - "OSPF_AUTH_ENABLE": "false", - "OSPF_AUTH_KEY": "", - "OSPF_AUTH_KEY_ID": "", - "OVERLAY_MODE": "cli", - "OVERLAY_MODE_PREV": "cli", - "OVERWRITE_GLOBAL_NXC": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION_V6": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV": "false", - "PER_VRF_LOOPBACK_IP_RANGE": "", - "PER_VRF_LOOPBACK_IP_RANGE_V6": "fd00::a05:0/112", - "PHANTOM_RP_LB_ID1": "", - "PHANTOM_RP_LB_ID2": "", - "PHANTOM_RP_LB_ID3": "", - "PHANTOM_RP_LB_ID4": "", - "PIM_HELLO_AUTH_ENABLE": "false", - "PIM_HELLO_AUTH_KEY": "", - "PM_ENABLE": "false", - "PM_ENABLE_PREV": "false", - "POWER_REDUNDANCY_MODE": "ps-redundant", - "PREMSO_PARENT_FABRIC": "", - "PTP_DOMAIN_ID": "", - "PTP_LB_ID": "", - "REPLICATION_MODE": "Multicast", - "ROUTER_ID_RANGE": "", - "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", - "RP_COUNT": "2", - "RP_LB_ID": "254", - "RP_MODE": "asm", - "RR_COUNT": "2", - "SEED_SWITCH_CORE_INTERFACES": "", - "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", - "SGT_ID_RANGE": "", - "SGT_NAME_PREFIX": "", - "SGT_PREPROVISION": "false", - "SITE_ID": "65000", - "SLA_ID_RANGE": "10000-19999", - "SNMP_SERVER_HOST_TRAP": "true", - "SPINE_COUNT": "1", - "SPINE_SWITCH_CORE_INTERFACES": "", - "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", - "SSPINE_COUNT": "0", - "STATIC_UNDERLAY_IP_ALLOC": "false", - "STP_BRIDGE_PRIORITY": "", - "STP_ROOT_OPTION": "unmanaged", - "STP_VLAN_RANGE": "", - "STRICT_CC_MODE": "false", - "SUBINTERFACE_RANGE": "2-511", - "SUBNET_RANGE": "10.4.0.0/16", - "SUBNET_TARGET_MASK": "30", - "SYSLOG_SERVER_IP_LIST": "", - "SYSLOG_SERVER_VRF": "", - "SYSLOG_SEV": "", - "TCAM_ALLOCATION": "true", - "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", - "UNDERLAY_IS_V6": "false", - "UNNUM_BOOTSTRAP_LB_ID": "", - "UNNUM_DHCP_END": "", - "UNNUM_DHCP_END_INTERNAL": "", - "UNNUM_DHCP_START": "", - "UNNUM_DHCP_START_INTERNAL": "", - "UPGRADE_FROM_VERSION": "", - "USE_LINK_LOCAL": "false", - "V6_SUBNET_RANGE": "", - "V6_SUBNET_TARGET_MASK": "126", - "VPC_AUTO_RECOVERY_TIME": "360", - "VPC_DELAY_RESTORE": "150", - "VPC_DELAY_RESTORE_TIME": "60", - "VPC_DOMAIN_ID_RANGE": "1-1000", - "VPC_ENABLE_IPv6_ND_SYNC": "true", - "VPC_PEER_KEEP_ALIVE_OPTION": "management", - "VPC_PEER_LINK_PO": "500", - "VPC_PEER_LINK_VLAN": "3600", - "VRF_LITE_AUTOCONFIG": "Manual", - "VRF_VLAN_RANGE": "2000-2299", - "abstract_anycast_rp": "anycast_rp", - "abstract_bgp": "base_bgp", - "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", - "abstract_bgp_rr": "evpn_bgp_rr", - "abstract_dhcp": "base_dhcp", - "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", - "abstract_extra_config_leaf": "extra_config_leaf", - "abstract_extra_config_spine": "extra_config_spine", - "abstract_extra_config_tor": "extra_config_tor", - "abstract_feature_leaf": "base_feature_leaf_upg", - "abstract_feature_spine": "base_feature_spine_upg", - "abstract_isis": "base_isis_level2", - "abstract_isis_interface": "isis_interface", - "abstract_loopback_interface": "int_fabric_loopback_11_1", - "abstract_multicast": "base_multicast_11_1", - "abstract_ospf": "base_ospf", - "abstract_ospf_interface": "ospf_interface_11_1", - "abstract_pim_interface": "pim_interface", - "abstract_route_map": "route_map", - "abstract_routed_host": "int_routed_host", - "abstract_trunk_host": "int_trunk_host", - "abstract_vlan_interface": "int_fabric_vlan_11_1", - "abstract_vpc_domain": "base_vpc_domain_11_1", - "dcnmUser": "admin", - "default_network": "Default_Network_Universal", - "default_pvlan_sec_network": "", - "default_vrf": "Default_VRF_Universal", - "enableRealTimeBackup": "", - "enableScheduledBackup": "", - "network_extension_template": "Default_Network_Extension_Universal", - "scheduledTime": "", - "temp_anycast_gateway": "anycast_gateway", - "temp_vpc_domain_mgmt": "vpc_domain_mgmt", - "temp_vpc_peer_link": "int_vpc_peer_link_po", - "vrf_extension_template": "Default_VRF_Extension_Universal" - }, - "operStatus": "CRITICAL", - "provisionMode": "DCNMTopDown", - "replicationMode": "Multicast", - "siteId": "65000", - "templateName": "Easy_Fabric", - "vrfExtensionTemplate": "Default_VRF_Extension_Universal", - "vrfTemplate": "Default_VRF_Universal" - } + "test_maintenance_mode_info_00300a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "MESSAGE OK" ], + "DATA": [], "MESSAGE": "OK", "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", "RETURN_CODE": 200 }, - "test_maintenance_mode_info_00520a": { + "test_maintenance_mode_info_00500a": { "TEST_NOTES": [ - "IS_READ_ONLY is True" + "RETURN_CODE 200", + "MESSAGE OK" ], - "DATA": [ - { - "asn": "65000", - "createdOn": 1716345062044, - "deviceType": "n9k", - "fabricId": "FABRIC-2", - "fabricName": "VXLAN_Fabric", - "fabricTechnology": "VXLANFabric", - "fabricTechnologyFriendly": "VXLAN EVPN", - "fabricType": "Switch_Fabric", - "fabricTypeFriendly": "Switch Fabric", - "id": 2, - "modifiedOn": 1716952430067, - "networkExtensionTemplate": "Default_Network_Extension_Universal", - "networkTemplate": "Default_Network_Universal", - "nvPairs": { - "AAA_REMOTE_IP_ENABLED": "false", - "AAA_SERVER_CONF": "", - "ACTIVE_MIGRATION": "false", - "ADVERTISE_PIP_BGP": "false", - "ADVERTISE_PIP_ON_BORDER": "true", - "AGENT_INTF": "eth0", - "AI_ML_QOS_POLICY": "AI_Fabric_QOS_400G", - "ALLOW_NXC": "true", - "ALLOW_NXC_PREV": "true", - "ANYCAST_BGW_ADVERTISE_PIP": "false", - "ANYCAST_GW_MAC": "2020.0000.00aa", - "ANYCAST_LB_ID": "", - "ANYCAST_RP_IP_RANGE": "10.254.254.0/24", - "ANYCAST_RP_IP_RANGE_INTERNAL": "10.254.254.0/24", - "AUTO_SYMMETRIC_DEFAULT_VRF": "false", - "AUTO_SYMMETRIC_VRF_LITE": "false", - "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", - "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", - "AUTO_VRFLITE_IFC_DEFAULT_VRF": "false", - "BANNER": "", - "BFD_AUTH_ENABLE": "false", - "BFD_AUTH_KEY": "", - "BFD_AUTH_KEY_ID": "", - "BFD_ENABLE": "false", - "BFD_ENABLE_PREV": "false", - "BFD_IBGP_ENABLE": "false", - "BFD_ISIS_ENABLE": "false", - "BFD_OSPF_ENABLE": "false", - "BFD_PIM_ENABLE": "false", - "BGP_AS": "65000", - "BGP_AS_PREV": "65000", - "BGP_AUTH_ENABLE": "false", - "BGP_AUTH_KEY": "", - "BGP_AUTH_KEY_TYPE": "3", - "BGP_LB_ID": "0", - "BOOTSTRAP_CONF": "", - "BOOTSTRAP_ENABLE": "false", - "BOOTSTRAP_ENABLE_PREV": "false", - "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", - "BOOTSTRAP_MULTISUBNET_INTERNAL": "", - "BRFIELD_DEBUG_FLAG": "Disable", - "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", - "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", - "CDP_ENABLE": "false", - "COPP_POLICY": "strict", - "DCI_SUBNET_RANGE": "10.33.0.0/16", - "DCI_SUBNET_TARGET_MASK": "30", - "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", - "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", - "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", - "DEFAULT_VRF_REDIS_BGP_RMAP": "", - "DEPLOYMENT_FREEZE": "true", - "DHCP_ENABLE": "false", - "DHCP_END": "", - "DHCP_END_INTERNAL": "", - "DHCP_IPV6_ENABLE": "", - "DHCP_IPV6_ENABLE_INTERNAL": "", - "DHCP_START": "", - "DHCP_START_INTERNAL": "", - "DNS_SERVER_IP_LIST": "", - "DNS_SERVER_VRF": "", - "DOMAIN_NAME_INTERNAL": "", - "ENABLE_AAA": "false", - "ENABLE_AGENT": "false", - "ENABLE_AI_ML_QOS_POLICY": "false", - "ENABLE_AI_ML_QOS_POLICY_FLAP": "false", - "ENABLE_DEFAULT_QUEUING_POLICY": "false", - "ENABLE_EVPN": "true", - "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", - "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", - "ENABLE_L3VNI_NO_VLAN": "false", - "ENABLE_MACSEC": "false", - "ENABLE_NETFLOW": "false", - "ENABLE_NETFLOW_PREV": "false", - "ENABLE_NGOAM": "true", - "ENABLE_NXAPI": "true", - "ENABLE_NXAPI_HTTP": "true", - "ENABLE_PBR": "false", - "ENABLE_PVLAN": "false", - "ENABLE_PVLAN_PREV": "false", - "ENABLE_SGT": "false", - "ENABLE_SGT_PREV": "false", - "ENABLE_TENANT_DHCP": "true", - "ENABLE_TRM": "false", - "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", - "ESR_OPTION": "PBR", - "EXTRA_CONF_INTRA_LINKS": "", - "EXTRA_CONF_LEAF": "", - "EXTRA_CONF_SPINE": "", - "EXTRA_CONF_TOR": "", - "EXT_FABRIC_TYPE": "", - "FABRIC_INTERFACE_TYPE": "p2p", - "FABRIC_MTU": "9216", - "FABRIC_MTU_PREV": "9216", - "FABRIC_NAME": "VXLAN_Fabric", - "FABRIC_TYPE": "Switch_Fabric", - "FABRIC_VPC_DOMAIN_ID": "", - "FABRIC_VPC_DOMAIN_ID_PREV": "", - "FABRIC_VPC_QOS": "false", - "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", - "FEATURE_PTP": "false", - "FEATURE_PTP_INTERNAL": "false", - "FF": "Easy_Fabric", - "GRFIELD_DEBUG_FLAG": "Disable", - "HD_TIME": "180", - "HOST_INTF_ADMIN_STATE": "true", - "IBGP_PEER_TEMPLATE": "", - "IBGP_PEER_TEMPLATE_LEAF": "", - "INBAND_DHCP_SERVERS": "", - "INBAND_MGMT": "false", - "INBAND_MGMT_PREV": "false", - "IS_READ_ONLY": "true", - "ISIS_AREA_NUM": "0001", - "ISIS_AREA_NUM_PREV": "", - "ISIS_AUTH_ENABLE": "false", - "ISIS_AUTH_KEY": "", - "ISIS_AUTH_KEYCHAIN_KEY_ID": "", - "ISIS_AUTH_KEYCHAIN_NAME": "", - "ISIS_LEVEL": "level-2", - "ISIS_OVERLOAD_ELAPSE_TIME": "", - "ISIS_OVERLOAD_ENABLE": "false", - "ISIS_P2P_ENABLE": "false", - "L2_HOST_INTF_MTU": "9216", - "L2_HOST_INTF_MTU_PREV": "9216", - "L2_SEGMENT_ID_RANGE": "30000-49000", - "L3VNI_MCAST_GROUP": "", - "L3_PARTITION_ID_RANGE": "50000-59000", - "LINK_STATE_ROUTING": "ospf", - "LINK_STATE_ROUTING_TAG": "UNDERLAY", - "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", - "LOOPBACK0_IPV6_RANGE": "", - "LOOPBACK0_IP_RANGE": "10.2.0.0/22", - "LOOPBACK1_IPV6_RANGE": "", - "LOOPBACK1_IP_RANGE": "10.3.0.0/22", - "MACSEC_ALGORITHM": "", - "MACSEC_CIPHER_SUITE": "", - "MACSEC_FALLBACK_ALGORITHM": "", - "MACSEC_FALLBACK_KEY_STRING": "", - "MACSEC_KEY_STRING": "", - "MACSEC_REPORT_TIMER": "", - "MGMT_GW": "", - "MGMT_GW_INTERNAL": "", - "MGMT_PREFIX": "", - "MGMT_PREFIX_INTERNAL": "", - "MGMT_V6PREFIX": "", - "MGMT_V6PREFIX_INTERNAL": "", - "MPLS_HANDOFF": "false", - "MPLS_ISIS_AREA_NUM": "0001", - "MPLS_ISIS_AREA_NUM_PREV": "", - "MPLS_LB_ID": "", - "MPLS_LOOPBACK_IP_RANGE": "", - "MSO_CONNECTIVITY_DEPLOYED": "", - "MSO_CONTROLER_ID": "", - "MSO_SITE_GROUP_NAME": "", - "MSO_SITE_ID": "", - "MST_INSTANCE_RANGE": "", - "MULTICAST_GROUP_SUBNET": "239.1.1.0/25", - "NETFLOW_EXPORTER_LIST": "", - "NETFLOW_MONITOR_LIST": "", - "NETFLOW_RECORD_LIST": "", - "NETWORK_VLAN_RANGE": "2300-2999", - "NTP_SERVER_IP_LIST": "", - "NTP_SERVER_VRF": "", - "NVE_LB_ID": "1", - "NXAPI_HTTPS_PORT": "443", - "NXAPI_HTTP_PORT": "80", - "NXC_DEST_VRF": "management", - "NXC_PROXY_PORT": "8080", - "NXC_PROXY_SERVER": "", - "NXC_SRC_INTF": "", - "OBJECT_TRACKING_NUMBER_RANGE": "100-299", - "OSPF_AREA_ID": "0.0.0.0", - "OSPF_AUTH_ENABLE": "false", - "OSPF_AUTH_KEY": "", - "OSPF_AUTH_KEY_ID": "", - "OVERLAY_MODE": "cli", - "OVERLAY_MODE_PREV": "cli", - "OVERWRITE_GLOBAL_NXC": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION_V6": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV": "false", - "PER_VRF_LOOPBACK_IP_RANGE": "", - "PER_VRF_LOOPBACK_IP_RANGE_V6": "fd00::a05:0/112", - "PHANTOM_RP_LB_ID1": "", - "PHANTOM_RP_LB_ID2": "", - "PHANTOM_RP_LB_ID3": "", - "PHANTOM_RP_LB_ID4": "", - "PIM_HELLO_AUTH_ENABLE": "false", - "PIM_HELLO_AUTH_KEY": "", - "PM_ENABLE": "false", - "PM_ENABLE_PREV": "false", - "POWER_REDUNDANCY_MODE": "ps-redundant", - "PREMSO_PARENT_FABRIC": "", - "PTP_DOMAIN_ID": "", - "PTP_LB_ID": "", - "REPLICATION_MODE": "Multicast", - "ROUTER_ID_RANGE": "", - "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", - "RP_COUNT": "2", - "RP_LB_ID": "254", - "RP_MODE": "asm", - "RR_COUNT": "2", - "SEED_SWITCH_CORE_INTERFACES": "", - "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", - "SGT_ID_RANGE": "", - "SGT_NAME_PREFIX": "", - "SGT_PREPROVISION": "false", - "SITE_ID": "65000", - "SLA_ID_RANGE": "10000-19999", - "SNMP_SERVER_HOST_TRAP": "true", - "SPINE_COUNT": "1", - "SPINE_SWITCH_CORE_INTERFACES": "", - "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", - "SSPINE_COUNT": "0", - "STATIC_UNDERLAY_IP_ALLOC": "false", - "STP_BRIDGE_PRIORITY": "", - "STP_ROOT_OPTION": "unmanaged", - "STP_VLAN_RANGE": "", - "STRICT_CC_MODE": "false", - "SUBINTERFACE_RANGE": "2-511", - "SUBNET_RANGE": "10.4.0.0/16", - "SUBNET_TARGET_MASK": "30", - "SYSLOG_SERVER_IP_LIST": "", - "SYSLOG_SERVER_VRF": "", - "SYSLOG_SEV": "", - "TCAM_ALLOCATION": "true", - "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", - "UNDERLAY_IS_V6": "false", - "UNNUM_BOOTSTRAP_LB_ID": "", - "UNNUM_DHCP_END": "", - "UNNUM_DHCP_END_INTERNAL": "", - "UNNUM_DHCP_START": "", - "UNNUM_DHCP_START_INTERNAL": "", - "UPGRADE_FROM_VERSION": "", - "USE_LINK_LOCAL": "false", - "V6_SUBNET_RANGE": "", - "V6_SUBNET_TARGET_MASK": "126", - "VPC_AUTO_RECOVERY_TIME": "360", - "VPC_DELAY_RESTORE": "150", - "VPC_DELAY_RESTORE_TIME": "60", - "VPC_DOMAIN_ID_RANGE": "1-1000", - "VPC_ENABLE_IPv6_ND_SYNC": "true", - "VPC_PEER_KEEP_ALIVE_OPTION": "management", - "VPC_PEER_LINK_PO": "500", - "VPC_PEER_LINK_VLAN": "3600", - "VRF_LITE_AUTOCONFIG": "Manual", - "VRF_VLAN_RANGE": "2000-2299", - "abstract_anycast_rp": "anycast_rp", - "abstract_bgp": "base_bgp", - "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", - "abstract_bgp_rr": "evpn_bgp_rr", - "abstract_dhcp": "base_dhcp", - "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", - "abstract_extra_config_leaf": "extra_config_leaf", - "abstract_extra_config_spine": "extra_config_spine", - "abstract_extra_config_tor": "extra_config_tor", - "abstract_feature_leaf": "base_feature_leaf_upg", - "abstract_feature_spine": "base_feature_spine_upg", - "abstract_isis": "base_isis_level2", - "abstract_isis_interface": "isis_interface", - "abstract_loopback_interface": "int_fabric_loopback_11_1", - "abstract_multicast": "base_multicast_11_1", - "abstract_ospf": "base_ospf", - "abstract_ospf_interface": "ospf_interface_11_1", - "abstract_pim_interface": "pim_interface", - "abstract_route_map": "route_map", - "abstract_routed_host": "int_routed_host", - "abstract_trunk_host": "int_trunk_host", - "abstract_vlan_interface": "int_fabric_vlan_11_1", - "abstract_vpc_domain": "base_vpc_domain_11_1", - "dcnmUser": "admin", - "default_network": "Default_Network_Universal", - "default_pvlan_sec_network": "", - "default_vrf": "Default_VRF_Universal", - "enableRealTimeBackup": "", - "enableScheduledBackup": "", - "network_extension_template": "Default_Network_Extension_Universal", - "scheduledTime": "", - "temp_anycast_gateway": "anycast_gateway", - "temp_vpc_domain_mgmt": "vpc_domain_mgmt", - "temp_vpc_peer_link": "int_vpc_peer_link_po", - "vrf_extension_template": "Default_VRF_Extension_Universal" - }, - "operStatus": "CRITICAL", - "provisionMode": "DCNMTopDown", - "replicationMode": "Multicast", - "siteId": "65000", - "templateName": "Easy_Fabric", - "vrfExtensionTemplate": "Default_VRF_Extension_Universal", - "vrfTemplate": "Default_VRF_Universal" - } + "DATA": [], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00510a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "MESSAGE OK" + ], + "DATA": [], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00520a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "MESSAGE OK" ], + "DATA": [], "MESSAGE": "OK", "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json index 5e7ae971a..bdbda6dba 100644 --- a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -122,125 +122,39 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 }, + "test_maintenance_mode_info_00210a": { + "TEST_NOTES": [ + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, "test_maintenance_mode_info_00300a": { "TEST_NOTES": [ - "DATA does not contain switch with ip address 192.168.1.2" + "DATA does not contain ipAddress 192.168.1.2", + "DATA[0].fabricName: VXLAN_Fabric", + "DATA[0].freezeMode: null", + "DATA[0].ipAddress: 192.168.1.1", + "DATA[0].mode: Normal", + "DATA[0].serialNumber: FDO211218FV", + "DATA[0].switchRole: leaf", + "DATA[0].systemMode: Normal", + "RETURN_CODE: 200", + "MESSAGE: OK" ], "DATA": [ { - "activeSupSlot": 0, - "availPorts": 0, - "ccStatus": "NA", - "cfsSyslogStatus": 1, - "colDBId": 0, - "connUnitStatus": 0, - "consistencyState": false, - "contact": null, - "cpuUsage": 0, - "deviceType": "External", - "displayHdrs": null, - "displayValues": null, - "domain": null, - "domainID": 0, - "elementType": null, - "fabricId": 3, - "fabricName": "FOO", - "fabricTechnology": "LANClassic", - "fcoeEnabled": false, - "fex": false, - "fexMap": {}, - "fid": 0, + "fabricName": "VXLAN_Fabric", "freezeMode": null, - "health": -1, - "hostName": "cvd-1314-leaf", - "index": 0, - "intentedpeerName": "", - "interfaces": null, "ipAddress": "192.168.1.1", - "ipDomain": "", - "isEchSupport": false, - "isLan": false, - "isNonNexus": false, - "isPmCollect": false, - "isTrapDelayed": false, - "isVpcConfigured": false, - "is_smlic_enabled": false, - "keepAliveState": null, - "lastScanTime": 0, - "licenseDetail": null, - "licenseViolation": false, - "linkName": null, - "location": null, - "logicalName": "cvd-1314-leaf", - "managable": true, - "mds": false, - "membership": null, - "memoryUsage": 0, - "mgmtAddress": null, "mode": "Normal", - "model": "N9K-C93180YC-EX", - "modelType": 0, - "moduleIndexOffset": 9999, - "modules": null, - "monitorMode": true, - "name": null, - "network": null, - "nonMdsModel": null, - "npvEnabled": false, - "numberOfPorts": 0, - "operMode": null, - "operStatus": "Minor", - "peer": null, - "peerSerialNumber": null, - "peerSwitchDbId": 0, - "peerlinkState": null, - "ports": 0, - "present": true, - "primaryIP": "", - "primarySwitchDbID": 0, - "principal": null, - "protoDiscSettings": null, - "recvIntf": null, - "release": "10.2(5)", - "role": null, - "sanAnalyticsCapable": false, - "scope": null, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "sendIntf": null, - "serialNumber": "FDO211218FV", - "sourceInterface": "mgmt0", - "sourceVrf": "management", - "standbySupState": 0, - "status": "ok", - "swType": null, - "swUUID": "DCNM-UUID-132770", - "swUUIDId": 132770, - "swWwn": null, - "swWwnName": null, - "switchDbID": 502030, + "serialNumber": "FDO123456FV", "switchRole": "leaf", - "switchRoleEnum": "Leaf", - "sysDescr": "", - "systemMode": "Normal", - "uid": 0, - "unmanagableCause": "", - "upTime": 0, - "upTimeNumber": 0, - "upTimeStr": "98 days, 21:55:52", - "usedPorts": 0, - "username": null, - "vdcId": 0, - "vdcMac": null, - "vdcName": "", - "vendor": "Cisco", - "version": null, - "vpcDomain": 0, - "vrf": "management", - "vsanWwn": null, - "vsanWwnName": null, - "waitForSwitchModeChg": false, - "wwn": null + "systemMode": "Normal" } ], "MESSAGE": "OK", @@ -250,123 +164,25 @@ }, "test_maintenance_mode_info_00400a": { "TEST_NOTES": [ - "DATA contains switch with ip address 192.168.1.2" + "DATA[0].fabricName: VXLAN_Fabric", + "DATA[0].freezeMode: null", + "DATA[0].ipAddress: 192.168.1.2", + "DATA[0].mode: Normal", + "DATA[0].serialNumber: FDO211218FV", + "DATA[0].switchRole: leaf", + "DATA[0].systemMode: Normal", + "RETURN_CODE: 200", + "MESSAGE: OK" ], "DATA": [ { - "activeSupSlot": 0, - "availPorts": 0, - "ccStatus": "NA", - "cfsSyslogStatus": 1, - "colDBId": 0, - "connUnitStatus": 0, - "consistencyState": false, - "contact": null, - "cpuUsage": 0, - "deviceType": "External", - "displayHdrs": null, - "displayValues": null, - "domain": null, - "domainID": 0, - "elementType": null, - "fabricId": 3, - "fabricName": "FOO", - "fabricTechnology": "LANClassic", - "fcoeEnabled": false, - "fex": false, - "fexMap": {}, - "fid": 0, + "fabricName": "VXLAN_Fabric", "freezeMode": null, - "health": -1, - "hostName": "cvd-1314-leaf", - "index": 0, - "intentedpeerName": "", - "interfaces": null, "ipAddress": "192.168.1.2", - "ipDomain": "", - "isEchSupport": false, - "isLan": false, - "isNonNexus": false, - "isPmCollect": false, - "isTrapDelayed": false, - "isVpcConfigured": false, - "is_smlic_enabled": false, - "keepAliveState": null, - "lastScanTime": 0, - "licenseDetail": null, - "licenseViolation": false, - "linkName": null, - "location": null, - "logicalName": "cvd-1314-leaf", - "managable": true, - "mds": false, - "membership": null, - "memoryUsage": 0, - "mgmtAddress": null, "mode": "Normal", - "model": "N9K-C93180YC-EX", - "modelType": 0, - "moduleIndexOffset": 9999, - "modules": null, - "monitorMode": true, - "name": null, - "network": null, - "nonMdsModel": null, - "npvEnabled": false, - "numberOfPorts": 0, - "operMode": null, - "operStatus": "Minor", - "peer": null, - "peerSerialNumber": null, - "peerSwitchDbId": 0, - "peerlinkState": null, - "ports": 0, - "present": true, - "primaryIP": "", - "primarySwitchDbID": 0, - "principal": null, - "protoDiscSettings": null, - "recvIntf": null, - "release": "10.2(5)", - "role": null, - "sanAnalyticsCapable": false, - "scope": null, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "sendIntf": null, - "serialNumber": "FDO211218FV", - "sourceInterface": "mgmt0", - "sourceVrf": "management", - "standbySupState": 0, - "status": "ok", - "swType": null, - "swUUID": "DCNM-UUID-132770", - "swUUIDId": 132770, - "swWwn": null, - "swWwnName": null, - "switchDbID": 502030, + "serialNumber": "FDO123456FV", "switchRole": "leaf", - "switchRoleEnum": "Leaf", - "sysDescr": "", - "systemMode": "Normal", - "uid": 0, - "unmanagableCause": "", - "upTime": 0, - "upTimeNumber": 0, - "upTimeStr": "98 days, 21:55:52", - "usedPorts": 0, - "username": null, - "vdcId": 0, - "vdcMac": null, - "vdcName": "", - "vendor": "Cisco", - "version": null, - "vpcDomain": 0, - "vrf": "management", - "vsanWwn": null, - "vsanWwnName": null, - "waitForSwitchModeChg": false, - "wwn": null + "systemMode": "Normal" } ], "MESSAGE": "OK", @@ -376,127 +192,25 @@ }, "test_maintenance_mode_info_00500a": { "TEST_NOTES": [ - "DATA contains switch with ip address 192.168.1.2", - "fabricName is VXLAN_Fabric", - "freezeMode is null", - "switchRole is leaf", - "serialNumber is FDO211218FV" + "DATA[0].fabricName: VXLAN_Fabric", + "DATA[0].freezeMode: null", + "DATA[0].ipAddress: 192.168.1.2", + "DATA[0].mode: Normal", + "DATA[0].serialNumber: FDO211218FV", + "DATA[0].switchRole: leaf", + "DATA[0].systemMode: Normal", + "RETURN_CODE: 200", + "MESSAGE: OK" ], "DATA": [ { - "activeSupSlot": 0, - "availPorts": 0, - "ccStatus": "NA", - "cfsSyslogStatus": 1, - "colDBId": 0, - "connUnitStatus": 0, - "consistencyState": false, - "contact": null, - "cpuUsage": 0, - "deviceType": "External", - "displayHdrs": null, - "displayValues": null, - "domain": null, - "domainID": 0, - "elementType": null, - "fabricId": 3, "fabricName": "VXLAN_Fabric", - "fabricTechnology": "VXLANFabric", - "fcoeEnabled": false, - "fex": false, - "fexMap": {}, - "fid": 0, "freezeMode": null, - "health": -1, - "hostName": "cvd-1314-leaf", - "index": 0, - "intentedpeerName": "", - "interfaces": null, "ipAddress": "192.168.1.2", - "ipDomain": "", - "isEchSupport": false, - "isLan": false, - "isNonNexus": false, - "isPmCollect": false, - "isTrapDelayed": false, - "isVpcConfigured": false, - "is_smlic_enabled": false, - "keepAliveState": null, - "lastScanTime": 0, - "licenseDetail": null, - "licenseViolation": false, - "linkName": null, - "location": null, - "logicalName": "cvd-1314-leaf", - "managable": true, - "mds": false, - "membership": null, - "memoryUsage": 0, - "mgmtAddress": null, "mode": "Normal", - "model": "N9K-C93180YC-EX", - "modelType": 0, - "moduleIndexOffset": 9999, - "modules": null, - "monitorMode": true, - "name": null, - "network": null, - "nonMdsModel": null, - "npvEnabled": false, - "numberOfPorts": 0, - "operMode": null, - "operStatus": "Minor", - "peer": null, - "peerSerialNumber": null, - "peerSwitchDbId": 0, - "peerlinkState": null, - "ports": 0, - "present": true, - "primaryIP": "", - "primarySwitchDbID": 0, - "principal": null, - "protoDiscSettings": null, - "recvIntf": null, - "release": "10.2(5)", - "role": null, - "sanAnalyticsCapable": false, - "scope": null, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "sendIntf": null, "serialNumber": "FDO123456FV", - "sourceInterface": "mgmt0", - "sourceVrf": "management", - "standbySupState": 0, - "status": "ok", - "swType": null, - "swUUID": "DCNM-UUID-132770", - "swUUIDId": 132770, - "swWwn": null, - "swWwnName": null, - "switchDbID": 502030, "switchRole": "leaf", - "switchRoleEnum": "Leaf", - "sysDescr": "", - "systemMode": "Normal", - "uid": 0, - "unmanagableCause": "", - "upTime": 0, - "upTimeNumber": 0, - "upTimeStr": "98 days, 21:55:52", - "usedPorts": 0, - "username": null, - "vdcId": 0, - "vdcMac": null, - "vdcName": "", - "vendor": "Cisco", - "version": null, - "vpcDomain": 0, - "vrf": "management", - "vsanWwn": null, - "vsanWwnName": null, - "waitForSwitchModeChg": false, - "wwn": null + "systemMode": "Normal" } ], "MESSAGE": "OK", @@ -506,127 +220,25 @@ }, "test_maintenance_mode_info_00510a": { "TEST_NOTES": [ - "DATA contains switch with ip address 192.168.1.2", - "fabricName is VXLAN_Fabric", - "freezeMode is true", - "switchRole is leaf", - "serialNumber is FDO211218FV" + "DATA[0].fabricName: VXLAN_Fabric", + "DATA[0].freezeMode: true", + "DATA[0].ipAddress: 192.168.1.2", + "DATA[0].mode: Normal", + "DATA[0].serialNumber: FDO211218FV", + "DATA[0].switchRole: leaf", + "DATA[0].systemMode: Normal", + "RETURN_CODE: 200", + "MESSAGE: OK" ], "DATA": [ { - "activeSupSlot": 0, - "availPorts": 0, - "ccStatus": "NA", - "cfsSyslogStatus": 1, - "colDBId": 0, - "connUnitStatus": 0, - "consistencyState": false, - "contact": null, - "cpuUsage": 0, - "deviceType": "External", - "displayHdrs": null, - "displayValues": null, - "domain": null, - "domainID": 0, - "elementType": null, - "fabricId": 3, "fabricName": "VXLAN_Fabric", - "fabricTechnology": "VXLANFabric", - "fcoeEnabled": false, - "fex": false, - "fexMap": {}, - "fid": 0, "freezeMode": true, - "health": -1, - "hostName": "cvd-1314-leaf", - "index": 0, - "intentedpeerName": "", - "interfaces": null, "ipAddress": "192.168.1.2", - "ipDomain": "", - "isEchSupport": false, - "isLan": false, - "isNonNexus": false, - "isPmCollect": false, - "isTrapDelayed": false, - "isVpcConfigured": false, - "is_smlic_enabled": false, - "keepAliveState": null, - "lastScanTime": 0, - "licenseDetail": null, - "licenseViolation": false, - "linkName": null, - "location": null, - "logicalName": "cvd-1314-leaf", - "managable": true, - "mds": false, - "membership": null, - "memoryUsage": 0, - "mgmtAddress": null, "mode": "Normal", - "model": "N9K-C93180YC-EX", - "modelType": 0, - "moduleIndexOffset": 9999, - "modules": null, - "monitorMode": true, - "name": null, - "network": null, - "nonMdsModel": null, - "npvEnabled": false, - "numberOfPorts": 0, - "operMode": null, - "operStatus": "Minor", - "peer": null, - "peerSerialNumber": null, - "peerSwitchDbId": 0, - "peerlinkState": null, - "ports": 0, - "present": true, - "primaryIP": "", - "primarySwitchDbID": 0, - "principal": null, - "protoDiscSettings": null, - "recvIntf": null, - "release": "10.2(5)", - "role": null, - "sanAnalyticsCapable": false, - "scope": null, - "secondaryIP": "", - "secondarySwitchDbID": 0, - "sendIntf": null, "serialNumber": "FDO123456FV", - "sourceInterface": "mgmt0", - "sourceVrf": "management", - "standbySupState": 0, - "status": "ok", - "swType": null, - "swUUID": "DCNM-UUID-132770", - "swUUIDId": 132770, - "swWwn": null, - "swWwnName": null, - "switchDbID": 502030, "switchRole": "leaf", - "switchRoleEnum": "Leaf", - "sysDescr": "", - "systemMode": "Normal", - "uid": 0, - "unmanagableCause": "", - "upTime": 0, - "upTimeNumber": 0, - "upTimeStr": "98 days, 21:55:52", - "usedPorts": 0, - "username": null, - "vdcId": 0, - "vdcMac": null, - "vdcName": "", - "vendor": "Cisco", - "version": null, - "vpcDomain": 0, - "vrf": "management", - "vsanWwn": null, - "vsanWwnName": null, - "waitForSwitchModeChg": false, - "wwn": null + "systemMode": "Normal" } ], "MESSAGE": "OK", @@ -636,29 +248,25 @@ }, "test_maintenance_mode_info_00520a": { "TEST_NOTES": [ - "DATA contains switch with ip address 192.168.1.2", - "fabricName is VXLAN_Fabric", - "freezeMode is true", - "switchRole is leaf", - "serialNumber is FDO211218FV" + "DATA[0].fabricName: VXLAN_Fabric", + "DATA[0].freezeMode: true", + "DATA[0].ipAddress: 192.168.1.2", + "DATA[0].mode: Normal", + "DATA[0].serialNumber: FDO211218FV", + "DATA[0].switchRole: leaf", + "DATA[0].systemMode: Maintenance", + "RETURN_CODE: 200", + "MESSAGE: OK" ], "DATA": [ { "fabricName": "VXLAN_Fabric", "freezeMode": true, - "hostName": "cvd-1314-leaf", "ipAddress": "192.168.1.2", - "logicalName": "cvd-1314-leaf", - "managable": true, "mode": "Normal", - "model": "N9K-C93180YC-EX", - "operStatus": "Minor", - "present": true, - "release": "10.2(5)", - "role": null, "serialNumber": "FDO123456FV", - "status": "ok", - "switchRole": "leaf" + "switchRole": "leaf", + "systemMode": "Maintenance" } ], "MESSAGE": "OK", diff --git a/tests/unit/module_utils/common/test_maintenance_mode_info.py b/tests/unit/module_utils/common/test_maintenance_mode_info.py index 6e7bd92d8..472d767ed 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode_info.py +++ b/tests/unit/module_utils/common/test_maintenance_mode_info.py @@ -44,14 +44,15 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender from ansible_collections.cisco.dcnm.tests.unit.mocks.mock_fabric_details_by_name import \ MockFabricDetailsByName from ansible_collections.cisco.dcnm.tests.unit.mocks.mock_switch_details import \ MockSwitchDetails from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( - MockSender, ResponseGenerator, does_not_raise, - maintenance_mode_info_fixture, responses_fabric_details_by_name, - responses_switch_details) + ResponseGenerator, does_not_raise, maintenance_mode_info_fixture, + responses_fabric_details_by_name, responses_switch_details) FABRIC_NAME = "VXLAN_Fabric" CONFIG = ["192.168.1.2"] @@ -60,13 +61,26 @@ def test_maintenance_mode_info_00000(maintenance_mode_info) -> None: """ - Classes and Methods - - MaintenanceModeInfo - - __init__() + ### Classes and Methods + - ``MaintenanceModeInfo()`` + - ``__init__()`` + + ### Summary + - Verify the __init__() method. + + ### Setup - Data + - None + + ### Setup - Code + - None + + ### Trigger + - ``MaintenanceModeInfo`` is instantiated. + + ### Expected Result + - Class attributes are initialized to expected values. + - Exception is not raised. - Test - - Class attributes are initialized to expected values - - Exception is not raised """ with does_not_raise(): instance = maintenance_mode_info @@ -87,26 +101,27 @@ def test_maintenance_mode_info_00000(maintenance_mode_info) -> None: def test_maintenance_mode_info_00100(maintenance_mode_info) -> None: """ ### Classes and Methods - - MaintenanceModeInfo() - - __init__() - - verify_refresh_parameters() - - refresh() + - ``MaintenanceModeInfo()`` + - ``verify_refresh_parameters()`` + - ``refresh()`` ### Summary - Verify MaintenanceModeInfo().refresh() raises ``ValueError`` when ``config`` is not set. - ### Code Flow - Setup - - MaintenanceModeInfo() is instantiated. + ### Setup - Data + - None + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated. - Other required attributes are set. - ### Code Flow - Test - - ``MaintenanceModeInfo().refresh()`` is called without having first set - ``MaintenanceModeInfo().config``. + ### Trigger + - ``refresh()`` is called without having first set ``config``. ### Expected Result - ``ValueError`` is raised. - - Exception message matches expected. + - Exception message matches expectations. """ with does_not_raise(): instance = maintenance_mode_info @@ -123,25 +138,27 @@ def test_maintenance_mode_info_00100(maintenance_mode_info) -> None: def test_maintenance_mode_info_00110(maintenance_mode_info) -> None: """ ### Classes and Methods - - ``MaintenanceModeInfo()`` - - __init__() - - verify_refresh_parameters() - - refresh() + - ``MaintenanceModeInfo()`` + - ``verify_refresh_parameters()`` + - ``refresh()`` ### Summary - Verify ``refresh()`` raises ``ValueError`` when ``rest_send`` is not set. - ### Code Flow - Setup - - ``MaintenanceModeInfo()`` is instantiated - - Other required attributes are set + ### Setup - Data + - None + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated. + - Other required attributes are set. - Code Flow - Test + ### Trigger - ``refresh()`` is called without having first set ``rest_send``. ### Expected Result - ``ValueError`` is raised. - - Exception message matches expected. + - Exception message matches expectations. """ with does_not_raise(): instance = maintenance_mode_info @@ -158,24 +175,26 @@ def test_maintenance_mode_info_00110(maintenance_mode_info) -> None: def test_maintenance_mode_info_00120(maintenance_mode_info) -> None: """ ### Classes and Methods - - MaintenanceModeInfo() - - __init__() - - verify_refresh_parameters() - - refresh() + - ``MaintenanceModeInfo()`` + - ``verify_refresh_parameters()`` + - ``refresh()`` ### Summary - Verify ``refresh()`` raises ``ValueError`` when ``results`` is not set. - ### Code Flow - Setup + ### Setup - Data + - None + + ### Setup - Code - ``MaintenanceModeInfo()`` is instantiated. - Other required attributes are set. - ### Code Flow - Test + ### Trigger - ``refresh()`` is called without having first set ``results``. ### Expected Result - ``ValueError`` is raised. - - Exception message matches expected. + - Exception message matches expectations. """ with does_not_raise(): instance = maintenance_mode_info @@ -246,9 +265,8 @@ def test_maintenance_mode_info_00200( ) -> None: """ ### Classes and Methods - - MaintenanceModeInfo() - - __init__() - - refresh() + - ``MaintenanceModeInfo()`` + - ``refresh()`` ### Summary - Verify ``refresh()`` raises ``ValueError`` when: @@ -257,18 +275,21 @@ def test_maintenance_mode_info_00200( - ``switch_details`` properties ``rest_send`` and ``results`` raise ``TypeError``. - ### Code Flow - Setup + ### Setup - Data + - None + + ### Setup - Code - ``MaintenanceModeInfo()`` is instantiated - Required attributes are set - ``FabricDetails()`` is mocked to conditionally raise ``TypeError``. - ``SwitchDetails()`` is mocked to conditionally raise ``TypeError``. - ### Code Flow - Test + ### Trigger - ``refresh()`` is called. ### Expected Result - ``ValueError`` is raised. - - Exception message matches expected. + - Exception message matches expectations. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -276,11 +297,11 @@ def test_maintenance_mode_info_00200( def responses(): yield responses_switch_details(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) rest_send = RestSend({"state": "query", "check_mode": False}) rest_send.response_handler = ResponseHandler() - rest_send.sender = mock_sender + rest_send.sender = sender with does_not_raise(): instance = MaintenanceModeInfo(PARAMS) @@ -302,7 +323,7 @@ def responses(): with does_not_raise(): instance.config = CONFIG - instance.rest_send = rest_send + instance.rest_send = RestSend({"state": "query", "check_mode": False}) instance.results = Results() with pytest.raises(expected_exception, match=mock_message): @@ -332,26 +353,29 @@ def test_maintenance_mode_info_00210( """ ### Classes and Methods - MaintenanceModeInfo() - - __init__() - refresh() ### Summary - - Verify ``refresh()`` raises ``ValueError`` when: - - ``switch_details.serial_number`` raises ``ValueError``. + - Verify ``refresh()`` raises ``ValueError`` when + ``switch_details.serial_number`` raises ``ValueError``. - ### Code Flow - Setup + ### Setup - Data + - ``responses_SwitchDetails.json``: + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code - ``MaintenanceModeInfo()`` is instantiated - Required attributes are set - - ``FabricDetails()`` is mocked not to raise any exceptions. - - ``SwitchDetails()`` is mocked to conditionally raise ``ValueError``. - in the ``serial_number.getter`` property. + - ``SwitchDetails()`` is mocked to conditionally raise + ``ValueError`` in the ``serial_number.getter`` property. - ### Code Flow - Test + ### Trigger - ``refresh()`` is called. ### Expected Result - ``ValueError`` is raised. - - Exception message matches expected. + - Exception message matches expectations. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -359,24 +383,21 @@ def test_maintenance_mode_info_00210( def responses(): yield responses_switch_details(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) rest_send = RestSend({"state": "query", "check_mode": False}) rest_send.response_handler = ResponseHandler() - rest_send.sender = mock_sender + rest_send.sender = sender with does_not_raise(): instance = MaintenanceModeInfo(PARAMS) - mock_fabric_details = MockFabricDetailsByName() - mock_switch_details = MockSwitchDetails() mock_switch_details.mock_class = mock_class mock_switch_details.mock_exception = mock_exception mock_switch_details.mock_message = mock_message mock_switch_details.mock_property = mock_property - monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) monkeypatch.setattr(instance, "switch_details", mock_switch_details) with does_not_raise(): @@ -388,9 +409,7 @@ def responses(): instance.refresh() -def test_maintenance_mode_info_00300( - monkeypatch, -) -> None: +def test_maintenance_mode_info_00300() -> None: """ ### Classes and Methods - MaintenanceModeInfo() @@ -398,54 +417,59 @@ def test_maintenance_mode_info_00300( - refresh() ### Summary - - Verify ``refresh()`` raises ``ValueError`` when: - ``switch_details.serial_number`` is ``None``. This happens - when the switch does not exist on the controller. - - ### Code Flow - Setup + Verify ``refresh()`` raises ``ValueError`` when + ``switch_details.serial_number`` is ``None``. + + This happens when the switch does not exist on the controller and causes + SwitchDetails()._get() to raise a ``ValueError``. + + ### Setup - Data + - ``ipAddress`` is set to something other than 192.168.1.2 + - ``responses_SwitchDetails.json``: + - "DATA[0].fabricName: VXLAN_Fabric", + - "DATA[0].freezeMode: null", + - "DATA[0].ipAddress: 192.168.1.1", + - "DATA[0].mode: Normal", + - "DATA[0].serialNumber: FDO211218FV", + - "DATA[0].switchRole: leaf", + - "DATA[0].systemMode: Normal" + - RETURN_CODE: 200 + - MESSAGE: OK + - ``responses_FabricDetailsByName.json``: + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code - ``MaintenanceModeInfo()`` is instantiated - Required attributes are set - - ``FabricDetails()`` is mocked not to raise any exceptions. - - ``SwitchDetails()`` is mocked not to raise any exceptions. - - ``responses_SwitchDetails.json`` contains a 200 response that - does not contain the switch ip address in CONFIG (192.168.1.2) - ### Code Flow - Test + ### Trigger - ``refresh()`` is called. ### Expected Result - ``ValueError`` is raised. - - Exception message matches expected. + - Exception message matches expectations. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" def responses(): - pass + yield responses_switch_details(key) + yield responses_fabric_details_by_name(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) rest_send = RestSend({"state": "query", "check_mode": False}) rest_send.response_handler = ResponseHandler() - rest_send.sender = mock_sender + rest_send.sender = sender with does_not_raise(): instance = MaintenanceModeInfo(PARAMS) - - mock_fabric_details = MockFabricDetailsByName() - mock_switch_details = MockSwitchDetails() - mock_switch_details.filter = CONFIG[0] - mock_switch_details.mock_response_key = key - - monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) - monkeypatch.setattr(instance, "switch_details", mock_switch_details) - - with does_not_raise(): instance.config = CONFIG instance.rest_send = rest_send instance.results = Results() - match = r"MaintenanceModeInfo\.refresh:\s+" + match = r"SwitchDetails\._get:\s+" match += r"Switch with ip_address 192\.168\.1\.2\s+" match += r"does not exist on the controller\." with pytest.raises(ValueError, match=match): @@ -475,39 +499,48 @@ def test_maintenance_mode_info_00400( """ ### Classes and Methods - MaintenanceModeInfo() - - __init__() - refresh() ### Summary - - Verify ``refresh()`` raises ``ValueError`` when: - - ``fabric_details.filter`` raises ``ValueError``. + - Verify ``refresh()`` raises ``ValueError`` when + ``fabric_details.filter`` raises ``ValueError``. + + ### Setup - Data + - ``responses_SwitchDetails.json``: + - "DATA[0].fabricName: VXLAN_Fabric", + - "DATA[0].freezeMode: null", + - "DATA[0].ipAddress: 192.168.1.2", + - "DATA[0].mode: Normal", + - "DATA[0].serialNumber: FDO211218FV", + - "DATA[0].switchRole: leaf", + - "DATA[0].systemMode: Normal" + - RETURN_CODE: 200 + - MESSAGE: OK ### Code Flow - Setup - - ``MaintenanceModeInfo()`` is instantiated - - Required attributes are set - - ``FabricDetails().filter`` is mocked to conditionally raise ``ValueError``. - - ``SwitchDetails()`` is mocked not to raise any exceptions. - - ``responses_SwitchDetails.json`` contains a 200 response that - contains the switch ip address in CONFIG (192.168.1.2) + - ``MaintenanceModeInfo()`` is instantiated. + - Required attributes are set. + - ``FabricDetailsByName().filter`` is mocked to conditionally raise + ``ValueError``. - ### Code Flow - Test + ### Trigger - ``refresh()`` is called. ### Expected Result - ``ValueError`` is raised. - - Exception message matches expected. + - Exception message matches expectations. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" def responses(): - pass + yield responses_switch_details(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) rest_send = RestSend({"state": "query", "check_mode": False}) rest_send.response_handler = ResponseHandler() - rest_send.sender = mock_sender + rest_send.sender = sender with does_not_raise(): instance = MaintenanceModeInfo(PARAMS) @@ -518,12 +551,7 @@ def responses(): mock_fabric_details.mock_message = mock_message mock_fabric_details.mock_property = mock_property - mock_switch_details = MockSwitchDetails() - mock_switch_details.filter = CONFIG[0] - mock_switch_details.mock_response_key = key - monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) - monkeypatch.setattr(instance, "switch_details", mock_switch_details) with does_not_raise(): instance.config = CONFIG @@ -534,27 +562,36 @@ def responses(): instance.refresh() -def test_maintenance_mode_info_00500(monkeypatch) -> None: +def test_maintenance_mode_info_00500() -> None: """ ### Classes and Methods - MaintenanceModeInfo() - - __init__() - refresh() ### Summary - - Verify happy path with freezeMode == False - - ### Code Flow - Setup + - Verify when ``freezeMode`` == null in the response, + ``freezeMode`` is set to False. + + ### Setup - Data + - ``responses_SwitchDetails.json``: + - "DATA[0].fabricName: VXLAN_Fabric", + - "DATA[0].freezeMode: null", + - "DATA[0].ipAddress: 192.168.1.2", + - "DATA[0].mode: Normal", + - "DATA[0].serialNumber: FDO211218FV", + - "DATA[0].switchRole: leaf", + - "DATA[0].systemMode: Normal" + - RETURN_CODE: 200 + - MESSAGE: OK + - ``responses_FabricDetailsByName.json``: + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code - ``MaintenanceModeInfo()`` is instantiated - Required attributes are set - - ``FabricDetails()`` is mocked not to raise any exceptions. - - ``SwitchDetails()`` is mocked not to raise any exceptions. - - ``responses_SwitchDetails.json`` contains a 200 response that - contains the switch ip address in CONFIG (192.168.1.2) - - ``responses_FabricDetailsByName.json`` contains a 200 response that - contains FABRIC_NAME. - - ### Code Flow - Test + + ### Trigger - ``refresh()`` is called. ### Expected Result @@ -566,26 +603,17 @@ def test_maintenance_mode_info_00500(monkeypatch) -> None: key = f"{method_name}a" def responses(): + yield responses_switch_details(key) yield responses_fabric_details_by_name(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) rest_send = RestSend({"state": "query", "check_mode": False}) rest_send.response_handler = ResponseHandler() - rest_send.sender = mock_sender + rest_send.sender = sender with does_not_raise(): instance = MaintenanceModeInfo(PARAMS) - - mock_fabric_details = MockFabricDetailsByName() - mock_switch_details = MockSwitchDetails() - mock_switch_details.filter = CONFIG[0] - mock_switch_details.mock_response_key = key - - monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) - monkeypatch.setattr(instance, "switch_details", mock_switch_details) - - with does_not_raise(): instance.config = CONFIG instance.rest_send = rest_send instance.results = Results() @@ -599,7 +627,7 @@ def responses(): assert instance.role == "leaf" -def test_maintenance_mode_info_00510(monkeypatch) -> None: +def test_maintenance_mode_info_00510() -> None: """ ### Classes and Methods - MaintenanceModeInfo() @@ -609,19 +637,27 @@ def test_maintenance_mode_info_00510(monkeypatch) -> None: ### Summary - Verify happy path with: - switch_details: freezeMode is True - - fabric_details: IS_READ_ONLY not present - ### Code Flow - Setup + ### Setup - Data + - ``responses_SwitchDetails.json``: + - "DATA[0].fabricName: VXLAN_Fabric", + - "DATA[0].freezeMode: true", + - "DATA[0].ipAddress: 192.168.1.2", + - "DATA[0].mode: Normal", + - "DATA[0].serialNumber: FDO211218FV", + - "DATA[0].switchRole: leaf", + - "DATA[0].systemMode: Normal" + - RETURN_CODE: 200 + - MESSAGE: OK + - ``responses_FabricDetailsByName.json``: + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code - ``MaintenanceModeInfo()`` is instantiated - Required attributes are set - - ``FabricDetails()`` is mocked not to raise any exceptions. - - ``SwitchDetails()`` is mocked not to raise any exceptions. - - ``responses_SwitchDetails.json`` contains a 200 response that - contains the switch ip address in CONFIG (192.168.1.2) - - ``responses_FabricDetailsByName.json`` contains a 200 response that - contains FABRIC_NAME. - - ### Code Flow - Test + + ### Trigger - ``refresh()`` is called. ### Expected Result @@ -633,26 +669,17 @@ def test_maintenance_mode_info_00510(monkeypatch) -> None: key = f"{method_name}a" def responses(): - pass + yield responses_switch_details(key) + yield responses_fabric_details_by_name(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) rest_send = RestSend({"state": "query", "check_mode": False}) rest_send.response_handler = ResponseHandler() - rest_send.sender = mock_sender + rest_send.sender = sender with does_not_raise(): instance = MaintenanceModeInfo(PARAMS) - - mock_fabric_details = MockFabricDetailsByName() - mock_switch_details = MockSwitchDetails() - mock_switch_details.filter = CONFIG[0] - mock_switch_details.mock_response_key = key - - monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) - monkeypatch.setattr(instance, "switch_details", mock_switch_details) - - with does_not_raise(): instance.config = CONFIG instance.rest_send = rest_send instance.results = Results() @@ -666,7 +693,7 @@ def responses(): assert instance.role == "leaf" -def test_maintenance_mode_info_00520(monkeypatch) -> None: +def test_maintenance_mode_info_00520() -> None: """ ### Classes and Methods - MaintenanceModeInfo() @@ -674,895 +701,61 @@ def test_maintenance_mode_info_00520(monkeypatch) -> None: - refresh() ### Summary - - Verify happy path with: - - switch_details: freezeMode is True - - switch_details: mode is Normal - - fabric_details: IS_READ_ONLY present and True - - fabric_details: DEPLOYMENT_FREEZE present and True - - ### Code Flow - Setup + - Verify: + - ``mode`` == "inconsistent" when ``mode`` != ``systemMode``. + + ### Setup - Data + - ``responses_SwitchDetails.json``: + - DATA[0].fabricName: VXLAN_Fabric + - DATA[0].freezeMode: true + - DATA[0].ipAddress: 192.168.1.2 + - DATA[0].mode: Normal + - DATA[0].serialNumber: FDO211218FV + - DATA[0].switchRole: leaf + - DATA[0].systemMode: Maintenance + - RETURN_CODE: 200 + - MESSAGE: OK + - ``responses_FabricDetailsByName.json``: + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code - ``MaintenanceModeInfo()`` is instantiated - Required attributes are set - - ``FabricDetails()`` is mocked not to raise any exceptions. - - ``SwitchDetails()`` is mocked not to raise any exceptions. - - ``responses_SwitchDetails.json`` contains a 200 response that - contains the switch ip address in CONFIG (192.168.1.2) - - ``responses_FabricDetailsByName.json`` contains a 200 response that - contains FABRIC_NAME. - - ### Code Flow - Test + + ### Trigger - ``refresh()`` is called. ### Expected Result + - Conditions in Summary are confirmed. - Exception is not raised. - ``MaintenanceModeInfo().results`` contains expected data. - - mode is "inconsistent" due to mode differing from freezeMode. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" def responses(): - pass + yield responses_switch_details(key) + yield responses_fabric_details_by_name(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) rest_send = RestSend({"state": "query", "check_mode": False}) rest_send.response_handler = ResponseHandler() - rest_send.sender = mock_sender + rest_send.sender = sender with does_not_raise(): instance = MaintenanceModeInfo(PARAMS) - - mock_fabric_details = MockFabricDetailsByName() - mock_fabric_details.mock_response_key = key - mock_fabric_details.filter = "VXLAN_Fabric" - - mock_switch_details = MockSwitchDetails() - mock_switch_details.mock_response_key = key - mock_switch_details.filter = CONFIG[0] - - monkeypatch.setattr(instance, "fabric_details", mock_fabric_details) - monkeypatch.setattr(instance, "switch_details", mock_switch_details) - - with does_not_raise(): instance.config = CONFIG instance.rest_send = rest_send instance.results = Results() instance.refresh() instance.filter = CONFIG[0] - assert instance.fabric_name == FABRIC_NAME - assert instance.fabric_freeze_mode is True - assert instance.fabric_read_only is True - assert instance.fabric_deployment_disabled is True assert instance.mode == "inconsistent" - assert instance.role == "leaf" - - -# @pytest.mark.parametrize( -# "mock_exception, expected_exception, mock_message", -# [ -# (ControllerResponseError, ValueError, "Bad controller response"), -# (ValueError, ValueError, "Bad value"), -# ], -# ) -# def test_maintenance_mode_info_00210( -# monkeypatch, maintenance_mode_info, mock_exception, expected_exception, mock_message -# ) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - refresh() - -# Summary -# - Verify MaintenanceModeInfo().refresh() raises ``ValueError`` when -# ``MaintenanceModeInfo().deploy_switches`` raises any of: -# - ``ControllerResponseError`` -# - ``ValueError`` - - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated -# - Required attributes are set -# - change_system_mode() is mocked to do nothing -# - deploy_switches() is mocked to raise each of the above exceptions - -# Code Flow - Test -# - MaintenanceModeInfo().refresh() is called for each exception - -# Expected Result -# - ``ValueError`` is raised -# - Exception message matches expected -# """ - -# def mock_change_system_mode(*args, **kwargs): -# pass - -# def mock_deploy_switches(*args, **kwargs): -# raise mock_exception(mock_message) - -# with does_not_raise(): -# instance = maintenance_mode_info -# instance.config = CONFIG -# instance.rest_send = RestSend({}) -# instance.results = Results() - -# monkeypatch.setattr(instance, "change_system_mode", mock_change_system_mode) -# monkeypatch.setattr(instance, "deploy_switches", mock_deploy_switches) -# with pytest.raises(expected_exception, match=mock_message): -# instance.refresh() - - -# @pytest.mark.parametrize( -# "mode, deploy", -# [ -# ("maintenance", True), -# ("maintenance", False), -# ("normal", True), -# ("normal", False), -# ], -# ) -# def test_maintenance_mode_info_00220(maintenance_mode_info, mode, deploy) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - refresh() -# - change_system_mode() -# - deploy_switches() - -# Summary -# - Verify refresh() success case: -# - RETURN_CODE is 200. -# - Controller response contains expected structure and values. - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated -# - Sender() is mocked to return expected responses -# - Required attributes are set -# - MaintenanceModeInfo().refresh() is called -# - responses_MaintenanceMode contains a dict with: -# - RETURN_CODE == 200 -# - DATA == {"status": "Success"} - -# Code Flow - Test -# - MaintenanceModeInfo().refresh() is called - -# Expected Result -# - Exception is not raised -# - instance.response_data returns expected data -# - MaintenanceModeInfo()._properties are updated -# """ -# method_name = inspect.stack()[0][3] -# key = f"{method_name}a" - -# def responses(): -# yield responses_maintenance_mode_info(key) - -# mock_sender = MockSender() -# mock_sender.gen = ResponseGenerator(responses()) - -# config = copy.deepcopy(CONFIG[0]) -# config["mode"] = mode -# config["deploy"] = deploy - -# with does_not_raise(): -# rest_send = RestSend({"state": "merged", "check_mode": False}) -# rest_send.sender = mock_sender -# rest_send.response_handler = ResponseHandler() -# instance = maintenance_mode_info -# instance.rest_send = rest_send -# instance.rest_send.unit_test = True -# instance.rest_send.timeout = 1 -# instance.results = Results() -# instance.config = [config] - -# with does_not_raise(): -# instance.refresh() - -# assert isinstance(instance.results.diff, list) -# assert isinstance(instance.results.metadata, list) -# assert isinstance(instance.results.response, list) -# assert isinstance(instance.results.result, list) -# assert instance.results.diff[0].get("fabric_name", None) == FABRIC_NAME -# assert instance.results.diff[0].get("ip_address", None) == "192.168.1.2" -# assert instance.results.diff[0].get("maintenance_mode", None) == mode -# assert instance.results.diff[0].get("sequence_number", None) == 1 -# assert instance.results.diff[0].get("serial_number", None) == "FDO22180ASJ" - -# assert instance.results.diff[1].get("config_deploy", None) is True -# assert instance.results.diff[1].get("sequence_number", None) == 2 - -# assert instance.results.metadata[0].get("action", None) == "change_sytem_mode" -# assert instance.results.metadata[0].get("sequence_number", None) == 1 -# assert instance.results.metadata[0].get("state", None) == "merged" - -# assert instance.results.metadata[1].get("action", None) == "config_deploy" -# assert instance.results.metadata[1].get("sequence_number", None) == 2 -# assert instance.results.metadata[1].get("state", None) == "merged" - -# assert instance.results.response[0].get("DATA", {}).get("status") == "Success" -# assert instance.results.response[0].get("MESSAGE", None) == "OK" -# assert instance.results.response[0].get("RETURN_CODE", None) == 200 -# assert instance.results.response[0].get("METHOD", None) == "POST" - -# value = "Configuration deployment completed." -# assert instance.results.response[1].get("DATA", {}).get("status") == value -# assert instance.results.response[1].get("MESSAGE", None) == "OK" -# assert instance.results.response[1].get("RETURN_CODE", None) == 200 -# assert instance.results.response[1].get("METHOD", None) == "POST" - -# assert instance.results.result[0].get("changed", None) is True -# assert instance.results.result[0].get("success", None) is True - -# assert instance.results.result[1].get("changed", None) is True -# assert instance.results.result[1].get("success", None) is True - - -# @pytest.mark.parametrize( -# "mode", -# [ -# ("maintenance"), -# ("normal"), -# ], -# ) -# def test_maintenance_mode_info_00230(maintenance_mode_info, mode) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - refresh() -# - change_system_mode() -# - deploy_switches() - -# Summary -# - Verify refresh() unsuccessful case: -# - RETURN_CODE == 500. -# - refresh raises ``ValueError`` when change_system_mode() raises -# ``ControllerResponseError``. -# - Controller response contains expected structure and values. - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated -# - Sender() is mocked to return expected responses -# - Required attributes are set -# - MaintenanceModeInfo().refresh() is called -# - responses_MaintenanceMode contains a dict with: -# - RETURN_CODE == 500 -# - DATA == {"status": "Failure"} - -# Code Flow - Test -# - ``MaintenanceModeInfo().refresh()`` is called -# - ``change_system_mode()`` raises ``ControllerResponseError`` -# - ``refresh()`` raises ``ValueError`` - -# Expected Result -# - ``refresh()`` raises ``ValueError`` -# - instance.response_data returns expected data -# - MaintenanceModeInfo()._properties are updated -# """ -# method_name = inspect.stack()[0][3] -# key = f"{method_name}a" - -# def responses(): -# yield responses_maintenance_mode_info(key) -# # yield responses_config_deploy(key) - -# mock_sender = MockSender() -# mock_sender.gen = ResponseGenerator(responses()) - -# config = copy.deepcopy(CONFIG[0]) -# config["mode"] = mode - -# with does_not_raise(): -# rest_send = RestSend({"state": "merged", "check_mode": False}) -# rest_send.sender = mock_sender -# rest_send.response_handler = ResponseHandler() -# instance = maintenance_mode_info -# instance.rest_send = rest_send -# instance.rest_send.unit_test = True -# instance.rest_send.timeout = 1 -# instance.results = Results() -# instance.config = [config] - -# match = r"MaintenanceMode\.change_system_mode:\s+" -# match += r"Unable to change system mode on switch:\s+" -# match += rf"fabric_name {config['fabric_name']},\s+" -# match += rf"ip_address {config['ip_address']},\s+" -# match += rf"serial_number {config['serial_number']}\.\s+" -# match += r"Got response\s+.*" -# with pytest.raises(ValueError, match=match): -# instance.refresh() - -# assert isinstance(instance.results.diff, list) -# assert isinstance(instance.results.metadata, list) -# assert isinstance(instance.results.response, list) -# assert isinstance(instance.results.result, list) -# assert len(instance.results.diff[0]) == 1 - -# assert instance.results.metadata[0].get("action", None) == "change_sytem_mode" -# assert instance.results.metadata[0].get("sequence_number", None) == 1 -# assert instance.results.metadata[0].get("state", None) == "merged" - -# assert instance.results.response[0].get("DATA", {}).get("status") == "Failure" -# assert instance.results.response[0].get("MESSAGE", None) == "Internal Server Error" -# assert instance.results.response[0].get("RETURN_CODE", None) == 500 -# assert instance.results.response[0].get("METHOD", None) == "POST" - -# assert instance.results.result[0].get("changed", None) is False -# assert instance.results.result[0].get("success", None) is False - - -# def test_maintenance_mode_info_00300(maintenance_mode_info) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - verify_config_parameters() -# - config.setter - -# Summary -# - Verify MaintenanceModeInfo().verify_config_parameters() raises -# - ``TypeError`` if: -# - value is not a list -# - Verify MaintenanceModeInfo().config.setter re-raises: -# - ``TypeError`` as ``ValueError`` - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated -# - config is set to a non-list value - -# Code Flow - Test -# - MaintenanceModeInfo().config.setter is accessed with non-list - -# Expected Result -# - verify_config_parameters() raises ``TypeError``. -# - config.setter re-raises as ``ValueError``. -# - Exception message matches expected. -# """ -# with does_not_raise(): -# instance = maintenance_mode_info -# match = r"MaintenanceMode\.verify_config_parameters:\s+" -# match += r"MaintenanceMode\.config must be a list\.\s+" -# match += r"Got type: str\." -# with pytest.raises(ValueError, match=match): -# instance.config = "NOT_A_LIST" - - -# @pytest.mark.parametrize( -# "remove_param", -# [("deploy"), ("fabric_name"), ("ip_address"), ("mode"), ("serial_number")], -# ) -# def test_maintenance_mode_info_00310(maintenance_mode_info, remove_param) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - verify_config_parameters() -# - config.setter - -# Summary -# - Verify MaintenanceModeInfo().verify_config_parameters() raises -# - ``ValueError`` if: -# - deploy is missing from config -# - fabric_name is missing from config -# - ip_address is missing from config -# - mode is missing from config -# - serial_number is missing from config - - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated - -# Code Flow - Test -# - MaintenanceModeInfo().config is set to a dict with all of the above -# keys present, except that each key, in turn, is removed. - -# Expected Result -# - ``ValueError`` is raised -# - Exception message matches expected -# """ - -# with does_not_raise(): -# instance = maintenance_mode_info - -# config = copy.deepcopy(CONFIG[0]) -# del config[remove_param] -# match = rf"MaintenanceMode\.verify_{remove_param}:\s+" -# match += rf"config is missing mandatory key: {remove_param}\." -# with pytest.raises(ValueError, match=match): -# instance.config = [config] - - -# @pytest.mark.parametrize( -# "param, raises", -# [ -# (False, None), -# (True, None), -# (10, ValueError), -# ("FOO", ValueError), -# (["FOO"], ValueError), -# ({"FOO": "BAR"}, ValueError), -# ], -# ) -# def test_maintenance_mode_info_00400(maintenance_mode_info, param, raises) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - verify_config_parameters() -# - config.setter - -# Summary -# - Verify MaintenanceModeInfo().verify_config_parameters() re-raises -# - ``ValueError`` if: -# - ``deploy`` raises ``TypeError`` - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated - -# Code Flow - Test -# - MaintenanceModeInfo().config is set to a dict. -# - The dict is updated with deploy set to valid and invalid -# values of ``deploy`` - -# Expected Result -# - ``ValueError`` is raised when deploy is not a boolean -# - Exception message matches expected -# - Exception is not raised when deploy is a boolean -# """ - -# with does_not_raise(): -# instance = maintenance_mode_info - -# config = copy.deepcopy(CONFIG[0]) -# config["deploy"] = param -# match = r"MaintenanceMode\.verify_deploy:\s+" -# match += r"Expected boolean for deploy\.\s+" -# match += r"Got type\s+" -# if raises: -# with pytest.raises(raises, match=match): -# instance.config = [config] -# else: -# instance.config = [config] -# assert instance.config[0]["deploy"] == param - - -# @pytest.mark.parametrize( -# "param, raises", -# [ -# ("MyFabric", None), -# ("MyFabric_123", None), -# ("10MyFabric", ValueError), -# ("_MyFabric", ValueError), -# ("MyFabric&BadFabric", ValueError), -# ], -# ) -# def test_maintenance_mode_info_00500(maintenance_mode_info, param, raises) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - verify_config_parameters() -# - config.setter - -# Summary -# - Verify MaintenanceModeInfo().verify_config_parameters() re-raises -# - ``ValueError`` if: -# - ``fabric_name`` raises ``ValueError`` due to being an -# invalid value. - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated - -# Code Flow - Test -# - MaintenanceModeInfo().config is set to a dict. -# - The dict is updated with fabric_name set to valid and invalid -# values of ``fabric_name`` - -# Expected Result -# - ``ValueError`` is raised when fabric_name is not a valid value -# - Exception message matches expected -# - Exception is not raised when fabric_name is a valid value -# """ - -# with does_not_raise(): -# instance = maintenance_mode_info - -# config = copy.deepcopy(CONFIG[0]) -# config["fabric_name"] = param -# match = r"ConversionUtils\.validate_fabric_name:\s+" -# match += rf"Invalid fabric name: {param}\.\s+" -# match += r"Fabric name must start with a letter A-Z or a-z and contain\s+" -# match += r"only the characters in:" -# if raises: -# with pytest.raises(raises, match=match): -# instance.config = [config] -# else: -# instance.config = [config] -# assert instance.config[0]["fabric_name"] == param - - -# @pytest.mark.parametrize( -# "param, raises", -# [ -# ("maintenance", None), -# ("normal", None), -# (10, ValueError), -# (["192.168.1.2"], ValueError), -# ({"ip_address": "192.168.1.2"}, ValueError), -# ], -# ) -# def test_maintenance_mode_info_00600(maintenance_mode_info, param, raises) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - verify_config_parameters() -# - config.setter - -# Summary -# - Verify MaintenanceModeInfo().verify_config_parameters() re-raises -# - ``ValueError`` if: -# - ``mode`` raises ``ValueError`` due to being an -# invalid value. - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated - -# Code Flow - Test -# - MaintenanceModeInfo().config is set to a dict. -# - The dict is updated with mode set to valid and invalid -# values of ``mode`` - -# Expected Result -# - ``ValueError`` is raised when mode is not a valid value -# - Exception message matches expected -# - Exception is not raised when mode is a valid value -# """ - -# with does_not_raise(): -# instance = maintenance_mode_info - -# config = copy.deepcopy(CONFIG[0]) -# config["mode"] = param -# match = r"MaintenanceMode\.verify_mode:\s+" -# match += r"mode must be one of\s+" -# if raises: -# with pytest.raises(raises, match=match): -# instance.config = [config] -# else: -# instance.config = [config] -# assert instance.config[0]["mode"] == param - - -# @pytest.mark.parametrize( -# "endpoint_instance, mock_exception, expected_exception, mock_message", -# [ -# ("ep_maintenance_mode_disable", TypeError, ValueError, "Bad type"), -# ("ep_maintenance_mode_disable", ValueError, ValueError, "Bad value"), -# ("ep_maintenance_mode_enable", TypeError, ValueError, "Bad type"), -# ("ep_maintenance_mode_enable", ValueError, ValueError, "Bad value"), -# ], -# ) -# def test_maintenance_mode_info_00700( -# monkeypatch, -# maintenance_mode_info, -# endpoint_instance, -# mock_exception, -# expected_exception, -# mock_message, -# ) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - refresh() - -# Summary -# - Verify MaintenanceModeInfo().change_system_mode() raises ``ValueError`` -# when ``EpMaintenanceModeEnable`` or ``EpMaintenanceModeDisable`` raise -# any of: -# - ``TypeError`` -# - ``ValueError`` - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated -# - Required attributes are set -# - EpMaintenanceModeEnable() is mocked to raise each -# of the above exceptions -# - EpMaintenanceModeDisable() is mocked to raise each -# of the above exceptions - -# Code Flow - Test -# - MaintenanceModeInfo().refresh() is called for each exception - -# Expected Result -# - ``ValueError`` is raised. -# - Exception message matches expected. -# """ - -# class MockEndpoint: -# """ -# Mock Ep*() class -# """ - -# def __init__(self): -# self._fabric_name = None -# self._serial_number = None - -# @property -# def fabric_name(self): -# """ -# Mock fabric_name getter/setter -# """ -# return self._fabric_name - -# @fabric_name.setter -# def fabric_name(self, value): -# raise mock_exception(mock_message) - -# @property -# def serial_number(self): -# """ -# Mock serial_number getter/setter -# """ -# return self._serial_number - -# @serial_number.setter -# def serial_number(self, value): -# self._serial_number = value - -# with does_not_raise(): -# instance = maintenance_mode_info -# config = copy.deepcopy(CONFIG[0]) -# if endpoint_instance == "ep_maintenance_mode_disable": -# config["mode"] = "normal" -# instance.config = [config] -# instance.rest_send = RestSend({}) -# instance.results = Results() - -# monkeypatch.setattr(instance, endpoint_instance, MockEndpoint()) -# with pytest.raises(expected_exception, match=mock_message): -# instance.refresh() - - -# @pytest.mark.parametrize( -# "endpoint_instance, mock_exception, expected_exception, mock_message", -# [ -# ("ep_fabric_config_deploy", TypeError, ValueError, "Bad type"), -# ("ep_fabric_config_deploy", ValueError, ValueError, "Bad value"), -# ], -# ) -# def test_maintenance_mode_info_00800( -# monkeypatch, -# maintenance_mode_info, -# endpoint_instance, -# mock_exception, -# expected_exception, -# mock_message, -# ) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - refresh() - -# Summary -# - Verify MaintenanceModeInfo().deploy_switches() raises ``ValueError`` -# when ``EpFabricConfigDeploy`` raises any of: -# - ``TypeError`` -# - ``ValueError`` - - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated -# - Required attributes are set -# - EpFabricConfigDeploy() is mocked to raise each of the above exceptions - -# Code Flow - Test -# - MaintenanceModeInfo().refresh() is called for each exception - -# Expected Result -# - ``TypeError`` and ``ValueError`` are raised. -# - Exception message matches expected. -# """ - -# class MockEndpoint: -# """ -# Mock EpFabricConfigDeploy() class -# """ - -# def __init__(self): -# self._fabric_name = None -# self._switch_id = None - -# @property -# def fabric_name(self): -# """ -# Mock fabric_name getter/setter -# """ -# return self._fabric_name - -# @fabric_name.setter -# def fabric_name(self, value): -# raise mock_exception(mock_message) - -# @property -# def switch_id(self): -# """ -# Mock switch_id getter/setter -# """ -# return self._switch_id - -# @switch_id.setter -# def switch_id(self, value): -# self._switch_id = value - -# def responses(): -# yield {"MESSAGE": "OK", "RETURN_CODE": 200, "DATA": {"status": "Success"}} - -# mock_sender = MockSender() -# mock_sender.gen = ResponseGenerator(responses()) -# rest_send = RestSend({"state": "merged", "check_mode": False}) -# rest_send.sender = mock_sender -# rest_send.response_handler = ResponseHandler() -# rest_send.unit_test = True -# rest_send.timeout = 1 - -# config = copy.deepcopy(CONFIG[0]) -# config["deploy"] = True - -# with does_not_raise(): -# instance = maintenance_mode_info -# instance.config = [config] -# instance.rest_send = rest_send -# instance.results = Results() - -# monkeypatch.setattr(instance, endpoint_instance, MockEndpoint()) -# with pytest.raises(expected_exception, match=mock_message): -# instance.refresh() - - -# @pytest.mark.parametrize( -# "mock_exception, expected_exception, mock_message", -# [ -# (TypeError, ValueError, r"Converted TypeError to ValueError"), -# (ValueError, ValueError, r"Converted ValueError to ValueError"), -# ], -# ) -# def test_maintenance_mode_info_00900( -# maintenance_mode_info, mock_exception, expected_exception, mock_message -# ) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - change_system_mode() - - -# Summary -# - Verify MaintenanceModeInfo().change_system_mode() raises ``ValueError`` -# when ``MaintenanceModeInfo().results()`` raises any of: -# - ``TypeError`` -# - ``ValueError`` - - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated -# - Required attributes are set -# - Results().response_current.setter is mocked to raise each of the above -# exceptions - -# Code Flow - Test -# - MaintenanceModeInfo().refresh() is called for each exception - -# Expected Result -# - ``ValueError`` is raised -# - Exception message matches expected -# """ - -# class MockResults: -# """ -# Mock the Results class -# """ - -# class_name = "Results" - -# def register_task_result(self, *args): -# """ -# do nothing -# """ - -# @property -# def response_current(self): -# """ -# mock response_current getter -# """ -# return {"success": True} - -# @response_current.setter -# def response_current(self, *args): -# raise mock_exception(mock_message) - -# def responses(): -# yield {"RETURN_CODE": 200, "MESSAGE": "OK", "DATA": {"status": "Success"}} - -# mock_sender = MockSender() -# mock_sender.gen = ResponseGenerator(responses()) - -# with does_not_raise(): -# rest_send = RestSend({"state": "merged", "check_mode": False}) -# rest_send.sender = mock_sender -# rest_send.response_handler = ResponseHandler() -# instance = maintenance_mode_info -# instance.rest_send = rest_send -# instance.rest_send.unit_test = True -# instance.rest_send.timeout = 1 -# instance.config = CONFIG -# instance.results = MockResults() - -# with pytest.raises(expected_exception, match=mock_message): -# instance.refresh() - - -# def test_maintenance_mode_info_01000(monkeypatch, maintenance_mode_info) -> None: -# """ -# Classes and Methods -# - MaintenanceModeInfo() -# - __init__() -# - refresh() - -# Summary -# - Verify MaintenanceModeInfo().refresh() raises ``ValueError`` when -# ``MaintenanceModeInfo().deploy_switches()`` raises -# ``ControllerResponseError`` when the RETURN_CODE in the -# response is not 200. - -# Code Flow - Setup -# - MaintenanceModeInfo() is instantiated -# - Required attributes are set - -# Code Flow - Test -# - MaintenanceModeInfo().refresh() is called with simulated responses: -# - 200 response for ``change_system_mode()`` -# - 500 response ``deploy_switches()`` - -# Expected Result -# - ``ValueError``is raised. -# - Exception message matches expected. -# """ - -# def responses(): -# yield {"MESSAGE": "OK", "RETURN_CODE": 200, "DATA": {"status": "Success"}} -# yield { -# "MESSAGE": "Internal server error", -# "RETURN_CODE": 500, -# "DATA": {"status": "Success"}, -# } - -# mock_sender = MockSender() -# mock_sender.gen = ResponseGenerator(responses()) -# rest_send = RestSend({"state": "merged", "check_mode": False}) -# rest_send.sender = mock_sender -# rest_send.response_handler = ResponseHandler() -# rest_send.unit_test = True -# rest_send.timeout = 1 - -# config = copy.deepcopy(CONFIG[0]) -# config["deploy"] = True - -# with does_not_raise(): -# instance = maintenance_mode_info -# instance.config = [config] -# instance.rest_send = rest_send -# instance.results = Results() - -# match = r"MaintenanceMode\.deploy_switches:\s+" -# match += r"Unable to deploy switches:\s+" -# match += r"fabric_name VXLAN_Fabric,\s+" -# match += r"serial_numbers FDO22180ASJ\.\s+" -# match += r"Got response.*\." -# with pytest.raises(ValueError, match=match): -# instance.refresh() + assert instance.results.response[0]["DATA"][0]["mode"] == "Normal" + assert instance.results.response[0]["DATA"][0]["systemMode"] == "Maintenance" + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True From 8ffbd720e93890b91cc2df219cacbcf0690616e3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 6 Jun 2024 11:26:37 -1000 Subject: [PATCH 134/230] MockSender(): Remove MockSender() was a test implementation of sender_file.py Sender(). Removed this, in favor of Send() from sender_file.py, in the following files: 1. tests/unit/module_utils/common/common_utils.py 2. tests/unit/module_utils/common/test_maintenance_mode.py 3. tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py --- .../unit/module_utils/common/common_utils.py | 89 +------------------ .../common/test_maintenance_mode.py | 75 ++++++++-------- .../dcnm_fabric/test_fabric_details_v2.py | 47 +++++----- 3 files changed, 64 insertions(+), 147 deletions(-) diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index 27dc9eea5..27b8a761a 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -52,8 +52,8 @@ class ResponseGenerator: """ - Given a generator, return the items in the generator with - each call to the next property + Given a coroutine which yields dictionaries, return the yielded items + with each call to the next property For usage in the context of dcnm_image_policy unit tests, see: test: test_image_policy_create_bulk_00037 @@ -87,91 +87,6 @@ def public_method_for_pylint(self) -> Any: """ -class MockSender: - """ - Mock the Sender class - - ### Usage - Typically, ``def responses()`` would yield a file reader with a - key into a json file. - - For example - ``` - def responses(): - yield responses_maintenance_mode(key) - yield responses_config_deploy(key) - ``` - - Below we are yielding dictionaries directly for simplicity. - - ```python - def responses(): - yield {"key1": "value1"} - yield {"key2": "value2"} - - sender = MockSender() - sender.gen = ResponseGenerator(responses()) - - rest_send = RestSend() - rest_send.sender = sender - # rest of test case... - """ - - def __init__(self): - self.class_name = "Sender" - self.properties = {} - self.properties["gen"] = None - - def commit(self): - """ - do nothing - """ - - @property - def gen(self): - """ - - getter: Return the ``ResponseGenerator()`` instance. - - setter: Set the ``ResponseGenerator()`` instance that provides - simulated responses. - """ - return self.properties["gen"] - - @gen.setter - def gen(self, value): - self.properties["gen"] = value - - @property - def response(self): - """ - return the simulated response - """ - return self.gen.next - - @response.setter - def response(self, *args, **kwargs): - pass - - @property - def path(self): - """ - do nothing - """ - - @path.setter - def path(self, *args, **kwargs): - pass - - @property - def verb(self): - """ - do nothing - """ - - @verb.setter - def verb(self, *args, **kwargs): - pass - - class MockAnsibleModule: """ Mock the AnsibleModule class diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 82eca7188..3cab37fd8 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -47,9 +47,11 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( - MockSender, ResponseGenerator, does_not_raise, maintenance_mode_fixture, - params, responses_config_deploy, responses_maintenance_mode) + ResponseGenerator, does_not_raise, maintenance_mode_fixture, params, + responses_config_deploy, responses_maintenance_mode) FABRIC_NAME = "VXLAN_Fabric" CONFIG = [ @@ -390,25 +392,23 @@ def responses(): yield responses_maintenance_mode(key) yield responses_config_deploy(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 config = copy.deepcopy(CONFIG[0]) config["mode"] = mode config["deploy"] = deploy with does_not_raise(): - rest_send = RestSend({"state": "merged", "check_mode": False}) - rest_send.sender = mock_sender - rest_send.response_handler = ResponseHandler() instance = maintenance_mode instance.rest_send = rest_send - instance.rest_send.unit_test = True - instance.rest_send.timeout = 1 instance.results = Results() instance.config = [config] - - with does_not_raise(): instance.commit() assert isinstance(instance.results.diff, list) @@ -497,22 +497,21 @@ def test_maintenance_mode_00230(maintenance_mode, mode) -> None: def responses(): yield responses_maintenance_mode(key) - # yield responses_config_deploy(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 config = copy.deepcopy(CONFIG[0]) config["mode"] = mode with does_not_raise(): - rest_send = RestSend({"state": "merged", "check_mode": False}) - rest_send.sender = mock_sender - rest_send.response_handler = ResponseHandler() instance = maintenance_mode instance.rest_send = rest_send - instance.rest_send.unit_test = True - instance.rest_send.timeout = 1 instance.results = Results() instance.config = [config] @@ -931,12 +930,13 @@ def __init__(self): @property def fabric_name(self): """ - Mock fabric_name getter/setter + Mock fabric_name getter/setter to raise an exception + in the setter. """ return self._fabric_name @fabric_name.setter - def fabric_name(self, value): + def fabric_name(self, *args): raise mock_exception(mock_message) @property @@ -953,11 +953,11 @@ def switch_id(self, value): def responses(): yield {"MESSAGE": "OK", "RETURN_CODE": 200, "DATA": {"status": "Success"}} - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) - rest_send = RestSend({"state": "merged", "check_mode": False}) - rest_send.sender = mock_sender + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) rest_send.response_handler = ResponseHandler() + rest_send.sender = sender rest_send.unit_test = True rest_send.timeout = 1 @@ -1028,9 +1028,8 @@ def register_task_result(self, *args): @property def response_current(self): """ - mock response_current getter + mock response_current to raise an exception in the setter. """ - return {"success": True} @response_current.setter def response_current(self, *args): @@ -1039,17 +1038,17 @@ def response_current(self, *args): def responses(): yield {"RETURN_CODE": 200, "MESSAGE": "OK", "DATA": {"status": "Success"}} - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 with does_not_raise(): - rest_send = RestSend({"state": "merged", "check_mode": False}) - rest_send.sender = mock_sender - rest_send.response_handler = ResponseHandler() instance = maintenance_mode instance.rest_send = rest_send - instance.rest_send.unit_test = True - instance.rest_send.timeout = 1 instance.config = CONFIG instance.results = MockResults() @@ -1092,11 +1091,11 @@ def responses(): "DATA": {"status": "Success"}, } - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) - rest_send = RestSend({"state": "merged", "check_mode": False}) - rest_send.sender = mock_sender + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) rest_send.response_handler = ResponseHandler() + rest_send.sender = sender rest_send.unit_test = True rest_send.timeout = 1 diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py index 710278c11..9468922e8 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py @@ -42,10 +42,10 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ FabricDetails -from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ - MockSender from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( ResponseGenerator, does_not_raise, fabric_details_v2_fixture, responses_fabric_details_v2) @@ -136,14 +136,15 @@ def test_fabric_details_v2_00100(fabric_details_v2) -> None: def responses(): yield responses_fabric_details_v2(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 with does_not_raise(): - rest_send = RestSend({"state": "merged", "check_mode": False}) - rest_send.response_handler = ResponseHandler() - rest_send.sender = mock_sender - rest_send.unit_test = True instance = fabric_details_v2 instance.rest_send = rest_send instance.results = Results() @@ -171,7 +172,7 @@ def responses(): assert True not in instance.results.changed -def test_fabric_details_v2_00110(monkeypatch, fabric_details_v2) -> None: +def test_fabric_details_v2_00110(fabric_details_v2) -> None: """ ### Classes and Methods - FabricDetails() @@ -205,14 +206,15 @@ def test_fabric_details_v2_00110(monkeypatch, fabric_details_v2) -> None: def responses(): yield responses_fabric_details_v2(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 with does_not_raise(): - rest_send = RestSend({"state": "merged", "check_mode": False}) - rest_send.response_handler = ResponseHandler() - rest_send.sender = mock_sender - rest_send.unit_test = True instance = fabric_details_v2 instance.rest_send = rest_send instance.results = Results() @@ -229,7 +231,7 @@ def responses(): assert len(instance.results.response) == 0 -def test_fabric_details_v2_00120(monkeypatch, fabric_details_v2) -> None: +def test_fabric_details_v2_00120(fabric_details_v2) -> None: """ ### Classes and Methods - FabricDetails() @@ -264,14 +266,15 @@ def test_fabric_details_v2_00120(monkeypatch, fabric_details_v2) -> None: def responses(): yield responses_fabric_details_v2(key) - mock_sender = MockSender() - mock_sender.gen = ResponseGenerator(responses()) + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 with does_not_raise(): - rest_send = RestSend({"state": "merged", "check_mode": False}) - rest_send.response_handler = ResponseHandler() - rest_send.sender = mock_sender - rest_send.unit_test = True instance = fabric_details_v2 instance.rest_send = rest_send instance.results = Results() From 75312dffcdc7f36448ab345260b3d499cecacb91 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 7 Jun 2024 06:26:25 -1000 Subject: [PATCH 135/230] RestSend(), FabricDetails(): Update docstrings 1. Update the docstrings in the v2 version of these classes. 2. FabricDetails().register_result(): Add try-except block around results update. --- plugins/module_utils/common/rest_send_v2.py | 3 +- .../module_utils/fabric/fabric_details_v2.py | 282 ++++++++++++------ 2 files changed, 192 insertions(+), 93 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index ab705167c..f6a0cb7d6 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -78,11 +78,12 @@ class RestSend: ### Usage example ```python + params = {"check_mode": False, "state": "merged"} sender = Sender() # class that implements the sender interface sender.ansible_module = ansible_module try: - rest_send = RestSend() + rest_send = RestSend(params) rest_send.sender = sender rest_send.response_handler = ResponseHandler() rest_send.unit_test = True # optional, use in unit tests for speed diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index e52cfc89d..55870eeba 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -87,18 +87,22 @@ def register_result(self): details and register the result. ### Raises - + - ``ValueError``if: + - ``Results()`` raises ``TypeError`` """ - self.results.action = "fabric_details" - self.results.response_current = self.rest_send.response_current - self.results.result_current = self.rest_send.result_current - if self.results.response_current.get("RETURN_CODE") == 200: - self.results.failed = False - else: - self.results.failed = True - # FabricDetails never changes the controller state - self.results.changed = False - self.results.register_task_result() + try: + self.results.action = "fabric_details" + self.results.response_current = self.rest_send.response_current + self.results.result_current = self.rest_send.result_current + if self.results.response_current.get("RETURN_CODE") == 200: + self.results.failed = False + else: + self.results.failed = True + # FabricDetails never changes the controller state + self.results.changed = False + self.results.register_task_result() + except TypeError as error: + raise ValueError(error) from error def validate_refresh_parameters(self) -> None: """ @@ -106,8 +110,9 @@ def validate_refresh_parameters(self) -> None: Validate that mandatory parameters are set before calling refresh(). ### Raises - - ``ValueError`` if instance.rest_send is not set. - - ``ValueError`` if instance.results is not set. + - ``ValueError``if: + - ``rest_send`` is not set. + - ``results`` is not set. """ method_name = inspect.stack()[0][3] if self.rest_send is None: @@ -128,8 +133,10 @@ def refresh_super(self): populate self.data with the results. ### Raises - - ``ValueError`` if the RestSend object raises - ``TypeError`` or ``ValueError``. + - ``ValueError`` if: + - ``validate_refresh_parameters()`` raises ``ValueError``. + - ``RestSend`` raises ``TypeError`` or ``ValueError``. + - ``register_result()`` raises ``ValueError``. ### Notes - ``self.data`` is a dictionary of fabric details, keyed on @@ -170,37 +177,54 @@ def refresh_super(self): return self.data[fabric_name] = item - self.register_result() + try: + self.register_result() + except ValueError as error: + raise ValueError(error) from error msg = f"{self.class_name}.{method_name}: calling self.rest_send.commit() DONE" self.log.debug(msg) def _get(self, item): """ + ### Summary overridden in subclasses """ def _get_nv_pair(self, item): """ + ### Summary overridden in subclasses """ @property def all_data(self): """ + ### Summary Return all fabric details from the controller (i.e. self.data) + + ``refresh`` must be called before accessing this property. + + ### Raises + None """ return self.data @property def asn(self): """ + ### Summary Return the BGP asn of the fabric specified with filter, if it exists. Return None otherwise - Type: string - Possible values: - - e.g. 65000 + ### Raises + None + + ### Type + string + + ### Returns + - e.g. "65000" - None """ try: @@ -213,14 +237,19 @@ def asn(self): @property def deployment_freeze(self): """ - Return the nvPairs.DEPLOYMENT_FREEZE of the fabric specified with filter, - if it exists. - Return None otherwise + ### Summary + The nvPairs.DEPLOYMENT_FREEZE of the fabric specified with filter. - Type: string - Possible values: - - true - - false + ### Raises + None + + ### Type + boolean + + ### Returns + - False + - True + - None """ try: return self._get_nv_pair("DEPLOYMENT_FREEZE") @@ -232,15 +261,19 @@ def deployment_freeze(self): @property def enable_pbr(self): """ - Return the PBR enable state of the fabric specified with filter, - if it exists. - Return None otherwise + ### Summary + The PBR enable state of the fabric specified with filter. - Type: boolean - Possible values: - - True - - False - - None + ### Raises + None + + ### Type + boolean + + ### Returns + - False + - True + - None """ try: return self._get_nv_pair("ENABLE_PBR") @@ -252,14 +285,18 @@ def enable_pbr(self): @property def fabric_id(self): """ - Return the fabricId of the fabric specified with filter, - if it exists. - Return None otherwise + ### Summary + The ``fabricId`` value of the fabric specified with filter. - Type: string - Possible values: - - e.g. FABRIC-5 - - None + ### Raises + None + + ### Type + string + + ### Returns + - e.g. FABRIC-5 + - None """ try: return self._get("fabricId") @@ -271,14 +308,18 @@ def fabric_id(self): @property def fabric_type(self): """ - Return the nvPairs.FABRIC_TYPE of the fabric specified with filter, - if it exists. - Return None otherwise + ### Summary + The ``nvPairs.FABRIC_TYPE`` value of the fabric specified with filter. - Type: string - Possible values: - - Switch_Fabric - - None + ### Raises + None + + ### Type + string + + ### Returns + - e.g. Switch_Fabric + - None """ try: return self._get_nv_pair("FABRIC_TYPE") @@ -290,14 +331,19 @@ def fabric_type(self): @property def is_read_only(self): """ - Return the nvPairs.IS_READ_ONLY of the fabric specified with filter, - if it exists. - Return None otherwise + ### Summary + The ``nvPairs.IS_READ_ONLY`` value of the fabric specified with filter. - Type: string - Possible values: - - true - - false + ### Raises + None + + ### Type + boolean + + ### Returns + - True + - False + - None """ try: return self._get_nv_pair("IS_READ_ONLY") @@ -309,15 +355,20 @@ def is_read_only(self): @property def replication_mode(self): """ - Return the nvPairs.REPLICATION_MODE of the fabric specified with filter, - if it exists. - Return None otherwise + ### Summary + The ``nvPairs.REPLICATION_MODE`` value of the fabric specified + with filter. - Type: string - Possible values: - - Ingress - - Multicast - - None + ### Raises + None + + ### Type + boolean + + ### Returns + - Ingress + - Multicast + - None """ try: return self._get_nv_pair("REPLICATION_MODE") @@ -329,15 +380,19 @@ def replication_mode(self): @property def template_name(self): """ - Return the templateName of the fabric specified with filter, - if it exists. - Return None otherwise + ### Summary + The ``templateName`` value of the fabric specified + with filter. - Type: string - Possible values: - - Easy_Fabric - - TODO - add other values - - None + ### Raises + None + + ### Type + string + + ### Returns + - e.g. Easy_Fabric + - None """ try: return self._get("templateName") @@ -349,17 +404,32 @@ def template_name(self): class FabricDetailsByName(FabricDetails): """ + ### Summary Retrieve fabric details from the controller and provide property accessors for the fabric attributes. - Usage (where params is AnsibleModule.params): + ### Raises + - ``ValueError`` if: + - ``super.__init__()`` raises ``ValueError``. + - ``refresh_super()`` raises ``ValueError``. + - ``refresh()`` raises ``ValueError``. + - ``filter`` is not set before accessing properties. + - ``fabric_name`` does not exist on the controller. + - An attempt is made to access a key that does not exist + for the filtered fabric. + + ### Usage ```python + from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import RestSend + from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import Results + from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import Sender + params = {"check_mode": False, "state": "merged"} - sender = Sender() # class implementing the sender interface + sender = Sender() sender.ansible_module = ansible_module - rest_send = RestSend() + rest_send = RestSend(params) rest_send.sender = sender rest_send.response_handler = ResponseHandler() @@ -381,10 +451,15 @@ class FabricDetailsByName(FabricDetails): Or: ```python - sender = Sender() # class that implements the sender interface + from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import RestSend + from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import Results + from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import Sender + + params = {"check_mode": False, "state": "merged"} + sender = Sender() sender.ansible_module = ansible_module - rest_send = RestSend() + rest_send = RestSend(params) rest_send.sender = sender rest_send.response_handler = ResponseHandler() @@ -395,8 +470,8 @@ class FabricDetailsByName(FabricDetails): all_fabrics = instance.all_data ``` - - Where ``all_fabrics`` will be a dictionary of all fabrics - on the controller, keyed on fabric name. + Where ``all_fabrics`` will be a dictionary of all fabrics on the + controller, keyed on fabric name. """ def __init__(self, params): @@ -474,13 +549,17 @@ def _get(self, item): def _get_nv_pair(self, item): """ - # Retrieve the value of the nvPair item for fabric_name. + ### Summary + Retrieve the value of the nvPair item for fabric_name. - - raise ``ValueError`` if ``self.filter`` has not been set. - - raise ``ValueError`` if ``self.filter`` (fabric_name) does not exist on the controller. - - raise ``ValueError`` if item is not a valid property name for the fabric. + ### Raises + - ``ValueError`` if: + - ``self.filter`` has not been set. + - ``self.filter`` (fabric_name) does not exist on the controller. + - ``item`` is not a valid property name for the fabric. - See also: ``self._get()`` + ### See also + ``self._get()`` """ method_name = inspect.stack()[0][3] @@ -515,9 +594,16 @@ def _get_nv_pair(self, item): @property def filtered_data(self): """ - - Return a dictionary of the fabric matching self.filter. - - Return None if the fabric does not exist on the controller. - - raise ``ValueError`` if self.filter has not been set. + ### Summary + The DATA portion of the dictionary for the fabric specified with filter. + + ### Raises + - ``ValueError`` if: + - ``self.filter`` has not been set. + + ### Returns + - A dictionary of the fabric matching self.filter. + - ``None``, if the fabric does not exist on the controller. """ method_name = inspect.stack()[0][3] if self.filter is None: @@ -556,13 +642,25 @@ class FabricDetailsByNvPair(FabricDetails): property to a dictionary containing fabrics on the controller that match ``filter_key`` and ``filter_value``. + ### Raises + - ``ValueError`` if: + - ``super.__init__()`` raises ``ValueError``. + - ``refresh_super()`` raises ``ValueError``. + - ``refresh()`` raises ``ValueError``. + - ``filter_key`` is not set before calling ``refresh()``. + - ``filter_value`` is not set before calling ``refresh()``. + ### Usage ```python + from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import RestSend + from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import Results + from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import Sender + params = {"check_mode": False, "state": "query"} - sender = Sender() # class implementing the sender interface + sender = Sender() sender.ansible_module = ansible_module - rest_send = RestSend() + rest_send = RestSend(params) rest_send.sender = sender rest_send.response_handler = ResponseHandler() @@ -648,13 +746,13 @@ def filtered_data(self): def filter_key(self): """ ### Summary - The nvPairs key on which to filter. + The ``nvPairs`` key on which to filter. ### Raises None ### Notes - ``filter_key``should be an exact match for the key in the nvPairs + ``filter_key``should be an exact match for the key in the ``nvPairs`` dictionary for the fabric. """ return self._filter_key @@ -667,13 +765,13 @@ def filter_key(self, value): def filter_value(self): """ ### Summary - The nvPairs value on which to filter. + The ``nvPairs`` value on which to filter. ### Raises None ### Notes - ``filter_value`` should be an exact match for the value in the nvPairs + ``filter_value`` should be an exact match for the value in the ``nvPairs`` dictionary for the fabric. """ return self._filter_value From 341e1f7c8c6a75423b44fcfa5e20f3949fced31b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 7 Jun 2024 06:48:29 -1000 Subject: [PATCH 136/230] MockFabricDetailsByName: Mock exceptions only. 1. MockFabricDetailsByName(): Modified to mock ONLY the exceptions raised by MockFabricDetailsByName. It no longer duplicates the functionality of Sender(). --- .../unit/mocks/mock_fabric_details_by_name.py | 71 ++++++++----------- 1 file changed, 30 insertions(+), 41 deletions(-) diff --git a/tests/unit/mocks/mock_fabric_details_by_name.py b/tests/unit/mocks/mock_fabric_details_by_name.py index afd136332..01ab992a0 100644 --- a/tests/unit/mocks/mock_fabric_details_by_name.py +++ b/tests/unit/mocks/mock_fabric_details_by_name.py @@ -19,13 +19,24 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ - responses_fabric_details_by_name - class MockFabricDetailsByName: """ - Mock the FabricDetailsByName class + ### Summary + Mock the exceptions raised by the methods and properties + in the ``MockFabricDetailsByName`` class. + + ### NOTES + - This class is used to test the exceptions raised by + ``MockFabricDetailsByName`` + - This class does NOT simulate the behavior of + ``MockFabricDetailsByName`` with respect its interaction with the + controller. For that, see the ``Sender`` class within + ``module_utils/common/sender_file.py``, + and the ``RestSend`` class within ``module_utils/common/rest_send.py``. + - Example usage for the ``Sender`` class can be found in + ``test_maintenance_mode_info_00500`` within + ``tests/unit/module_utils/common/test_maintenance_mode_info.py``. """ def __init__(self) -> None: @@ -38,7 +49,6 @@ def null_mock_exception(): self._mock_exception = null_mock_exception self._mock_message = None self._mock_property = None - self._mock_response_key = None self._filter = None self._info = {} @@ -49,35 +59,12 @@ def null_mock_exception(): self._results = None self._is_read_only = None - def _get(self, key): - """ - Get the value of the key from the info dict. - """ - return self.data_subclass.get(self.filter, {}).get(key, None) - def refresh(self): """ Mocked refresh method """ if self.mock_class == self.class_name and self.mock_property == "refresh": raise self.mock_exception(self.mock_message) - if self.mock_response_key is None: - return - self.populate_info() - - def populate_info(self): - """ - Populate the info dict. - """ - self._info = {} - self.data_subclass = {} - self.response = responses_fabric_details_by_name(self.mock_response_key) - self.response_data = self.response.get("DATA", []) - for fabric in self.response_data: - nv_pairs = fabric.get("nvPairs", {}) - fabric_name = nv_pairs.get("FABRIC_NAME", None) - self._info[fabric_name] = nv_pairs - self.data_subclass[fabric_name] = nv_pairs @property def mock_class(self): @@ -123,18 +110,6 @@ def mock_property(self): def mock_property(self, value): self._mock_property = value - @property - def mock_response_key(self): - """ - The key used to extract controller response from the mocked response - in ``responses_FabricDetails.json``. - """ - return self._mock_response_key - - @mock_response_key.setter - def mock_response_key(self, value): - self._mock_response_key = value - @property def filter(self): """ @@ -197,4 +172,18 @@ def is_read_only(self): """ Mocked is_read_only property """ - return self._get("IS_READ_ONLY") + if ( + self.mock_class == self.class_name + and self.mock_property == "system_mode.setter" + ): + raise self.mock_exception(self.mock_message) + return self._is_read_only + + @is_read_only.setter + def is_read_only(self, value): + if ( + self.mock_class == self.class_name + and self.mock_property == "is_read_only.setter" + ): + raise self.mock_exception(self.mock_message) + self._is_read_only = value From ce1865e1f7e448f50364b10e03c6f4982ea6929d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 7 Jun 2024 07:19:57 -1000 Subject: [PATCH 137/230] MaintenanceModeInfo: 81% unit test coverage 1. MaintenanceModeInfo: Add negative test 00310- switch serialNumber key in controller response is null, or missing. 2. MaintenanceModeInfo: Update testcase 00300 docstring to clarify difference with 00310. 3. MaintenanceModeInfo(): Update ValueError message with more detail. This is the message tested by testcase 00310. --- .../common/maintenance_mode_info.py | 3 +- .../responses_FabricDetailsByName.json | 11 +++ .../fixtures/responses_SwitchDetails.json | 30 ++++++++ .../common/test_maintenance_mode_info.py | 74 ++++++++++++++++++- 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode_info.py b/plugins/module_utils/common/maintenance_mode_info.py index 7fe2c7535..1a9bd69d2 100644 --- a/plugins/module_utils/common/maintenance_mode_info.py +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -253,7 +253,8 @@ def refresh(self): if serial_number is None: msg = f"{self.class_name}.{method_name}: " msg += f"Switch with ip_address {ip_address} " - msg += "does not exist on the controller." + msg += "does not exist on the controller, or is missing its " + msg += "serialNumber key." raise ValueError(msg) fabric_name = self.switch_details.fabric_name diff --git a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json index 50be76d7a..1c0c363c5 100644 --- a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json +++ b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json @@ -324,6 +324,17 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", "RETURN_CODE": 200 }, + "test_maintenance_mode_info_00310a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "MESSAGE OK" + ], + "DATA": [], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, "test_maintenance_mode_info_00500a": { "TEST_NOTES": [ "RETURN_CODE 200", diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json index bdbda6dba..cdf4789ad 100644 --- a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -124,6 +124,7 @@ }, "test_maintenance_mode_info_00210a": { "TEST_NOTES": [ + "No switches exist on the controller", "RETURN_CODE: 200", "MESSAGE: OK" ], @@ -162,6 +163,35 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 }, + "test_maintenance_mode_info_00310a": { + "TEST_NOTES": [ + "DATA contains 192.168.1.2, but serial number is null", + "DATA[0].fabricName: VXLAN_Fabric", + "DATA[0].freezeMode: null", + "DATA[0].ipAddress: 192.168.1.2", + "DATA[0].mode: Normal", + "DATA[0].serialNumber: null", + "DATA[0].switchRole: leaf", + "DATA[0].systemMode: Normal", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "mode": "Normal", + "serialNumber": null, + "switchRole": "leaf", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, "test_maintenance_mode_info_00400a": { "TEST_NOTES": [ "DATA[0].fabricName: VXLAN_Fabric", diff --git a/tests/unit/module_utils/common/test_maintenance_mode_info.py b/tests/unit/module_utils/common/test_maintenance_mode_info.py index 472d767ed..6e8a957ba 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode_info.py +++ b/tests/unit/module_utils/common/test_maintenance_mode_info.py @@ -418,10 +418,9 @@ def test_maintenance_mode_info_00300() -> None: ### Summary Verify ``refresh()`` raises ``ValueError`` when - ``switch_details.serial_number`` is ``None``. + ``switch_details._get()`` raises ``ValueError``. - This happens when the switch does not exist on the controller and causes - SwitchDetails()._get() to raise a ``ValueError``. + This happens when the switch is not found in the response from the controller. ### Setup - Data - ``ipAddress`` is set to something other than 192.168.1.2 @@ -476,6 +475,75 @@ def responses(): instance.refresh() +def test_maintenance_mode_info_00310() -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + + ### Summary + Verify ``refresh()`` raises ``ValueError`` when + ``switch_details.serial_number`` is ``None``. + + This happens when the switch exists on the controller but its + serial_number is null. This is a negative test case since we + expect the serial_number to be set. + + ### Setup - Data + - ``ipAddress`` is set to something other than 192.168.1.2 + - ``responses_SwitchDetails.json``: + - "DATA[0].fabricName: VXLAN_Fabric", + - "DATA[0].freezeMode: null", + - "DATA[0].ipAddress: 192.168.1.2", + - "DATA[0].mode: Normal", + - "DATA[0].serialNumber: null", + - "DATA[0].switchRole: leaf", + - "DATA[0].systemMode: Normal" + - RETURN_CODE: 200 + - MESSAGE: OK + - ``responses_FabricDetailsByName.json``: + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + + ### Trigger + - ``refresh()`` is called. + + ### Expected Result + - ``ValueError`` is raised. + - Exception message matches expectations. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + yield responses_fabric_details_by_name(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + + match = r"MaintenanceModeInfo\.refresh:\s+" + match += r"Switch with ip_address 192\.168\.1\.2\s+" + match += r"does not exist on the controller, or is\s+" + match += r"missing its serialNumber key\." + with pytest.raises(ValueError, match=match): + instance.refresh() + + @pytest.mark.parametrize( "mock_class, mock_property, mock_exception, expected_exception, mock_message", [ From 2ff573030ba4d38bbb830afbe995177d69c22120 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 7 Jun 2024 08:08:10 -1000 Subject: [PATCH 138/230] MaintenanceModeInfo: Fix fabric_read_only property assignment 1. MaintenanceModeInfo(): property was being assigned the value of fabric_freeze_mode. Fixed. 2. MaintenanceModeInfo: Add unit test 00600, which verifies fabric_read_only is True if nvPairs.IS_READ_ONLY is true. 3. MaintenanceModeInfo: Fix assert for fabric_read_only value in unit test 00510. 4. responses_FabricDetailsByName.json: Remove unused fixture test_maintenance_mode_info_00210a 5. responses_FabricDetailsByName.json: Add fixture test_maintenance_mode_info_00600a. 6. responses_SwitchDetails.json: Add fixture for test_maintenance_mode_info_00600a. --- .../common/maintenance_mode_info.py | 3 +- .../responses_FabricDetailsByName.json | 334 ++---------------- .../fixtures/responses_SwitchDetails.json | 28 ++ .../common/test_maintenance_mode_info.py | 73 +++- 4 files changed, 122 insertions(+), 316 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode_info.py b/plugins/module_utils/common/maintenance_mode_info.py index 1a9bd69d2..880df0fad 100644 --- a/plugins/module_utils/common/maintenance_mode_info.py +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -266,6 +266,7 @@ def refresh(self): self.fabric_details.filter = fabric_name except ValueError as error: raise ValueError(error) from error + fabric_read_only = self.fabric_details.is_read_only info[ip_address] = {} @@ -468,7 +469,7 @@ def fabric_read_only(self): - ``False``: The fabric is in a state where configuration changes can be made. """ - return self._get("fabric_freeze_mode") + return self._get("fabric_read_only") @property def info(self) -> dict: diff --git a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json index 1c0c363c5..08003ae5d 100644 --- a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json +++ b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json @@ -1,318 +1,4 @@ { - "test_maintenance_mode_info_00210a": { - "DATA": [ - { - "asn": "65000", - "createdOn": 1716345062044, - "deviceType": "n9k", - "fabricId": "FABRIC-2", - "fabricName": "VXLAN_Fabric", - "fabricTechnology": "VXLANFabric", - "fabricTechnologyFriendly": "VXLAN EVPN", - "fabricType": "Switch_Fabric", - "fabricTypeFriendly": "Switch Fabric", - "id": 2, - "modifiedOn": 1716952430067, - "networkExtensionTemplate": "Default_Network_Extension_Universal", - "networkTemplate": "Default_Network_Universal", - "nvPairs": { - "AAA_REMOTE_IP_ENABLED": "false", - "AAA_SERVER_CONF": "", - "ACTIVE_MIGRATION": "false", - "ADVERTISE_PIP_BGP": "false", - "ADVERTISE_PIP_ON_BORDER": "true", - "AGENT_INTF": "eth0", - "AI_ML_QOS_POLICY": "AI_Fabric_QOS_400G", - "ALLOW_NXC": "true", - "ALLOW_NXC_PREV": "true", - "ANYCAST_BGW_ADVERTISE_PIP": "false", - "ANYCAST_GW_MAC": "2020.0000.00aa", - "ANYCAST_LB_ID": "", - "ANYCAST_RP_IP_RANGE": "10.254.254.0/24", - "ANYCAST_RP_IP_RANGE_INTERNAL": "10.254.254.0/24", - "AUTO_SYMMETRIC_DEFAULT_VRF": "false", - "AUTO_SYMMETRIC_VRF_LITE": "false", - "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", - "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", - "AUTO_VRFLITE_IFC_DEFAULT_VRF": "false", - "BANNER": "", - "BFD_AUTH_ENABLE": "false", - "BFD_AUTH_KEY": "", - "BFD_AUTH_KEY_ID": "", - "BFD_ENABLE": "false", - "BFD_ENABLE_PREV": "false", - "BFD_IBGP_ENABLE": "false", - "BFD_ISIS_ENABLE": "false", - "BFD_OSPF_ENABLE": "false", - "BFD_PIM_ENABLE": "false", - "BGP_AS": "65000", - "BGP_AS_PREV": "65000", - "BGP_AUTH_ENABLE": "false", - "BGP_AUTH_KEY": "", - "BGP_AUTH_KEY_TYPE": "3", - "BGP_LB_ID": "0", - "BOOTSTRAP_CONF": "", - "BOOTSTRAP_ENABLE": "false", - "BOOTSTRAP_ENABLE_PREV": "false", - "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", - "BOOTSTRAP_MULTISUBNET_INTERNAL": "", - "BRFIELD_DEBUG_FLAG": "Disable", - "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", - "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", - "CDP_ENABLE": "false", - "COPP_POLICY": "strict", - "DCI_SUBNET_RANGE": "10.33.0.0/16", - "DCI_SUBNET_TARGET_MASK": "30", - "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", - "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", - "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", - "DEFAULT_VRF_REDIS_BGP_RMAP": "", - "DEPLOYMENT_FREEZE": "false", - "DHCP_ENABLE": "false", - "DHCP_END": "", - "DHCP_END_INTERNAL": "", - "DHCP_IPV6_ENABLE": "", - "DHCP_IPV6_ENABLE_INTERNAL": "", - "DHCP_START": "", - "DHCP_START_INTERNAL": "", - "DNS_SERVER_IP_LIST": "", - "DNS_SERVER_VRF": "", - "DOMAIN_NAME_INTERNAL": "", - "ENABLE_AAA": "false", - "ENABLE_AGENT": "false", - "ENABLE_AI_ML_QOS_POLICY": "false", - "ENABLE_AI_ML_QOS_POLICY_FLAP": "false", - "ENABLE_DEFAULT_QUEUING_POLICY": "false", - "ENABLE_EVPN": "true", - "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", - "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", - "ENABLE_L3VNI_NO_VLAN": "false", - "ENABLE_MACSEC": "false", - "ENABLE_NETFLOW": "false", - "ENABLE_NETFLOW_PREV": "false", - "ENABLE_NGOAM": "true", - "ENABLE_NXAPI": "true", - "ENABLE_NXAPI_HTTP": "true", - "ENABLE_PBR": "false", - "ENABLE_PVLAN": "false", - "ENABLE_PVLAN_PREV": "false", - "ENABLE_SGT": "false", - "ENABLE_SGT_PREV": "false", - "ENABLE_TENANT_DHCP": "true", - "ENABLE_TRM": "false", - "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", - "ESR_OPTION": "PBR", - "EXTRA_CONF_INTRA_LINKS": "", - "EXTRA_CONF_LEAF": "", - "EXTRA_CONF_SPINE": "", - "EXTRA_CONF_TOR": "", - "EXT_FABRIC_TYPE": "", - "FABRIC_INTERFACE_TYPE": "p2p", - "FABRIC_MTU": "9216", - "FABRIC_MTU_PREV": "9216", - "FABRIC_NAME": "VXLAN_Fabric", - "FABRIC_TYPE": "Switch_Fabric", - "FABRIC_VPC_DOMAIN_ID": "", - "FABRIC_VPC_DOMAIN_ID_PREV": "", - "FABRIC_VPC_QOS": "false", - "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", - "FEATURE_PTP": "false", - "FEATURE_PTP_INTERNAL": "false", - "FF": "Easy_Fabric", - "GRFIELD_DEBUG_FLAG": "Disable", - "HD_TIME": "180", - "HOST_INTF_ADMIN_STATE": "true", - "IBGP_PEER_TEMPLATE": "", - "IBGP_PEER_TEMPLATE_LEAF": "", - "INBAND_DHCP_SERVERS": "", - "INBAND_MGMT": "false", - "INBAND_MGMT_PREV": "false", - "ISIS_AREA_NUM": "0001", - "ISIS_AREA_NUM_PREV": "", - "ISIS_AUTH_ENABLE": "false", - "ISIS_AUTH_KEY": "", - "ISIS_AUTH_KEYCHAIN_KEY_ID": "", - "ISIS_AUTH_KEYCHAIN_NAME": "", - "ISIS_LEVEL": "level-2", - "ISIS_OVERLOAD_ELAPSE_TIME": "", - "ISIS_OVERLOAD_ENABLE": "false", - "ISIS_P2P_ENABLE": "false", - "L2_HOST_INTF_MTU": "9216", - "L2_HOST_INTF_MTU_PREV": "9216", - "L2_SEGMENT_ID_RANGE": "30000-49000", - "L3VNI_MCAST_GROUP": "", - "L3_PARTITION_ID_RANGE": "50000-59000", - "LINK_STATE_ROUTING": "ospf", - "LINK_STATE_ROUTING_TAG": "UNDERLAY", - "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", - "LOOPBACK0_IPV6_RANGE": "", - "LOOPBACK0_IP_RANGE": "10.2.0.0/22", - "LOOPBACK1_IPV6_RANGE": "", - "LOOPBACK1_IP_RANGE": "10.3.0.0/22", - "MACSEC_ALGORITHM": "", - "MACSEC_CIPHER_SUITE": "", - "MACSEC_FALLBACK_ALGORITHM": "", - "MACSEC_FALLBACK_KEY_STRING": "", - "MACSEC_KEY_STRING": "", - "MACSEC_REPORT_TIMER": "", - "MGMT_GW": "", - "MGMT_GW_INTERNAL": "", - "MGMT_PREFIX": "", - "MGMT_PREFIX_INTERNAL": "", - "MGMT_V6PREFIX": "", - "MGMT_V6PREFIX_INTERNAL": "", - "MPLS_HANDOFF": "false", - "MPLS_ISIS_AREA_NUM": "0001", - "MPLS_ISIS_AREA_NUM_PREV": "", - "MPLS_LB_ID": "", - "MPLS_LOOPBACK_IP_RANGE": "", - "MSO_CONNECTIVITY_DEPLOYED": "", - "MSO_CONTROLER_ID": "", - "MSO_SITE_GROUP_NAME": "", - "MSO_SITE_ID": "", - "MST_INSTANCE_RANGE": "", - "MULTICAST_GROUP_SUBNET": "239.1.1.0/25", - "NETFLOW_EXPORTER_LIST": "", - "NETFLOW_MONITOR_LIST": "", - "NETFLOW_RECORD_LIST": "", - "NETWORK_VLAN_RANGE": "2300-2999", - "NTP_SERVER_IP_LIST": "", - "NTP_SERVER_VRF": "", - "NVE_LB_ID": "1", - "NXAPI_HTTPS_PORT": "443", - "NXAPI_HTTP_PORT": "80", - "NXC_DEST_VRF": "management", - "NXC_PROXY_PORT": "8080", - "NXC_PROXY_SERVER": "", - "NXC_SRC_INTF": "", - "OBJECT_TRACKING_NUMBER_RANGE": "100-299", - "OSPF_AREA_ID": "0.0.0.0", - "OSPF_AUTH_ENABLE": "false", - "OSPF_AUTH_KEY": "", - "OSPF_AUTH_KEY_ID": "", - "OVERLAY_MODE": "cli", - "OVERLAY_MODE_PREV": "cli", - "OVERWRITE_GLOBAL_NXC": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION_V6": "false", - "PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV": "false", - "PER_VRF_LOOPBACK_IP_RANGE": "", - "PER_VRF_LOOPBACK_IP_RANGE_V6": "fd00::a05:0/112", - "PHANTOM_RP_LB_ID1": "", - "PHANTOM_RP_LB_ID2": "", - "PHANTOM_RP_LB_ID3": "", - "PHANTOM_RP_LB_ID4": "", - "PIM_HELLO_AUTH_ENABLE": "false", - "PIM_HELLO_AUTH_KEY": "", - "PM_ENABLE": "false", - "PM_ENABLE_PREV": "false", - "POWER_REDUNDANCY_MODE": "ps-redundant", - "PREMSO_PARENT_FABRIC": "", - "PTP_DOMAIN_ID": "", - "PTP_LB_ID": "", - "REPLICATION_MODE": "Multicast", - "ROUTER_ID_RANGE": "", - "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", - "RP_COUNT": "2", - "RP_LB_ID": "254", - "RP_MODE": "asm", - "RR_COUNT": "2", - "SEED_SWITCH_CORE_INTERFACES": "", - "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", - "SGT_ID_RANGE": "", - "SGT_NAME_PREFIX": "", - "SGT_PREPROVISION": "false", - "SITE_ID": "65000", - "SLA_ID_RANGE": "10000-19999", - "SNMP_SERVER_HOST_TRAP": "true", - "SPINE_COUNT": "1", - "SPINE_SWITCH_CORE_INTERFACES": "", - "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", - "SSPINE_COUNT": "0", - "STATIC_UNDERLAY_IP_ALLOC": "false", - "STP_BRIDGE_PRIORITY": "", - "STP_ROOT_OPTION": "unmanaged", - "STP_VLAN_RANGE": "", - "STRICT_CC_MODE": "false", - "SUBINTERFACE_RANGE": "2-511", - "SUBNET_RANGE": "10.4.0.0/16", - "SUBNET_TARGET_MASK": "30", - "SYSLOG_SERVER_IP_LIST": "", - "SYSLOG_SERVER_VRF": "", - "SYSLOG_SEV": "", - "TCAM_ALLOCATION": "true", - "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", - "UNDERLAY_IS_V6": "false", - "UNNUM_BOOTSTRAP_LB_ID": "", - "UNNUM_DHCP_END": "", - "UNNUM_DHCP_END_INTERNAL": "", - "UNNUM_DHCP_START": "", - "UNNUM_DHCP_START_INTERNAL": "", - "UPGRADE_FROM_VERSION": "", - "USE_LINK_LOCAL": "false", - "V6_SUBNET_RANGE": "", - "V6_SUBNET_TARGET_MASK": "126", - "VPC_AUTO_RECOVERY_TIME": "360", - "VPC_DELAY_RESTORE": "150", - "VPC_DELAY_RESTORE_TIME": "60", - "VPC_DOMAIN_ID_RANGE": "1-1000", - "VPC_ENABLE_IPv6_ND_SYNC": "true", - "VPC_PEER_KEEP_ALIVE_OPTION": "management", - "VPC_PEER_LINK_PO": "500", - "VPC_PEER_LINK_VLAN": "3600", - "VRF_LITE_AUTOCONFIG": "Manual", - "VRF_VLAN_RANGE": "2000-2299", - "abstract_anycast_rp": "anycast_rp", - "abstract_bgp": "base_bgp", - "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", - "abstract_bgp_rr": "evpn_bgp_rr", - "abstract_dhcp": "base_dhcp", - "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", - "abstract_extra_config_leaf": "extra_config_leaf", - "abstract_extra_config_spine": "extra_config_spine", - "abstract_extra_config_tor": "extra_config_tor", - "abstract_feature_leaf": "base_feature_leaf_upg", - "abstract_feature_spine": "base_feature_spine_upg", - "abstract_isis": "base_isis_level2", - "abstract_isis_interface": "isis_interface", - "abstract_loopback_interface": "int_fabric_loopback_11_1", - "abstract_multicast": "base_multicast_11_1", - "abstract_ospf": "base_ospf", - "abstract_ospf_interface": "ospf_interface_11_1", - "abstract_pim_interface": "pim_interface", - "abstract_route_map": "route_map", - "abstract_routed_host": "int_routed_host", - "abstract_trunk_host": "int_trunk_host", - "abstract_vlan_interface": "int_fabric_vlan_11_1", - "abstract_vpc_domain": "base_vpc_domain_11_1", - "dcnmUser": "admin", - "default_network": "Default_Network_Universal", - "default_pvlan_sec_network": "", - "default_vrf": "Default_VRF_Universal", - "enableRealTimeBackup": "", - "enableScheduledBackup": "", - "network_extension_template": "Default_Network_Extension_Universal", - "scheduledTime": "", - "temp_anycast_gateway": "anycast_gateway", - "temp_vpc_domain_mgmt": "vpc_domain_mgmt", - "temp_vpc_peer_link": "int_vpc_peer_link_po", - "vrf_extension_template": "Default_VRF_Extension_Universal" - }, - "operStatus": "CRITICAL", - "provisionMode": "DCNMTopDown", - "replicationMode": "Multicast", - "siteId": "65000", - "templateName": "Easy_Fabric", - "vrfExtensionTemplate": "Default_VRF_Extension_Universal", - "vrfTemplate": "Default_VRF_Universal" - } - ], - "MESSAGE": "OK", - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", - "RETURN_CODE": 200 - }, "test_maintenance_mode_info_00300a": { "TEST_NOTES": [ "RETURN_CODE 200", @@ -367,5 +53,25 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00600a": { + "TEST_NOTES": [ + "nvPairs.FABRIC_NAME LAN_Classic", + "nvPairs.IS_READ_ONLY true", + "RETURN_CODE 200", + "MESSAGE OK" + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "LAN_Classic", + "IS_READ_ONLY": "true" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json index cdf4789ad..fe5bf7eb6 100644 --- a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -303,5 +303,33 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00600a": { + "TEST_NOTES": [ + "DATA[0].fabricName: LAN_Classic", + "DATA[0].freezeMode: null", + "DATA[0].ipAddress: 192.168.1.2", + "DATA[0].mode: Normal", + "DATA[0].serialNumber: FDO211218FV", + "DATA[0].switchRole: leaf", + "DATA[0].systemMode: Normal", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "LAN_Classic", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "mode": "Normal", + "serialNumber": "FDO123456FV", + "switchRole": "leaf", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_maintenance_mode_info.py b/tests/unit/module_utils/common/test_maintenance_mode_info.py index 6e8a957ba..a3125ebf4 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode_info.py +++ b/tests/unit/module_utils/common/test_maintenance_mode_info.py @@ -755,7 +755,7 @@ def responses(): instance.filter = CONFIG[0] assert instance.fabric_name == FABRIC_NAME assert instance.fabric_freeze_mode is True - assert instance.fabric_read_only is True + assert instance.fabric_read_only is False assert instance.fabric_deployment_disabled is True assert instance.mode == "normal" assert instance.role == "leaf" @@ -827,3 +827,74 @@ def responses(): assert instance.results.result[1]["success"] is True assert instance.results.result[0]["found"] is True assert instance.results.result[1]["found"] is True + + +def test_maintenance_mode_info_00600() -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + - FabricDetailsByName() + - refresh() + + ### Summary + - Verify: + - ``fabric_read_only`` is set to True when ``IS_READ_ONLY`` + is true in the controller response (FabricDetailsByName). + + ### Setup - Data + - ``responses_SwitchDetails.json``: + - DATA[0].fabricName: LAN_Classic + - DATA[0].freezeMode: null + - DATA[0].ipAddress: 192.168.1.2 + - DATA[0].mode: Normal + - DATA[0].serialNumber: FDO211218FV + - DATA[0].switchRole: leaf + - DATA[0].systemMode: Normal + - RETURN_CODE: 200 + - MESSAGE: OK + - ``responses_FabricDetailsByName.json``: + - DATA[0].nvPairs.FABRIC_NAME: LAN_Classic + - DATA[0].nvPairs.IS_READ_ONLY: true + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + + ### Trigger + - ``refresh()`` is called. + + ### Expected Result + - Conditions in Summary are confirmed. + - Exception is not raised. + - ``MaintenanceModeInfo().results`` contains expected data. + + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + yield responses_fabric_details_by_name(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = CONFIG[0] + assert instance.fabric_read_only is True + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True From d6f8582f6493525a6827ba806d1ec9d81de5f1c5 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 7 Jun 2024 08:48:37 -1000 Subject: [PATCH 139/230] MaintenanceModeInfo().__init__(): initialize self._filter, more... 1. MaintenanceModeInfo(): self._filter was not being set in __init__(). Fixed. 2. SwitchDetails().role: Update docstring to clarify that role is an alias of switch_role. 3. MaintenanceModeInfo: Add the following unit tests: - test_maintenance_mode_info_00700: Verify role is set to "na" when switchRole is null in the controller response. - test_maintenance_mode_info_00800: Verify get() raises ValueError if filter is not set. --- .../common/maintenance_mode_info.py | 1 + plugins/module_utils/common/switch_details.py | 3 + .../responses_FabricDetailsByName.json | 11 ++ .../fixtures/responses_SwitchDetails.json | 28 +++++ .../common/test_maintenance_mode_info.py | 112 +++++++++++++++++- 5 files changed, 154 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/common/maintenance_mode_info.py b/plugins/module_utils/common/maintenance_mode_info.py index 880df0fad..a63f5a51d 100644 --- a/plugins/module_utils/common/maintenance_mode_info.py +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -134,6 +134,7 @@ def __init__(self, params): self.switch_details = SwitchDetails() self._config = None + self._filter = None self._info = None self._rest_send = None self._results = None diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 0c4eb0922..1d68222b0 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -538,6 +538,9 @@ def role(self): - The ``switchRole`` value of the filtered switch, if it exists. - ``None`` otherwise. - Example: spine + + ### NOTES + - ``role`` is an alias of ``switch_role``. """ return self._get("switchRole") diff --git a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json index 08003ae5d..876236967 100644 --- a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json +++ b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json @@ -73,5 +73,16 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00700a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "MESSAGE OK" + ], + "DATA": [], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json index fe5bf7eb6..7b407ef11 100644 --- a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -331,5 +331,33 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00700a": { + "TEST_NOTES": [ + "DATA[0].fabricName: LAN_Classic", + "DATA[0].freezeMode: null", + "DATA[0].ipAddress: 192.168.1.2", + "DATA[0].mode: Normal", + "DATA[0].serialNumber: FDO211218FV", + "DATA[0].switchRole: null", + "DATA[0].systemMode: Normal", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "LAN_Classic", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "mode": "Normal", + "serialNumber": "FDO123456FV", + "switchRole": null, + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_maintenance_mode_info.py b/tests/unit/module_utils/common/test_maintenance_mode_info.py index a3125ebf4..9e9ba5bdc 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode_info.py +++ b/tests/unit/module_utils/common/test_maintenance_mode_info.py @@ -833,7 +833,6 @@ def test_maintenance_mode_info_00600() -> None: """ ### Classes and Methods - MaintenanceModeInfo() - - __init__() - refresh() - FabricDetailsByName() - refresh() @@ -898,3 +897,114 @@ def responses(): assert instance.results.result[1]["success"] is True assert instance.results.result[0]["found"] is True assert instance.results.result[1]["found"] is True + + +def test_maintenance_mode_info_00700() -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - refresh() + - SwitchDetails() + - refresh() + - FabricDetailsByName() + - refresh() + + ### Summary + - Verify: + - ``role`` is set to "na" when ``switchRole`` is null in the + controller response. + + ### Setup - Data + - ``responses_SwitchDetails.json``: + - DATA[0].fabricName: LAN_Classic + - DATA[0].freezeMode: null + - DATA[0].ipAddress: 192.168.1.2 + - DATA[0].mode: Normal + - DATA[0].serialNumber: FDO211218FV + - DATA[0].switchRole: null + - DATA[0].systemMode: Normal + - RETURN_CODE: 200 + - MESSAGE: OK + - ``responses_FabricDetailsByName.json``: + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + + ### Trigger + - ``refresh()`` is called. + + ### Expected Result + - Conditions in Summary are confirmed. + - Exception is not raised. + - ``MaintenanceModeInfo().results`` contains expected data. + + ### NOTES + - ``SwitchDetails().role`` is an alias of ``SwitchDetails().switch_role``. + - ``MaintenanceModeInfo().role`` is set based on the value of + ``SwitchDetails().role``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + yield responses_fabric_details_by_name(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = CONFIG[0] + assert instance.role == "na" + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True + + +def test_maintenance_mode_info_00800() -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - refresh() + - SwitchDetails() + - refresh() + - FabricDetailsByName() + - refresh() + + ### Summary + - Verify: + - _get() raises ``ValueError`` if ``filter`` is not set. + + ### Setup - Data + None + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + + ### Trigger + - ``MaintenanceModeInfo().role`` is accessed without setting + ``filter``. + + ### Expected Result + - Conditions in Summary are confirmed. + """ + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + match = r"MaintenanceModeInfo\._get:\s+" + match += r"set instance\.filter before accessing\s+" + match += r"property role*\." + with pytest.raises(ValueError, match=match): + instance.role # pylint: disable=pointless-statement From 1974b21a061169c7bbc04d0a16d3d241cc94cc44 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 7 Jun 2024 10:12:45 -1000 Subject: [PATCH 140/230] MaintenanceModeInfo: 89% unit test coverage 1. test_maintenance_mode_info_00810a: Verify ``get()`` raises ``ValueError`` if ``filter`` (switch IP) is not found in the controller response when the user accesses a property. 2. test_maintenance_mode_info_00820: Verify ``refresh`` re-raises ``ValueError`` raised by ``SwitchDetails()._get()`` when ``item`` is not found in the controller response. In this, case ``item`` is ``freezeMode``. --- .../common/maintenance_mode_info.py | 29 ++-- .../responses_FabricDetailsByName.json | 36 +++++ .../fixtures/responses_SwitchDetails.json | 56 +++++++ .../common/test_maintenance_mode_info.py | 145 ++++++++++++++++++ 4 files changed, 254 insertions(+), 12 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode_info.py b/plugins/module_utils/common/maintenance_mode_info.py index a63f5a51d..ae414fa5a 100644 --- a/plugins/module_utils/common/maintenance_mode_info.py +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -258,10 +258,17 @@ def refresh(self): msg += "serialNumber key." raise ValueError(msg) - fabric_name = self.switch_details.fabric_name - freeze_mode = self.switch_details.freeze_mode - mode = self.switch_details.maintenance_mode - role = self.switch_details.switch_role + try: + fabric_name = self.switch_details.fabric_name + freeze_mode = self.switch_details.freeze_mode + mode = self.switch_details.maintenance_mode + role = self.switch_details.switch_role + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error setting properties for switch with ip_address " + msg += f"{ip_address}. " + msg += f"Error details: {error}" + raise ValueError(msg) from error try: self.fabric_details.filter = fabric_name @@ -297,9 +304,12 @@ def _get(self, item): Return the value of the item from the filtered switch. ### Raises - - ``ValueError`` if ``filter`` is not set. - - ``ValueError`` if ``filter`` is not in the controller response. - - ``ValueError`` if item is not in the filtered switch dict. + - ``ValueError`` if: + - ``filter`` is not set. + - ``filter`` is not in the controller response. + ### NOTES + - We do not need to check that ``item`` exists in the filtered + switch dict, since ``refresh()`` has already done so. """ method_name = inspect.stack()[0][3] @@ -315,11 +325,6 @@ def _get(self, item): msg += "the controller." raise ValueError(msg) - if item not in self._info[self.filter]: - msg = f"{self.class_name}.{method_name}: " - msg += f"{self.filter} does not have a key named {item}." - raise ValueError(msg) - return self.conversion.make_boolean( self.conversion.make_none(self._info[self.filter].get(item)) ) diff --git a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json index 876236967..32d44e170 100644 --- a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json +++ b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json @@ -84,5 +84,41 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00810a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "MESSAGE OK" + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "VXLAN_Fabric", + "IS_READ_ONLY": "false" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00820a": { + "TEST_NOTES": [ + "RETURN_CODE 200", + "MESSAGE OK" + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "VXLAN_Fabric", + "IS_READ_ONLY": "false" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json index 7b407ef11..2033cd90d 100644 --- a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -359,5 +359,61 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00810a": { + "TEST_NOTES": [ + "DATA[0].fabricName: VXLAN_Fabric", + "DATA[0].freezeMode: null", + "DATA[0].ipAddress: 192.168.1.2", + "DATA[0].mode: Normal", + "DATA[0].serialNumber: FDO211218FV", + "DATA[0].switchRole: null", + "DATA[0].systemMode: Normal", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "mode": "Normal", + "serialNumber": "FDO123456FV", + "switchRole": null, + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_00820a": { + "TEST_NOTES": [ + "DATA[0] is missing the freezeMode key", + "DATA[0].fabricName: VXLAN_Fabric", + "DATA[0].freezeMode: MISSING", + "DATA[0].ipAddress: 192.168.1.2", + "DATA[0].mode: Normal", + "DATA[0].serialNumber: FDO211218FV", + "DATA[0].switchRole: null", + "DATA[0].systemMode: Normal", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "ipAddress": "192.168.1.2", + "mode": "Normal", + "serialNumber": "FDO123456FV", + "switchRole": null, + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_maintenance_mode_info.py b/tests/unit/module_utils/common/test_maintenance_mode_info.py index 9e9ba5bdc..aff3dcc65 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode_info.py +++ b/tests/unit/module_utils/common/test_maintenance_mode_info.py @@ -1008,3 +1008,148 @@ def test_maintenance_mode_info_00800() -> None: match += r"property role*\." with pytest.raises(ValueError, match=match): instance.role # pylint: disable=pointless-statement + + +def test_maintenance_mode_info_00810() -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - refresh() + - SwitchDetails() + - refresh() + - FabricDetailsByName() + - refresh() + + ### Summary + - Verify: + - ``_get()`` raises ``ValueError`` if ``filter`` (switch IP) + is not found in the controller response when the user accesses + a property. + + ### Setup - Data + - ``CONFIG``: ["192.168.1.2"] + - ``responses_SwitchDetails.json``: + - DATA[0].fabricName: LAN_Classic + - DATA[0].freezeMode: null + - DATA[0].ipAddress: 192.168.1.2 + - DATA[0].mode: Normal + - DATA[0].serialNumber: FDO211218FV + - DATA[0].switchRole: null + - DATA[0].systemMode: Normal + - RETURN_CODE: 200 + - MESSAGE: OK + - ``responses_FabricDetailsByName.json``: + - DATA[0].nvPairs.FABRIC_NAME: VXLAN_Fabric + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + - ``refresh()`` is called. + - ``filter`` is set to 1.2.3.4 + + + ### Trigger + - ``serial_number`` is accessed + + ### Expected Result + - Conditions in Summary are confirmed. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + yield responses_fabric_details_by_name(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "1.2.3.4" + + match = r"MaintenanceModeInfo\._get:\s+" + with pytest.raises(ValueError, match=match): + instance.serial_number # pylint: disable=pointless-statement + + +def test_maintenance_mode_info_00820() -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - refresh() + - SwitchDetails() + - refresh() + - FabricDetailsByName() + - refresh() + + ### Summary + - Verify: + - ``refresh`` re-raises ``ValueError`` raised by + ``SwitchDetails()._get()`` when ``item`` is not found in the + controller response. In this, case ``item`` is ``freezeMode``. + + ### Setup - Data + - ``CONFIG``: ["192.168.1.2"] + - ``responses_SwitchDetails.json`` is missing the key ``freezeMode``. + - ``responses_SwitchDetails.json``: + - DATA[0].fabricName: LAN_Classic + - DATA[0].ipAddress: 192.168.1.2 + - DATA[0].mode: Normal + - DATA[0].serialNumber: FDO211218FV + - DATA[0].switchRole: null + - DATA[0].systemMode: Normal + - RETURN_CODE: 200 + - MESSAGE: OK + - ``responses_FabricDetailsByName.json``: + - DATA[0].nvPairs.FABRIC_NAME: VXLAN_Fabric + - DATA[0].nvPairs.IS_READ_ONLY: false + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + + + ### Trigger + - ``refresh()`` is called. + + ### Expected Result + - Conditions in Summary are confirmed. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + yield responses_fabric_details_by_name(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + + match = r"MaintenanceModeInfo\.refresh:\s+" + match += r"Error setting properties for switch with ip_address\s+" + match += r"192\.168\.1\.2\.\s+" + match += r"Error details: SwitchDetails\._get: 192\.168\.1\.2 does not\s+" + match += r"have a key named freezeMode\." + with pytest.raises(ValueError, match=match): + instance.refresh() From c1ce7e97eb374cde13d19e5102c3b1e4f10a41b4 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 7 Jun 2024 10:53:26 -1000 Subject: [PATCH 141/230] MaintenanceModeInfo: 94% unit test coverage 1. test_maintenance_mode_info_00900: Verify ``config`` raises ``TypeError`` when set to an invalid type. 2. test_maintenance_mode_info_00910: Verify ``config`` raises ``TypeError`` when an element in the list is not a ``str`` --- .../common/maintenance_mode_info.py | 6 +- .../common/test_maintenance_mode_info.py | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/common/maintenance_mode_info.py b/plugins/module_utils/common/maintenance_mode_info.py index ae414fa5a..7af3780ad 100644 --- a/plugins/module_utils/common/maintenance_mode_info.py +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -393,9 +393,11 @@ def config(self, value): for item in value: if not isinstance(item, str): msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.config must be a list of strings " + msg += "config must be a list of strings " msg += "containing ip addresses. " - msg += f"Got type: {type(item).__name__}." + msg += "value contains element of type " + msg += f"{type(item).__name__}. " + msg += f"value: {value}." raise TypeError(msg) self._config = value diff --git a/tests/unit/module_utils/common/test_maintenance_mode_info.py b/tests/unit/module_utils/common/test_maintenance_mode_info.py index aff3dcc65..98d3e9a70 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode_info.py +++ b/tests/unit/module_utils/common/test_maintenance_mode_info.py @@ -1153,3 +1153,72 @@ def responses(): match += r"have a key named freezeMode\." with pytest.raises(ValueError, match=match): instance.refresh() + + +def test_maintenance_mode_info_00900() -> None: + """ + ### Classes and Methods + - ``MaintenanceModeInfo()`` + - ``config.setter`` + + ### Summary + - Verify: + - ``config`` raises ``TypeError`` when set to an invalid type. + + ### Setup - Data + None + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + + ### Trigger + - ``config`` is set to a value that is not a ``list``. + + ### Expected Result + - Conditions in Summary are confirmed. + """ + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + match = r"MaintenanceModeInfo\.config:\s+" + match += r"MaintenanceModeInfo\.config must be a list\.\s+" + match += r"Got type: str\." + with pytest.raises(TypeError, match=match): + instance.config = "NOT_A_LIST" + + +def test_maintenance_mode_info_00910() -> None: + """ + ### Classes and Methods + - ``MaintenanceModeInfo()`` + - ``config.setter`` + + ### Summary + - Verify: + - ``config`` raises ``TypeError`` when an element in the list is + not a ``str``. + + ### Setup - Data + None + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + + ### Trigger + - ``config`` is set to a value that is not a ``list``. + + ### Expected Result + - Conditions in Summary are confirmed. + """ + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + match = r"MaintenanceModeInfo\.config:\s+" + match += r"config must be a list\s+" + match += r"of strings containing ip addresses\.\s+" + match += r"value contains element of type int.\s+" + match += r"value:.*\." + with pytest.raises(TypeError, match=match): + instance.config = ["192.168.1.1", 10, "192.168.1.2"] From 86d0a125acaa0a7000aaceef06e78c7c547f5c1a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 7 Jun 2024 13:10:32 -1000 Subject: [PATCH 142/230] MaintenanceModeInfo: 100% unit test coverage. 1. MaintenanceModeInfo: Add ip_address as a key in the info dict i.e. info[ip_address]["ip_address"] 2. Add the following test cases: - test_maintenance_mode_info_01000: Verify ``info`` raises ``ValueError`` when accessed before ``refresh()`` is called. - test_maintenance_mode_info_01010 Verify ``info`` returns expected information in the happy path. --- .../common/maintenance_mode_info.py | 7 + .../responses_FabricDetailsByName.json | 19 +++ .../fixtures/responses_SwitchDetails.json | 28 ++++ .../common/test_maintenance_mode_info.py | 134 ++++++++++++++++++ 4 files changed, 188 insertions(+) diff --git a/plugins/module_utils/common/maintenance_mode_info.py b/plugins/module_utils/common/maintenance_mode_info.py index 7af3780ad..a59c8d4e8 100644 --- a/plugins/module_utils/common/maintenance_mode_info.py +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -279,24 +279,31 @@ def refresh(self): info[ip_address] = {} info[ip_address].update({"fabric_name": fabric_name}) + info[ip_address].update({"ip_address": ip_address}) + if freeze_mode is True: info[ip_address].update({"fabric_freeze_mode": True}) else: info[ip_address].update({"fabric_freeze_mode": False}) + if fabric_read_only is True: info[ip_address].update({"fabric_read_only": True}) else: info[ip_address].update({"fabric_read_only": False}) + if freeze_mode is True or fabric_read_only is True: info[ip_address].update({"fabric_deployment_disabled": True}) else: info[ip_address].update({"fabric_deployment_disabled": False}) + info[ip_address].update({"mode": mode}) + if role is not None: info[ip_address].update({"role": role}) else: info[ip_address].update({"role": "na"}) info[ip_address].update({"serial_number": serial_number}) + self.info = copy.deepcopy(info) def _get(self, item): diff --git a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json index 32d44e170..217198f3e 100644 --- a/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json +++ b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json @@ -120,5 +120,24 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_01010a": { + "TEST_NOTES": [ + "nvPairs.IS_READ_ONLY false", + "RETURN_CODE 200", + "MESSAGE OK" + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "VXLAN_Fabric", + "IS_READ_ONLY": "false" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json index 2033cd90d..278719b17 100644 --- a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -415,5 +415,33 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 + }, + "test_maintenance_mode_info_01010a": { + "TEST_NOTES": [ + "DATA[0].fabricName: VXLAN_Fabric", + "DATA[0].freezeMode: null", + "DATA[0].ipAddress: 192.168.1.2", + "DATA[0].mode: Normal", + "DATA[0].serialNumber: FDO123456FV", + "DATA[0].switchRole: leaf", + "DATA[0].systemMode: Maintenance", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "mode": "Normal", + "serialNumber": "FDO123456FV", + "switchRole": "leaf", + "systemMode": "Maintenance" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_maintenance_mode_info.py b/tests/unit/module_utils/common/test_maintenance_mode_info.py index 98d3e9a70..6d20cc9f3 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode_info.py +++ b/tests/unit/module_utils/common/test_maintenance_mode_info.py @@ -1222,3 +1222,137 @@ def test_maintenance_mode_info_00910() -> None: match += r"value:.*\." with pytest.raises(TypeError, match=match): instance.config = ["192.168.1.1", 10, "192.168.1.2"] + + +def test_maintenance_mode_info_01000() -> None: + """ + ### Classes and Methods + - ``MaintenanceModeInfo()`` + - ``info.getter`` + + ### Summary + - Verify: + - ``info`` raises ``ValueError`` when accessed before + ``refresh()`` is called. + + ### Setup - Data + None + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + + ### Trigger + - ``info`` is accessed without having first called ``refresh()``. + + ### Expected Result + - Conditions in Summary are confirmed. + """ + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + match = r"MaintenanceModeInfo\.info:\s+" + match += r"MaintenanceModeInfo\.refresh\(\) must be called before\s+" + match += r"accessing MaintenanceModeInfo\.info\." + with pytest.raises(ValueError, match=match): + info = instance.info # pylint: disable=unused-variable + + +def test_maintenance_mode_info_01010() -> None: + """ + ### Classes and Methods + - ``MaintenanceModeInfo()`` + - ``info.getter`` + + ### Summary + - Verify: + - ``info`` returns expected information in the happy path. + + ### Setup - Data + - ``CONFIG``: ["192.168.1.2"] + - ``responses_SwitchDetails.json``: + - DATA[0].fabricName: VXLAN_Fabric + - DATA[0].freezeMode: null + - DATA[0].ipAddress: 192.168.1.2 + - DATA[0].mode: Normal + - DATA[0].serialNumber: FDO211218FV + - DATA[0].switchRole: leaf + - DATA[0].systemMode: Maintenance + - RETURN_CODE: 200 + - MESSAGE: OK + - ``responses_FabricDetailsByName.json``: + - DATA[0].nvPairs.FABRIC_NAME: VXLAN_Fabric + - DATA[0].nvPairs.IS_READ_ONLY: false + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + + ### Trigger + - ``info`` is accessed without having first called ``refresh()``. + + ### Expected Result + - Conditions in Summary are confirmed. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + yield responses_fabric_details_by_name(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + instance.config = CONFIG + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + assert instance.info[CONFIG[0]]["fabric_name"] == FABRIC_NAME + assert instance.info[CONFIG[0]]["fabric_freeze_mode"] is False + assert instance.info[CONFIG[0]]["fabric_read_only"] is False + assert instance.info[CONFIG[0]]["fabric_deployment_disabled"] is False + assert instance.info[CONFIG[0]]["ip_address"] == "192.168.1.2" + assert instance.info[CONFIG[0]]["mode"] == "inconsistent" + assert instance.info[CONFIG[0]]["role"] == "leaf" + assert instance.info[CONFIG[0]]["serial_number"] == "FDO123456FV" + + +def test_maintenance_mode_info_01020() -> None: + """ + ### Classes and Methods + - ``MaintenanceModeInfo()`` + - ``info.setter`` + + ### Summary + - Verify: + - ``info`` raises ``TypeError`` when set to an invalid type. + + ### Setup - Data + None + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + + ### Trigger + - ``info`` is set to a value that is not a ``dict``. + + ### Expected Result + - Conditions in Summary are confirmed. + """ + with does_not_raise(): + instance = MaintenanceModeInfo(PARAMS) + + match = r"MaintenanceModeInfo\.info\.setter:\s+" + match += r"value must be a dict\.\s+" + match += r"Got value NOT_A_DICT of type str\." + with pytest.raises(TypeError, match=match): + instance.info = "NOT_A_DICT" From e723018145c10c1f8c85b2ff3e46f5daa587311c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 7 Jun 2024 15:06:19 -1000 Subject: [PATCH 143/230] FabricDetails() v2: 43% unit test coverage. Added the following test cases: 1. test_fabric_details_v2_00130 Verify refresh_super() behavior when RETURN_CODE is 500. 2. test_fabric_details_v2_00140 Verify refresh_super() raises ``ValueError when ``register_result()`` raises ``ValueError``. --- .../module_utils/fabric/fabric_details_v2.py | 9 +- .../fixtures/responses_FabricDetails_V2.json | 36 +++++ .../dcnm_fabric/test_fabric_details_v2.py | 137 ++++++++++++++++++ 3 files changed, 180 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index 55870eeba..970bf35bf 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -69,6 +69,7 @@ def __init__(self, params): msg += f"params: {params}." raise ValueError(msg) + self.action = "fabric_details" self.log = logging.getLogger(f"dcnm.{self.class_name}") msg = "ENTERED FabricDetails() (v2)" @@ -90,8 +91,9 @@ def register_result(self): - ``ValueError``if: - ``Results()`` raises ``TypeError`` """ + method_name = inspect.stack()[0][3] try: - self.results.action = "fabric_details" + self.results.action = self.action self.results.response_current = self.rest_send.response_current self.results.result_current = self.rest_send.result_current if self.results.response_current.get("RETURN_CODE") == 200: @@ -102,7 +104,10 @@ def register_result(self): self.results.changed = False self.results.register_task_result() except TypeError as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Failed to register result. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error def validate_refresh_parameters(self) -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetails_V2.json b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetails_V2.json index d3fac4a7c..b5a2eb17a 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetails_V2.json +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetails_V2.json @@ -340,5 +340,41 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", "RETURN_CODE": 200 + }, + "test_fabric_details_v2_00130a": { + "TEST_NOTES": [ + "DATA[0].nvPairs.FABRIC_NAME: VXLAN_Fabric", + "RETURN_CODE is 500", + "MESSAGE: Internal server error" + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "VXLAN_Fabric" + } + } + ], + "MESSAGE": "Internal server error", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 500 + }, + "test_fabric_details_v2_00140a": { + "TEST_NOTES": [ + "DATA[0].nvPairs.FABRIC_NAME: VXLAN_Fabric", + "RETURN_CODE is 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "VXLAN_Fabric" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 } } diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py index 9468922e8..b6755ed2c 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py @@ -305,6 +305,143 @@ def responses(): assert instance.all_data.get("f1", {}).get("nvPairs", {}).get("FABRIC_NAME") == "f1" +def test_fabric_details_v2_00130(fabric_details_v2) -> None: + """ + ### Classes and Methods + - FabricDetails() + - register_result() + - refresh_super() + + ### Summary + - Verify refresh_super() behavior when: + - RETURN_CODE is 500. + + ### Setup Data + - ``responses_FabricDetails_V2.json``: + - DATA[0].nvPairs.FABRIC_NAME: VXLAN_Fabric + - RETURN_CODE: 500 + - MESSAGE: Internal server error + + ### Setup Code + - FabricDetails() is instantiated + - FabricDetails().RestSend() is instantiated + - FabricDetails().Results() is instantiated + - FabricDetails().refresh_super() is called + + ### Trigger + - FabricDetails().refresh_super() is called + + ### Expected Result + - Exception is not raised + - Results() are updated to expected values + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = fabric_details_v2 + instance.rest_send = rest_send + instance.results = Results() + + with does_not_raise(): + instance.refresh_super() + + assert isinstance(instance.results.diff, list) + assert isinstance(instance.results.result, list) + assert isinstance(instance.results.response, list) + + assert len(instance.results.diff) == 1 + assert len(instance.results.result) == 1 + assert len(instance.results.response) == 1 + + assert instance.results.diff[0].get("sequence_number", None) == 1 + + assert instance.results.response[0].get("RETURN_CODE", None) == 500 + assert instance.results.result[0].get("found", None) is False + assert instance.results.result[0].get("success", None) is False + + assert True in instance.results.failed + assert False not in instance.results.failed + assert False in instance.results.changed + assert True not in instance.results.changed + assert ( + instance.all_data.get("VXLAN_Fabric", {}).get("nvPairs", {}).get("FABRIC_NAME") + == "VXLAN_Fabric" + ) + + +def test_fabric_details_v2_00140(fabric_details_v2, monkeypatch) -> None: + """ + ### Classes and Methods + - FabricDetails() + - register_result() + - refresh_super() + + ### Summary + - Verify refresh_super() raises ``ValueError when: + - ``register_result()`` raises ``ValueError``. + + ### Setup Data + - ``responses_FabricDetails_V2.json``: + - DATA[0].nvPairs.FABRIC_NAME: VXLAN_Fabric + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup Code + - FabricDetails() is instantiated + - FabricDetails().action is monkey-patched to int 10. + - FabricDetails().RestSend() is instantiated + - FabricDetails().Results() is instantiated + - FabricDetails().refresh_super() is called + + ###Code Flow - Test + - FabricDetails().refresh_super() is called + + ### Expected Result + - Exception is not raised + - instance.all_data returns expected fabric data + - Results() are updated + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend({"state": "query", "check_mode": False}) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = fabric_details_v2 + instance.rest_send = rest_send + instance.results = Results() + + monkeypatch.setattr(instance, "action", 10) + + match = r"FabricDetails\.register_result:\s+" + match += r"Failed to register result\.\s+" + match += r"Error detail:\s+" + match += r"Results\.action: instance\.action must be a string\. Got 10\." + with pytest.raises(ValueError, match=match): + instance.refresh_super() + + def test_fabric_details_v2_00200(fabric_details_v2) -> None: """ ### Classes and Methods From ba2b64577217033c476cbfc5651644e3bb46ac87 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 9 Jun 2024 13:55:44 -0700 Subject: [PATCH 144/230] FabricDetails() v2: unit test coverage 47% Add the following test cases: - test_fabric_details_v2_00150 Verify refresh_super() behavior when ``rest_send`` is not set. - test_fabric_details_v2_00160 Verify refresh_super() behavior when ``results`` is not set. - test_fabric_details_v2_00170 Verify refresh_super() raises ``ValueError`` when ``rest_send`` raises ``TypeError``. --- .../module_utils/fabric/fabric_details_v2.py | 3 + .../dcnm_fabric/test_fabric_details_v2.py | 111 ++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index 970bf35bf..22572ebbf 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -81,6 +81,9 @@ def __init__(self, params): self.conversion = ConversionUtils() self.ep_fabrics = EpFabrics() + self._rest_send = None + self._results = None + def register_result(self): """ ### Summary diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py index b6755ed2c..102c975a7 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py @@ -442,6 +442,117 @@ def responses(): instance.refresh_super() +def test_fabric_details_v2_00150(fabric_details_v2) -> None: + """ + ### Classes and Methods + - FabricDetails() + - __init__() + - refresh_super() + + ### Summary + - Verify refresh_super() behavior when: + - ``rest_send`` is not set. + + ### Setup - Code + - FabricDetails() is instantiated + - FabricDetails().Results() is instantiated + + ### Trigger + - FabricDetails().refresh_super() is called + + ### Expected Result + - ``ValueError`` is raised. + - Error message matches expected. + """ + with does_not_raise(): + instance = fabric_details_v2 + instance.results = Results() + + match = r"FabricDetails\.validate_refresh_parameters:\s+" + match += r"FabricDetails\.rest_send must be set before calling\s+" + match += r"FabricDetails\.refresh\(\)\." + with pytest.raises(ValueError, match=match): + instance.refresh_super() + + +def test_fabric_details_v2_00160(fabric_details_v2) -> None: + """ + ### Classes and Methods + - FabricDetails() + - __init__() + - refresh_super() + + ### Summary + - Verify refresh_super() behavior when: + - ``results`` is not set. + + ### Setup - Code + - FabricDetails() is instantiated + - FabricDetails().RestSend() is instantiated + + ### Trigger + - FabricDetails().refresh_super() is called + + ### Expected Result + - ``ValueError`` is raised. + - Error message matches expected. + """ + with does_not_raise(): + instance = fabric_details_v2 + instance.rest_send = RestSend({"state": "merged", "check_mode": False}) + + match = r"FabricDetails\.validate_refresh_parameters:\s+" + match += r"FabricDetails\.results must be set before calling\s+" + match += r"FabricDetails\.refresh\(\)\." + with pytest.raises(ValueError, match=match): + instance.refresh_super() + + +def test_fabric_details_v2_00170(fabric_details_v2, monkeypatch) -> None: + """ + ### Classes and Methods + - FabricDetails() + - __init__() + - refresh_super() + + ### Summary + - Verify refresh_super() raises ``ValueError`` when + ``rest_send`` raises ``TypeError``. + + ### Setup - Code + - FabricDetails() is instantiated. + - FabricDetails().results is set. + - FabricDetails().rest_send is set. + - EpFabrics().verb is mocked to raise ``TypeError`` + + ### Trigger + - FabricDetails().refresh_super() is called + + ### Expected Result + - ``ValueError`` is raised. + - Error message matches expected. + """ + + class MockEpFabrics: + @property + def verb(self): + raise TypeError("MockEpFabrics.bad_verb") + + @property + def path(self): + return "/path" + + with does_not_raise(): + instance = fabric_details_v2 + instance.rest_send = RestSend({"state": "merged", "check_mode": False}) + instance.results = Results() + + monkeypatch.setattr(instance, "ep_fabrics", MockEpFabrics()) + match = r"MockEpFabrics\.bad_verb" + with pytest.raises(ValueError, match=match): + instance.refresh_super() + + def test_fabric_details_v2_00200(fabric_details_v2) -> None: """ ### Classes and Methods From cb67b836409c96284121cb75ebbffd4938c6590f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 10 Jun 2024 10:49:04 -0700 Subject: [PATCH 145/230] FabricDetails: 76% unit test coverage 1. dcnm_fabric/utils.py: Add fixture and response reader for fabric_details_by_name_v2 2. test_fabric_details_by_name_v2.py - Add the following test cases - test_fabric_details_by_name_v2_00200: Verify property access after 200 controller response - test_fabric_details_by_name_v2_00300: Verify properties return None if property is missing in the controller response. --- .../module_utils/fabric/fabric_details_v2.py | 30 +- .../responses_FabricDetailsByName_V2.json | 342 ++++++++++++++++++ .../test_fabric_details_by_name_v2.py | 199 ++++++++++ tests/unit/modules/dcnm/dcnm_fabric/utils.py | 23 ++ 4 files changed, 592 insertions(+), 2 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json create mode 100644 tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index 22572ebbf..a66eea634 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -223,7 +223,9 @@ def asn(self): """ ### Summary Return the BGP asn of the fabric specified with filter, if it exists. - Return None otherwise + Return None otherwise. + + This is an alias of BGP_AS. ### Raises None @@ -236,12 +238,36 @@ def asn(self): - None """ try: - return self._get("asn") + return self._get_nv_pair("BGP_AS") except ValueError as error: msg = f"Failed to retrieve asn: Error detail: {error}" self.log.debug(msg) return None + @property + def bgp_as(self): + """ + ### Summary + Return ``nvPairs.BGP_AS`` of the fabric specified with filter, if it exists. + Return None otherwise + + ### Raises + None + + ### Type + string + + ### Returns + - e.g. "65000" + - None + """ + try: + return self._get_nv_pair("BGP_AS") + except ValueError as error: + msg = f"Failed to retrieve bgp_as: Error detail: {error}" + self.log.debug(msg) + return None + @property def deployment_freeze(self): """ diff --git a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json new file mode 100644 index 000000000..8f3fec4fc --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json @@ -0,0 +1,342 @@ +{ + "test_notes": [ + "Mocked responses for FabricDetails() class" + ], + "test_fabric_details_by_name_v2_00200a": { + "TEST_NOTES": [ + "Verify property return values.", + "DATA contains one fabric dict.", + "RETURN_CODE == 200." + ], + "DATA": [ + { + "asn": "65001", + "createdOn": 1711411093680, + "deviceType": "n9k", + "fabricId": "FABRIC-2", + "fabricName": "f1", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN EVPN", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", + "id": 2, + "modifiedOn": 1711411096857, + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplate": "Default_Network_Universal", + "nvPairs": { + "AAA_REMOTE_IP_ENABLED": "false", + "AAA_SERVER_CONF": "", + "ACTIVE_MIGRATION": "false", + "ADVERTISE_PIP_BGP": "false", + "ADVERTISE_PIP_ON_BORDER": "true", + "AGENT_INTF": "eth0", + "AI_ML_QOS_POLICY": "AI_Fabric_QOS_400G", + "ALLOW_NXC": "true", + "ALLOW_NXC_PREV": "true", + "ANYCAST_BGW_ADVERTISE_PIP": "false", + "ANYCAST_GW_MAC": "2020.0000.00aa", + "ANYCAST_LB_ID": "", + "ANYCAST_RP_IP_RANGE": "10.254.254.0/24", + "ANYCAST_RP_IP_RANGE_INTERNAL": "", + "AUTO_SYMMETRIC_DEFAULT_VRF": "false", + "AUTO_SYMMETRIC_VRF_LITE": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", + "AUTO_VRFLITE_IFC_DEFAULT_VRF": "false", + "BANNER": "", + "BFD_AUTH_ENABLE": "false", + "BFD_AUTH_KEY": "", + "BFD_AUTH_KEY_ID": "", + "BFD_ENABLE": "false", + "BFD_ENABLE_PREV": "", + "BFD_IBGP_ENABLE": "false", + "BFD_ISIS_ENABLE": "false", + "BFD_OSPF_ENABLE": "false", + "BFD_PIM_ENABLE": "false", + "BGP_AS": "65001", + "BGP_AS_PREV": "65001", + "BGP_AUTH_ENABLE": "false", + "BGP_AUTH_KEY": "", + "BGP_AUTH_KEY_TYPE": "3", + "BGP_LB_ID": "0", + "BOOTSTRAP_CONF": "", + "BOOTSTRAP_ENABLE": "false", + "BOOTSTRAP_ENABLE_PREV": "false", + "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", + "BOOTSTRAP_MULTISUBNET_INTERNAL": "", + "BRFIELD_DEBUG_FLAG": "Disable", + "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", + "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", + "CDP_ENABLE": "false", + "COPP_POLICY": "strict", + "DCI_SUBNET_RANGE": "10.33.0.0/16", + "DCI_SUBNET_TARGET_MASK": "30", + "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", + "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", + "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", + "DEFAULT_VRF_REDIS_BGP_RMAP": "", + "DEPLOYMENT_FREEZE": "false", + "DHCP_ENABLE": "false", + "DHCP_END": "", + "DHCP_END_INTERNAL": "", + "DHCP_IPV6_ENABLE": "", + "DHCP_IPV6_ENABLE_INTERNAL": "", + "DHCP_START": "", + "DHCP_START_INTERNAL": "", + "DNS_SERVER_IP_LIST": "", + "DNS_SERVER_VRF": "", + "DOMAIN_NAME_INTERNAL": "", + "ENABLE_AAA": "false", + "ENABLE_AGENT": "false", + "ENABLE_AI_ML_QOS_POLICY": "false", + "ENABLE_AI_ML_QOS_POLICY_FLAP": "false", + "ENABLE_DEFAULT_QUEUING_POLICY": "false", + "ENABLE_EVPN": "true", + "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", + "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", + "ENABLE_L3VNI_NO_VLAN": "false", + "ENABLE_MACSEC": "false", + "ENABLE_NETFLOW": "false", + "ENABLE_NETFLOW_PREV": "false", + "ENABLE_NGOAM": "true", + "ENABLE_NXAPI": "true", + "ENABLE_NXAPI_HTTP": "true", + "ENABLE_PBR": "false", + "ENABLE_PVLAN": "false", + "ENABLE_PVLAN_PREV": "false", + "ENABLE_SGT": "false", + "ENABLE_SGT_PREV": "false", + "ENABLE_TENANT_DHCP": "true", + "ENABLE_TRM": "false", + "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", + "ESR_OPTION": "PBR", + "EXTRA_CONF_INTRA_LINKS": "", + "EXTRA_CONF_LEAF": "", + "EXTRA_CONF_SPINE": "", + "EXTRA_CONF_TOR": "", + "EXT_FABRIC_TYPE": "", + "FABRIC_INTERFACE_TYPE": "p2p", + "FABRIC_MTU": "9216", + "FABRIC_MTU_PREV": "9216", + "FABRIC_NAME": "f1", + "FABRIC_TYPE": "Switch_Fabric", + "FABRIC_VPC_DOMAIN_ID": "", + "FABRIC_VPC_DOMAIN_ID_PREV": "", + "FABRIC_VPC_QOS": "false", + "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", + "FEATURE_PTP": "false", + "FEATURE_PTP_INTERNAL": "false", + "FF": "Easy_Fabric", + "GRFIELD_DEBUG_FLAG": "Disable", + "HD_TIME": "180", + "HOST_INTF_ADMIN_STATE": "true", + "IBGP_PEER_TEMPLATE": "", + "IBGP_PEER_TEMPLATE_LEAF": "", + "INBAND_DHCP_SERVERS": "", + "INBAND_MGMT": "false", + "INBAND_MGMT_PREV": "false", + "ISIS_AREA_NUM": "0001", + "ISIS_AREA_NUM_PREV": "", + "ISIS_AUTH_ENABLE": "false", + "ISIS_AUTH_KEY": "", + "ISIS_AUTH_KEYCHAIN_KEY_ID": "", + "ISIS_AUTH_KEYCHAIN_NAME": "", + "ISIS_LEVEL": "level-2", + "ISIS_OVERLOAD_ELAPSE_TIME": "", + "ISIS_OVERLOAD_ENABLE": "", + "ISIS_P2P_ENABLE": "", + "L2_HOST_INTF_MTU": "9216", + "L2_HOST_INTF_MTU_PREV": "9216", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "L3VNI_MCAST_GROUP": "", + "L3_PARTITION_ID_RANGE": "50000-59000", + "LINK_STATE_ROUTING": "ospf", + "LINK_STATE_ROUTING_TAG": "UNDERLAY", + "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", + "LOOPBACK0_IPV6_RANGE": "", + "LOOPBACK0_IP_RANGE": "10.2.0.0/22", + "LOOPBACK1_IPV6_RANGE": "", + "LOOPBACK1_IP_RANGE": "10.3.0.0/22", + "MACSEC_ALGORITHM": "", + "MACSEC_CIPHER_SUITE": "", + "MACSEC_FALLBACK_ALGORITHM": "", + "MACSEC_FALLBACK_KEY_STRING": "", + "MACSEC_KEY_STRING": "", + "MACSEC_REPORT_TIMER": "", + "MGMT_GW": "", + "MGMT_GW_INTERNAL": "", + "MGMT_PREFIX": "", + "MGMT_PREFIX_INTERNAL": "", + "MGMT_V6PREFIX": "", + "MGMT_V6PREFIX_INTERNAL": "", + "MPLS_HANDOFF": "false", + "MPLS_ISIS_AREA_NUM": "0001", + "MPLS_ISIS_AREA_NUM_PREV": "", + "MPLS_LB_ID": "", + "MPLS_LOOPBACK_IP_RANGE": "", + "MSO_CONNECTIVITY_DEPLOYED": "", + "MSO_CONTROLER_ID": "", + "MSO_SITE_GROUP_NAME": "", + "MSO_SITE_ID": "", + "MST_INSTANCE_RANGE": "", + "MULTICAST_GROUP_SUBNET": "239.1.1.0/25", + "NETFLOW_EXPORTER_LIST": "", + "NETFLOW_MONITOR_LIST": "", + "NETFLOW_RECORD_LIST": "", + "NETWORK_VLAN_RANGE": "2300-2999", + "NTP_SERVER_IP_LIST": "", + "NTP_SERVER_VRF": "", + "NVE_LB_ID": "1", + "NXAPI_HTTPS_PORT": "443", + "NXAPI_HTTP_PORT": "80", + "NXC_DEST_VRF": "", + "NXC_PROXY_PORT": "8080", + "NXC_PROXY_SERVER": "", + "NXC_SRC_INTF": "", + "OBJECT_TRACKING_NUMBER_RANGE": "100-299", + "OSPF_AREA_ID": "0.0.0.0", + "OSPF_AUTH_ENABLE": "false", + "OSPF_AUTH_KEY": "", + "OSPF_AUTH_KEY_ID": "", + "OVERLAY_MODE": "cli", + "OVERLAY_MODE_PREV": "cli", + "OVERWRITE_GLOBAL_NXC": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION_V6_PREV": "false", + "PER_VRF_LOOPBACK_IP_RANGE": "", + "PER_VRF_LOOPBACK_IP_RANGE_V6": "", + "PHANTOM_RP_LB_ID1": "", + "PHANTOM_RP_LB_ID2": "", + "PHANTOM_RP_LB_ID3": "", + "PHANTOM_RP_LB_ID4": "", + "PIM_HELLO_AUTH_ENABLE": "false", + "PIM_HELLO_AUTH_KEY": "", + "PM_ENABLE": "false", + "PM_ENABLE_PREV": "false", + "POWER_REDUNDANCY_MODE": "ps-redundant", + "PREMSO_PARENT_FABRIC": "", + "PTP_DOMAIN_ID": "", + "PTP_LB_ID": "", + "REPLICATION_MODE": "Multicast", + "ROUTER_ID_RANGE": "", + "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", + "RP_COUNT": "2", + "RP_LB_ID": "254", + "RP_MODE": "asm", + "RR_COUNT": "2", + "SEED_SWITCH_CORE_INTERFACES": "", + "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", + "SGT_ID_RANGE": "", + "SGT_NAME_PREFIX": "", + "SGT_PREPROVISION": "", + "SITE_ID": "65001", + "SLA_ID_RANGE": "10000-19999", + "SNMP_SERVER_HOST_TRAP": "true", + "SPINE_COUNT": "0", + "SPINE_SWITCH_CORE_INTERFACES": "", + "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", + "SSPINE_COUNT": "0", + "STATIC_UNDERLAY_IP_ALLOC": "false", + "STP_BRIDGE_PRIORITY": "", + "STP_ROOT_OPTION": "unmanaged", + "STP_VLAN_RANGE": "", + "STRICT_CC_MODE": "false", + "SUBINTERFACE_RANGE": "2-511", + "SUBNET_RANGE": "10.4.0.0/16", + "SUBNET_TARGET_MASK": "30", + "SYSLOG_SERVER_IP_LIST": "", + "SYSLOG_SERVER_VRF": "", + "SYSLOG_SEV": "", + "TCAM_ALLOCATION": "true", + "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", + "UNDERLAY_IS_V6": "false", + "UNNUM_BOOTSTRAP_LB_ID": "", + "UNNUM_DHCP_END": "", + "UNNUM_DHCP_END_INTERNAL": "", + "UNNUM_DHCP_START": "", + "UNNUM_DHCP_START_INTERNAL": "", + "UPGRADE_FROM_VERSION": "", + "USE_LINK_LOCAL": "true", + "V6_SUBNET_RANGE": "", + "V6_SUBNET_TARGET_MASK": "126", + "VPC_AUTO_RECOVERY_TIME": "360", + "VPC_DELAY_RESTORE": "150", + "VPC_DELAY_RESTORE_TIME": "60", + "VPC_DOMAIN_ID_RANGE": "1-1000", + "VPC_ENABLE_IPv6_ND_SYNC": "true", + "VPC_PEER_KEEP_ALIVE_OPTION": "management", + "VPC_PEER_LINK_PO": "500", + "VPC_PEER_LINK_VLAN": "3600", + "VRF_LITE_AUTOCONFIG": "Manual", + "VRF_VLAN_RANGE": "2000-2299", + "abstract_anycast_rp": "anycast_rp", + "abstract_bgp": "base_bgp", + "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", + "abstract_bgp_rr": "evpn_bgp_rr", + "abstract_dhcp": "base_dhcp", + "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", + "abstract_extra_config_leaf": "extra_config_leaf", + "abstract_extra_config_spine": "extra_config_spine", + "abstract_extra_config_tor": "extra_config_tor", + "abstract_feature_leaf": "base_feature_leaf_upg", + "abstract_feature_spine": "base_feature_spine_upg", + "abstract_isis": "base_isis_level2", + "abstract_isis_interface": "isis_interface", + "abstract_loopback_interface": "int_fabric_loopback_11_1", + "abstract_multicast": "base_multicast_11_1", + "abstract_ospf": "base_ospf", + "abstract_ospf_interface": "ospf_interface_11_1", + "abstract_pim_interface": "pim_interface", + "abstract_route_map": "route_map", + "abstract_routed_host": "int_routed_host", + "abstract_trunk_host": "int_trunk_host", + "abstract_vlan_interface": "int_fabric_vlan_11_1", + "abstract_vpc_domain": "base_vpc_domain_11_1", + "default_network": "Default_Network_Universal", + "default_pvlan_sec_network": "", + "default_vrf": "Default_VRF_Universal", + "enableRealTimeBackup": "", + "enableScheduledBackup": "", + "network_extension_template": "Default_Network_Extension_Universal", + "scheduledTime": "", + "temp_anycast_gateway": "anycast_gateway", + "temp_vpc_domain_mgmt": "vpc_domain_mgmt", + "temp_vpc_peer_link": "int_vpc_peer_link_po", + "vrf_extension_template": "Default_VRF_Extension_Universal" + }, + "operStatus": "HEALTHY", + "provisionMode": "DCNMTopDown", + "replicationMode": "Multicast", + "siteId": "65001", + "templateName": "Easy_Fabric", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplate": "Default_VRF_Universal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_by_name_v2_00300a": { + "TEST_NOTES": [ + "Verify asn property exception.", + "DATA contains one fabric dict.", + "DATA[0].nvPairs.FABRIC_NAME == f1", + "RETURN_CODE == 200." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "f1" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + } +} diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py new file mode 100644 index 000000000..1815372d2 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py @@ -0,0 +1,199 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-import +# pylint: disable=redefined-outer-name +# pylint: disable=protected-access +# pylint: disable=unused-argument +# pylint: disable=invalid-name + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import inspect + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ + EpFabrics +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ + FabricDetailsByName +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( + ResponseGenerator, does_not_raise, fabric_details_by_name_v2_fixture, + responses_fabric_details_by_name_v2) + +PARAMS = {"state": "query", "check_mode": False} + + +def test_fabric_details_by_name_v2_00200(fabric_details_by_name_v2) -> None: + """ + ### Classes and Methods + - FabricDetails() + - __init__() + - refresh_super() + + ### Summary + - Verify property access after 200 controller response: + - RETURN_CODE is 200. + - Controller response contains one fabric (f1). + + ### Code Flow - Setup + - FabricDetails() is instantiated + - FabricDetails().RestSend() is instantiated + - FabricDetails().Results() is instantiated + - FabricDetails().refresh_super() is called + - responses_FabricDetails contains a dict with: + - RETURN_CODE == 200 + - DATA == [] + + ###Code Flow - Test + - FabricDetails().refresh_super() is called. + - All properties are accessed and verified. + + ### Expected Result + - Exception is not raised. + - All properties return expected values. + - Results() are updated. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_by_name_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = fabric_details_by_name_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.filter = "f1" + + with does_not_raise(): + instance.refresh() + + assert isinstance(instance.results.diff, list) + assert isinstance(instance.results.result, list) + assert isinstance(instance.results.response, list) + + assert len(instance.results.diff) == 1 + assert len(instance.results.result) == 1 + assert len(instance.results.response) == 1 + + assert instance.results.diff[0].get("sequence_number", None) == 1 + + assert instance.results.response[0].get("RETURN_CODE", None) == 200 + assert instance.results.result[0].get("found", None) is True + assert instance.results.result[0].get("success", None) is True + + assert False in instance.results.failed + assert True not in instance.results.failed + assert False in instance.results.changed + assert True not in instance.results.changed + + assert instance.all_data.get("f1", {}).get("asn", None) == "65001" + assert instance.all_data.get("f1", {}).get("nvPairs", {}).get("FABRIC_NAME") == "f1" + + assert instance.asn == "65001" + assert instance.deployment_freeze is False + assert instance.enable_pbr is False + assert instance.fabric_id == "FABRIC-2" + assert instance.fabric_type == "Switch_Fabric" + assert instance.is_read_only is None + assert instance.replication_mode == "Multicast" + assert instance.template_name == "Easy_Fabric" + + +def test_fabric_details_by_name_v2_00300(fabric_details_by_name_v2) -> None: + """ + ### Classes and Methods + - FabricDetails() + - __init__() + - refresh() + + ### Summary + - Verify properties return None if property is missing in the + controller response. + - RETURN_CODE is 200. + - Controller response contains one fabric (f1). + + ### Setup - Code + - FabricDetailsByName() is instantiated + - FabricDetailsByName().RestSend() is instantiated + - FabricDetailsByName().Results() is instantiated + - FabricDetailsByName().refresh() is called + + ### Setup - Data + - responses_FabricDetailsByName_V2 contains a dict with: + - RETURN_CODE == 200 + - DATA[0].nvPairs.FABRIC_NAME == "f1" + - DATA[0].nvPairs + + ### Expected Result + - ``ValueError`` is raised for each property. + - Results() are updated. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_by_name_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = fabric_details_by_name_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.filter = "f1" + instance.refresh() + + assert instance.asn is None + assert instance.bgp_as is None + assert instance.deployment_freeze is None + assert instance.enable_pbr is None + assert instance.fabric_id is None + assert instance.fabric_type is None + assert instance.is_read_only is None + assert instance.replication_mode is None + assert instance.template_name is None diff --git a/tests/unit/modules/dcnm/dcnm_fabric/utils.py b/tests/unit/modules/dcnm/dcnm_fabric/utils.py index f4c3147be..5178234cc 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/utils.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/utils.py @@ -39,6 +39,8 @@ FabricDetails, FabricDetailsByName, FabricDetailsByNvPair) from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ FabricDetails as FabricDetailsV2 +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ + FabricDetailsByName as FabricDetailsByNameV2 from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_summary import \ FabricSummary from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ @@ -247,6 +249,17 @@ def fabric_details_by_name_fixture(): return FabricDetailsByName(instance.params) +@pytest.fixture(name="fabric_details_by_name_v2") +def fabric_details_by_name_v2_fixture(): + """ + mock FabricDetailsByName version 2 + """ + instance = MockAnsibleModule() + instance.state = "query" + instance.check_mode = False + return FabricDetailsByNameV2(instance.params) + + @pytest.fixture(name="fabric_details_by_nv_pair") def fabric_details_by_nv_pair_fixture(): """ @@ -527,6 +540,16 @@ def responses_fabric_details_by_name(key: str) -> Dict[str, str]: return data +def responses_fabric_details_by_name_v2(key: str) -> Dict[str, str]: + """ + Return responses for FabricDetailsByName + """ + data_file = "responses_FabricDetailsByName_V2" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + def responses_fabric_details_by_nv_pair(key: str) -> Dict[str, str]: """ Return responses for FabricDetailsByNvPair From 6eb494ff15929a61c7ab4fc44741a8c1931c8375 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 10 Jun 2024 11:27:59 -0700 Subject: [PATCH 146/230] FabricDetails: 78% unit test coverage 1. test_fabric_details_by_name_v2_00000 Verify that refresh() raises ``ValueError`` if ``refresh_super()`` raises ``ValueError`` --- .../test_fabric_details_by_name_v2.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py index 1815372d2..4843592a7 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py @@ -29,6 +29,7 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import copy import inspect import pytest @@ -53,6 +54,56 @@ PARAMS = {"state": "query", "check_mode": False} +def test_fabric_details_by_name_v2_00000(monkeypatch) -> None: + """ + ### Classes and Methods + - FabricDetailsByName() + - __init__() + + ### Summary + - Verify that refresh() raises ``ValueError`` if ``refresh_super()`` + raises ``ValueError`` + + ### Setup - Code + - FabricDetails().refresh_supper() is mocked to raise ``ValueError``. + - FabricDetailsByName() is instantiated + - FabricDetailsByName().RestSend() is instantiated + - FabricDetailsByName().Results() is instantiated + + ### Setup - Data + - None + + ### Trigger + - FabricDetailsByName().refresh() is called + + ### Expected Result + - FabricDetailsByName().refresh() raises ``ValueError``. + - Error message matches expectation. + """ + # method_name = inspect.stack()[0][3] + # key = f"{method_name}a" + + # def responses(): + # yield {} + + # sender = Sender() + # sender.gen = ResponseGenerator(responses()) + # rest_send = RestSend(PARAMS) + # rest_send.response_handler = ResponseHandler() + # rest_send.sender = sender + # rest_send.unit_test = True + # rest_send.timeout = 1 + + match = r"FabricDetailsByName\.__init__:\s+" + match += r"Failed in super\(\)\.__init__\(\)\.\s+" + match += r"Error detail: FabricDetailsByName\.__init__:\s+" + match += r"check_mode is missing from params\. params:.*" + params = copy.copy(PARAMS) + params.pop("check_mode", None) + with pytest.raises(ValueError, match=match): + FabricDetailsByName(params) # pytest: disable=pointless-statement + + def test_fabric_details_by_name_v2_00200(fabric_details_by_name_v2) -> None: """ ### Classes and Methods From d62e09607206e79f16f8767835556b505b216ae0 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 10 Jun 2024 23:40:18 -0700 Subject: [PATCH 147/230] FabricDetails: 79% unit test coverage 1. Add testcase: test_fabric_details_by_name_v2_00400 Verify refresh() raises ``ValueError`` if ``FabricDetails().refresh_super()`` raises ``ValueError``. 2. test_fabric_details_by_name_v2.py: Remove unused imports EpFabrics, ConversionUtils. --- .../test_fabric_details_by_name_v2.py | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py index 4843592a7..b3888054b 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py @@ -33,10 +33,6 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import \ - EpFabrics -from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ - ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ ResponseHandler from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ @@ -80,20 +76,6 @@ def test_fabric_details_by_name_v2_00000(monkeypatch) -> None: - FabricDetailsByName().refresh() raises ``ValueError``. - Error message matches expectation. """ - # method_name = inspect.stack()[0][3] - # key = f"{method_name}a" - - # def responses(): - # yield {} - - # sender = Sender() - # sender.gen = ResponseGenerator(responses()) - # rest_send = RestSend(PARAMS) - # rest_send.response_handler = ResponseHandler() - # rest_send.sender = sender - # rest_send.unit_test = True - # rest_send.timeout = 1 - match = r"FabricDetailsByName\.__init__:\s+" match += r"Failed in super\(\)\.__init__\(\)\.\s+" match += r"Error detail: FabricDetailsByName\.__init__:\s+" @@ -107,7 +89,7 @@ def test_fabric_details_by_name_v2_00000(monkeypatch) -> None: def test_fabric_details_by_name_v2_00200(fabric_details_by_name_v2) -> None: """ ### Classes and Methods - - FabricDetails() + - FabricDetailsByName() - __init__() - refresh_super() @@ -125,7 +107,7 @@ def test_fabric_details_by_name_v2_00200(fabric_details_by_name_v2) -> None: - RETURN_CODE == 200 - DATA == [] - ###Code Flow - Test + ### Code Flow - Test - FabricDetails().refresh_super() is called. - All properties are accessed and verified. @@ -192,7 +174,7 @@ def responses(): def test_fabric_details_by_name_v2_00300(fabric_details_by_name_v2) -> None: """ ### Classes and Methods - - FabricDetails() + - FabricDetailsByName() - __init__() - refresh() @@ -248,3 +230,42 @@ def responses(): assert instance.is_read_only is None assert instance.replication_mode is None assert instance.template_name is None + + +def test_fabric_details_by_name_v2_00400(fabric_details_by_name_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByName() + - __init__() + - refresh() + + ### Summary + - Verify refresh() raises ``ValueError`` if + ``FabricDetails().refresh_super()`` raises ``ValueError``. + - RETURN_CODE is 200. + - Controller response contains one fabric (f1). + + ### Setup - Code + - FabricDetailsByName() is instantiated + - FabricDetailsByName().RestSend() is instantiated + - FabricDetailsByName().Results() is NOT instantiated. + + ### Setup - Data + - None + + ### Expected Result + - ``ValueException`` is raised by ``refresh_super()`` and caught by + ``refresh()``. + """ + with does_not_raise(): + instance = fabric_details_by_name_v2 + instance.rest_send = RestSend(PARAMS) + instance.filter = "f1" + + match = r"Failed to refresh fabric details:\s+" + match += r"Error detail:\s+" + match += r"FabricDetailsByName\.validate_refresh_parameters:\s+" + match += r"FabricDetailsByName\.results must be set before calling\s+" + match += r"FabricDetailsByName\.refresh\(\)\..*" + with pytest.raises(ValueError, match=match): + instance.refresh() From 6ad4605cf30ad281c2d93a40412425f441103b27 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 11 Jun 2024 09:21:39 -0700 Subject: [PATCH 148/230] FabricDetailsByName: 81% unit test coverage 1. Added test cases: - test_fabric_details_by_name_v2_00500a Verify ``_get_nv_pair()`` raises ``ValueError`` if ``filter`` is not set prior to accessing a property. 2. Updated docstrings for other test cases for accuracy. --- .../responses_FabricDetailsByName_V2.json | 20 ++++- .../test_fabric_details_by_name_v2.py | 76 +++++++++++++++---- 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json index 8f3fec4fc..d3fefb23d 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json @@ -322,9 +322,10 @@ }, "test_fabric_details_by_name_v2_00300a": { "TEST_NOTES": [ - "Verify asn property exception.", + "Verify properties missing in the controller response return None.", "DATA contains one fabric dict.", "DATA[0].nvPairs.FABRIC_NAME == f1", + "DATA[0].nvPairs contains no other items.", "RETURN_CODE == 200." ], "DATA": [ @@ -338,5 +339,22 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", "RETURN_CODE": 200 + }, + "test_fabric_details_by_name_v2_00500a": { + "TEST_NOTES": [ + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "f1" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 } } diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py index b3888054b..af8ed1051 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py @@ -57,23 +57,22 @@ def test_fabric_details_by_name_v2_00000(monkeypatch) -> None: - __init__() ### Summary - - Verify that refresh() raises ``ValueError`` if ``refresh_super()`` + - Verify that __init__ raises ``ValueError`` if ``super().__init__`` raises ``ValueError`` ### Setup - Code - - FabricDetails().refresh_supper() is mocked to raise ``ValueError``. - - FabricDetailsByName() is instantiated - - FabricDetailsByName().RestSend() is instantiated - - FabricDetailsByName().Results() is instantiated + - None ### Setup - Data - - None + - params is modified to remove ``check_mode``. ### Trigger - - FabricDetailsByName().refresh() is called + - FabricDetailsByName() is instantiated. ### Expected Result - - FabricDetailsByName().refresh() raises ``ValueError``. + - FabricDetailsByName().__init__() raises ``ValueError`` because + FabricDetails().__init__() raises ``ValueError`` because params + is missing mandatory key ``check_mode``. - Error message matches expectation. """ match = r"FabricDetailsByName\.__init__:\s+" @@ -179,10 +178,7 @@ def test_fabric_details_by_name_v2_00300(fabric_details_by_name_v2) -> None: - refresh() ### Summary - - Verify properties return None if property is missing in the - controller response. - - RETURN_CODE is 200. - - Controller response contains one fabric (f1). + - Verify properties missing in the controller response return ``None``. ### Setup - Code - FabricDetailsByName() is instantiated @@ -196,9 +192,11 @@ def test_fabric_details_by_name_v2_00300(fabric_details_by_name_v2) -> None: - DATA[0].nvPairs.FABRIC_NAME == "f1" - DATA[0].nvPairs + ### Trigger + - All supported properties are accessed and verified. + ### Expected Result - - ``ValueError`` is raised for each property. - - Results() are updated. + - All supported properties return ``None``. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -269,3 +267,53 @@ def test_fabric_details_by_name_v2_00400(fabric_details_by_name_v2) -> None: match += r"FabricDetailsByName\.refresh\(\)\..*" with pytest.raises(ValueError, match=match): instance.refresh() + + +def test_fabric_details_by_name_v2_00500(fabric_details_by_name_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByName() + - __init__() + - refresh() + - _get_nv_pair() + - bgp_as.getter + + ### Summary + - Verify ``_get_nv_pair()`` raises ``ValueError`` if ``filter`` is not + set prior to accessing a property. + + ### Setup - Code + - Sender() is instantiated and configured. + - RestSend() is instantiated and configured. + - Results() is instantiated. + - FabricDetailsByName() is instantiated and configured. + - FabricDetailsByName().refresh() is called. + + ### Setup - Data + - responses() yields a 200 response. + + ### Trigger + ``bgp_as`` is accessed before setting ``filter``. + + ### Expected Result + - ``_get_nv_pair()`` raises ``ValueError``. + - ``bgp_as.getter`` catches ``ValueError`` and returns ``None``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_by_name_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + with does_not_raise(): + instance = fabric_details_by_name_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + bgp_as = instance.bgp_as + assert bgp_as is None From 33aa10582cfa9d1a30b881e856119ecdca583fb7 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 11 Jun 2024 09:38:23 -0700 Subject: [PATCH 149/230] Update sanity/ignore-2.[15,16].txt Added to both: plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module --- tests/sanity/ignore-2.15.txt | 1 + tests/sanity/ignore-2.16.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index 1e315bd7d..15705d33b 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -16,6 +16,7 @@ plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GP plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.7!skip plugins/httpapi/dcnm.py import-3.9!skip plugins/httpapi/dcnm.py import-3.10!skip diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index 64c0f2d2c..20cfc7582 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -16,3 +16,4 @@ plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GP plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module From 403ab55b9b12a2d6692a0d43c5ff9692ac05b8db Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 11 Jun 2024 11:04:49 -0700 Subject: [PATCH 150/230] fabric_details_v2.py: 84% unit test coverage 1. Added the following test cases: - test_fabric_details_by_name_v2_00510a Verify that property getters for ``nvPairs`` items return ``None`` when ``_get_nv_pair()`` raises ``ValueError`` because fabric does not exist. - test_fabric_details_by_name_v2_00600 Verify that ``filtered_data`` property getter raises ``ValueError`` when ``filter`` is not set. - test_fabric_details_by_name_v2_00610 Verify that ``filtered_data`` property returns expected values when ``filter`` is set and matches a fabric on the controller. 2. FabricDetailsByName().filtered_data.getter: Modify error message. --- .../module_utils/fabric/fabric_details_v2.py | 4 +- .../responses_FabricDetailsByName_V2.json | 54 ++++++ .../test_fabric_details_by_name_v2.py | 158 +++++++++++++++++- 3 files changed, 212 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index a66eea634..6351b4354 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -642,8 +642,8 @@ def filtered_data(self): method_name = inspect.stack()[0][3] if self.filter is None: msg = f"{self.class_name}.{method_name}: " - msg += f"{self.class_name}.filter must be set before calling " - msg += f"{self.class_name}.filtered_data" + msg += f"{self.class_name}.filter must be set before accessing " + msg += f"{self.class_name}.filtered_data." raise ValueError(msg) return self.data_subclass.get(self.filter, None) diff --git a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json index d3fefb23d..d08a350a7 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json @@ -356,5 +356,59 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", "RETURN_CODE": 200 + }, + "test_fabric_details_by_name_v2_00510a": { + "TEST_NOTES": [ + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "WRONG_FABRIC" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_by_name_v2_00600a": { + "TEST_NOTES": [ + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "SOME_FABRIC" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_by_name_v2_00610a": { + "TEST_NOTES": [ + "FABRIC_NAME matches filter.", + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "BGP_AS": "65001", + "FABRIC_NAME": "MATCHING_FABRIC", + "ENABLE_NETFLOW": "false" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 } } diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py index af8ed1051..d63b35d50 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py @@ -279,8 +279,9 @@ def test_fabric_details_by_name_v2_00500(fabric_details_by_name_v2) -> None: - bgp_as.getter ### Summary - - Verify ``_get_nv_pair()`` raises ``ValueError`` if ``filter`` is not - set prior to accessing a property. + - Verify that property getters for ``nvPairs`` items return ``None`` + when ``_get_nv_pair()`` raises ``ValueError`` because ``filter`` + is not set prior to accessing a property. ### Setup - Code - Sender() is instantiated and configured. @@ -317,3 +318,156 @@ def responses(): instance.refresh() bgp_as = instance.bgp_as assert bgp_as is None + + +def test_fabric_details_by_name_v2_00510(fabric_details_by_name_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByName() + - __init__() + - refresh() + - _get_nv_pair() + - bgp_as.getter + + ### Summary + - Verify that property getters for ``nvPairs`` items return ``None`` + when ``_get_nv_pair()`` raises ``ValueError`` because fabric + does not exist. + + ### Setup - Code + - Sender() is instantiated and configured. + - RestSend() is instantiated and configured. + - Results() is instantiated. + - FabricDetailsByName() is instantiated and configured. + - FabricDetailsByName().refresh() is called. + + ### Setup - Data + - responses() yields a 200 response that does not contain any fabrics. + + ### Trigger + ``bgp_as`` is accessed. + + ### Expected Result + - ``_get_nv_pair()`` raises ``ValueError``. + - ``bgp_as.getter`` catches ``ValueError`` and returns ``None``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_by_name_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + with does_not_raise(): + instance = fabric_details_by_name_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "FABRIC_DOES_NOT_EXIST" + bgp_as = instance.bgp_as + assert bgp_as is None + + +def test_fabric_details_by_name_v2_00600(fabric_details_by_name_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByName() + - __init__() + - refresh() + - filtered_data.getter + + ### Summary + - Verify that ``filtered_data`` property getter raises ``ValueError`` + when ``filter`` is not set. + + ### Setup - Code + - Sender() is instantiated and configured. + - RestSend() is instantiated and configured. + - Results() is instantiated. + - FabricDetailsByName() is instantiated and configured. + - FabricDetailsByName().refresh() is called. + + ### Setup - Data + - responses() yields a 200 response. + + ### Trigger + ``filtered_data.getter`` is accessed. + + ### Expected Result + - ``filtered_data.getter`` raises ``ValueError``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_by_name_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + with does_not_raise(): + instance = fabric_details_by_name_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + match = r"FabricDetailsByName\.filtered_data:\s+" + match += r"FabricDetailsByName\.filter must be set\s+" + match += r"before accessing FabricDetailsByName\.filtered_data\." + with pytest.raises(ValueError, match=match): + instance.filtered_data # pylint: disable=pointless-statement + + +def test_fabric_details_by_name_v2_00610(fabric_details_by_name_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByName() + - __init__() + - refresh() + - filtered_data.getter + + ### Summary + - Verify that ``filtered_data`` property returns expected values + when ``filter`` is set and matches a fabric on the controller. + + ### Setup - Code + - Sender() is instantiated and configured. + - RestSend() is instantiated and configured. + - Results() is instantiated. + - FabricDetailsByName() is instantiated and configured. + - FabricDetailsByName().refresh() is called. + + ### Setup - Data + - responses() yields a 200 response with a matching fabric. + + ### Trigger + ``filtered_data.getter`` is accessed. + + ### Expected Result + - ``filtered_data.getter`` returns expected value. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_by_name_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + with does_not_raise(): + instance = fabric_details_by_name_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "MATCHING_FABRIC" + data = instance.filtered_data + assert data.get("nvPairs", {}).get("BGP_AS") == "65001" + assert data.get("nvPairs", {}).get("ENABLE_NETFLOW") == "false" From 6a40128975498230aa35fa36cb574a0542b6195f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 11 Jun 2024 11:48:32 -0700 Subject: [PATCH 151/230] fabric_details_v2.py: 87% unit test coverage 1. Add the following test cases test_fabric_details_by_name_v2_00700a Verify that property getters for top-level items return ``None`` when ``_get()`` raises ``ValueError`` because ``filter`` is not set prior to accessing a property. test_fabric_details_by_name_v2_00710 Verify that property getters for top-level items return ``None`` when ``_get()`` raises ``ValueError`` because fabric does not exist. --- .../responses_FabricDetailsByName_V2.json | 34 ++++++ .../test_fabric_details_by_name_v2.py | 103 ++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json index d08a350a7..ed53309b9 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json @@ -410,5 +410,39 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", "RETURN_CODE": 200 + }, + "test_fabric_details_by_name_v2_00700a": { + "TEST_NOTES": [ + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "f1" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_by_name_v2_00710a": { + "TEST_NOTES": [ + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "WRONG_FABRIC" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 } } diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py index d63b35d50..fccf1c0e2 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py @@ -471,3 +471,106 @@ def responses(): data = instance.filtered_data assert data.get("nvPairs", {}).get("BGP_AS") == "65001" assert data.get("nvPairs", {}).get("ENABLE_NETFLOW") == "false" + + +def test_fabric_details_by_name_v2_00700(fabric_details_by_name_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByName() + - __init__() + - refresh() + - _get() + - template_name.getter + + ### Summary + - Verify that property getters for top-level items return ``None`` + when ``_get()`` raises ``ValueError`` because ``filter`` + is not set prior to accessing a property. + + ### Setup - Code + - Sender() is instantiated and configured. + - RestSend() is instantiated and configured. + - Results() is instantiated. + - FabricDetailsByName() is instantiated and configured. + - FabricDetailsByName().refresh() is called. + + ### Setup - Data + - responses() yields a 200 response. + + ### Trigger + ``template_name`` is accessed before setting ``filter``. + + ### Expected Result + - ``_get()`` raises ``ValueError``. + - ``template_name.getter`` catches ``ValueError`` and returns ``None``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_by_name_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + with does_not_raise(): + instance = fabric_details_by_name_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + template_name = instance.template_name + assert template_name is None + + +def test_fabric_details_by_name_v2_00710(fabric_details_by_name_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByName() + - __init__() + - refresh() + - _get() + - template_name.getter + + ### Summary + - Verify that property getters for top-level items return ``None`` + when ``_get()`` raises ``ValueError`` because fabric + does not exist. + + ### Setup - Code + - Sender() is instantiated and configured. + - RestSend() is instantiated and configured. + - Results() is instantiated. + - FabricDetailsByName() is instantiated and configured. + - FabricDetailsByName().refresh() is called. + + ### Setup - Data + - responses() yields a 200 response that does not contain any fabrics. + + ### Trigger + ``template_name.getter`` is accessed. + + ### Expected Result + - ``_get()`` raises ``ValueError``. + - ``template_name.getter`` catches ``ValueError`` and returns ``None``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_by_name_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + with does_not_raise(): + instance = fabric_details_by_name_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "FABRIC_DOES_NOT_EXIST" + template_name = instance.template_name + assert template_name is None From 22459f5b6d7f31db6e8e92fd6bccec63f31657c5 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 11 Jun 2024 14:02:13 -0700 Subject: [PATCH 152/230] fabric_details_v2.py: 95% unit test coverage 1. FabricDetailsByNvPair(): fix docstring to move refresh() to the right place (must be called AFTER setting filter_key and filter_value. 2. Add the following test cases: - test_fabric_details_by_nv_pair_v2_00000 Verify that __init__ raises ``ValueError`` if ``super().__init__`` raises ``ValueError`` - test_fabric_details_by_nv_pair_v2_00200 Verify nvPair access after 200 controller response. --- .../module_utils/fabric/fabric_details_v2.py | 2 +- .../responses_FabricDetailsByNvPair_V2.json | 166 +++++ .../test_fabric_details_by_nv_pair_v2.py | 579 ++++++++++++++++++ tests/unit/modules/dcnm/dcnm_fabric/utils.py | 24 +- 4 files changed, 769 insertions(+), 2 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByNvPair_V2.json create mode 100644 tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index 6351b4354..79f3f2c9f 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -699,9 +699,9 @@ class FabricDetailsByNvPair(FabricDetails): rest_send.response_handler = ResponseHandler() instance = FabricDetailsNvPair(params) - instance.refresh() instance.filter_key = "DCI_SUBNET_RANGE" instance.filter_value = "10.33.0.0/16" + instance.refresh() fabrics = instance.filtered_data ``` """ diff --git a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByNvPair_V2.json b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByNvPair_V2.json new file mode 100644 index 000000000..76683a3e4 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByNvPair_V2.json @@ -0,0 +1,166 @@ +{ + "test_notes": [ + "Mocked responses for FabricDetails() class" + ], + "test_fabric_details_by_nv_pair_v2_00200a": { + "TEST_NOTES": [ + "Verify matching fabrics are returned.", + "DATA contains 3x fabric dict.", + "2x fabrics match on filter_key/value FEATURE_PTP.", + "1x fabrics do not match on filter_key/value FEATURE_PTP.", + "RETURN_CODE == 200." + ], + "DATA": [ + { + "nvPairs": { + "BGP_AS": "65001", + "FABRIC_NAME": "f1", + "FEATURE_PTP": "false" + } + }, + { + "nvPairs": { + "BGP_AS": "65002", + "FABRIC_NAME": "f2", + "FEATURE_PTP": "false" + } + }, + { + "nvPairs": { + "BGP_AS": "65003", + "FABRIC_NAME": "f3", + "FEATURE_PTP": "true" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_by_nv_pair_v2_00300a": { + "TEST_NOTES": [ + "Verify properties missing in the controller response return None.", + "DATA contains one fabric dict.", + "DATA[0].nvPairs.FABRIC_NAME == f1", + "DATA[0].nvPairs contains no other items.", + "RETURN_CODE == 200." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "f1" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_by_nv_pair_v2_00500a": { + "TEST_NOTES": [ + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "f1" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_by_nv_pair_v2_00510a": { + "TEST_NOTES": [ + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "WRONG_FABRIC" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_by_nv_pair_v2_00600a": { + "TEST_NOTES": [ + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "SOME_FABRIC" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_by_nv_pair_v2_00610a": { + "TEST_NOTES": [ + "FABRIC_NAME matches filter.", + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "BGP_AS": "65001", + "FABRIC_NAME": "MATCHING_FABRIC", + "ENABLE_NETFLOW": "false" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_by_nv_pair_v2_00700a": { + "TEST_NOTES": [ + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "f1" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_fabric_details_by_nv_pair_v2_00710a": { + "TEST_NOTES": [ + "RETURN_CODE == 200.", + "MESSAGE == OK." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME": "WRONG_FABRIC" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + } +} diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py new file mode 100644 index 000000000..487ff2916 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py @@ -0,0 +1,579 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-import +# pylint: disable=redefined-outer-name +# pylint: disable=protected-access +# pylint: disable=unused-argument +# pylint: disable=invalid-name + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy +import inspect + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ + FabricDetailsByNvPair +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( + ResponseGenerator, does_not_raise, fabric_details_by_nv_pair_v2_fixture, + responses_fabric_details_by_nv_pair_v2) + +PARAMS = {"state": "query", "check_mode": False} + + +def test_fabric_details_by_nv_pair_v2_00000(monkeypatch) -> None: + """ + ### Classes and Methods + - FabricDetailsByNvPair() + - __init__() + + ### Summary + - Verify that __init__ raises ``ValueError`` if ``super().__init__`` + raises ``ValueError`` + + ### Setup - Code + - None + + ### Setup - Data + - params is modified to remove ``check_mode``. + + ### Trigger + - FabricDetailsByNvPair() is instantiated. + + ### Expected Result + - FabricDetailsByNvPair().__init__() raises ``ValueError`` because + FabricDetails().__init__() raises ``ValueError`` because params + is missing mandatory key ``check_mode``. + - Error message matches expectation. + """ + match = r"FabricDetailsByNvPair\.__init__:\s+" + match += r"Failed in super\(\)\.__init__\(\)\.\s+" + match += r"Error detail: FabricDetailsByNvPair\.__init__:\s+" + match += r"check_mode is missing from params\. params:.*" + params = copy.copy(PARAMS) + params.pop("check_mode", None) + with pytest.raises(ValueError, match=match): + FabricDetailsByNvPair(params) # pytest: disable=pointless-statement + + +def test_fabric_details_by_nv_pair_v2_00200(fabric_details_by_nv_pair_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByNvPair() + - __init__() + - refresh_super() + + ### Summary + - Verify nvPair access after 200 controller response. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - FabricDetailsByNvPair() is instantiated and configured. + - FabricDetailsByNvPair().refresh() is called. + + ### Setup - Data + - responses_FabricDetailsByNvPair_V2 contains a response with + - 3x fabrics + - 2x fabrics that match filter_key and filter_value + - 1x fabrics do not match filter_key and filter_value. + - RETURN_CODE == 200 + - DATA == [<3x fabrics>] + + ### Trigger + - FabricDetailsByNvPair().filtered_data is accessed + + ### Expected Result + - Exception is not raised. + - All fabrics matching ``filter_key`` and ``filter_value`` + are returned in ``filtered_data``. + - ``Results()`` are updated. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_by_nv_pair_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = fabric_details_by_nv_pair_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.filter_key = "FEATURE_PTP" + instance.filter_value = "false" + instance.refresh() + + assert isinstance(instance.results.diff, list) + assert isinstance(instance.results.result, list) + assert isinstance(instance.results.response, list) + + assert len(instance.results.diff) == 1 + assert len(instance.results.result) == 1 + assert len(instance.results.response) == 1 + + assert instance.results.diff[0].get("sequence_number", None) == 1 + + assert instance.results.response[0].get("RETURN_CODE", None) == 200 + assert instance.results.result[0].get("found", None) is True + assert instance.results.result[0].get("success", None) is True + + assert False in instance.results.failed + assert True not in instance.results.failed + assert False in instance.results.changed + assert True not in instance.results.changed + + assert ( + instance.filtered_data.get("f1", {}).get("nvPairs", {}).get("FEATURE_PTP", None) + == "false" + ) + assert ( + instance.filtered_data.get("f2", {}).get("nvPairs", {}).get("FEATURE_PTP", None) + == "false" + ) + assert ( + instance.filtered_data.get("f3", {}).get("nvPairs", {}).get("FEATURE_PTP", None) + is None + ) + + +# def test_fabric_details_by_nv_pair_v2_00300(fabric_details_by_nv_pair_v2) -> None: +# """ +# ### Classes and Methods +# - FabricDetailsByNvPair() +# - __init__() +# - refresh() + +# ### Summary +# - Verify missing nvPairs items ``None``. + +# ### Setup - Code +# - FabricDetailsByNvPair() is instantiated +# - FabricDetailsByNvPair().RestSend() is instantiated +# - FabricDetailsByNvPair().Results() is instantiated +# - FabricDetailsByNvPair().refresh() is called + +# ### Setup - Data +# - responses_FabricDetailsByNvPair_V2 contains a dict with: +# - RETURN_CODE == 200 +# - DATA[0].nvPairs.FABRIC_NAME == "f1" +# - DATA[0].nvPairs + +# ### Trigger +# - All supported properties are accessed and verified. + +# ### Expected Result +# - All supported properties return ``None``. +# """ +# method_name = inspect.stack()[0][3] +# key = f"{method_name}a" + +# def responses(): +# yield responses_fabric_details_by_nv_pair_v2(key) + +# sender = Sender() +# sender.gen = ResponseGenerator(responses()) +# rest_send = RestSend(PARAMS) +# rest_send.response_handler = ResponseHandler() +# rest_send.sender = sender +# rest_send.unit_test = True +# rest_send.timeout = 1 + +# with does_not_raise(): +# instance = fabric_details_by_nv_pair_v2 +# instance.rest_send = rest_send +# instance.results = Results() +# instance.filter = "f1" +# instance.refresh() + +# assert instance.asn is None +# assert instance.bgp_as is None +# assert instance.deployment_freeze is None +# assert instance.enable_pbr is None +# assert instance.fabric_id is None +# assert instance.fabric_type is None +# assert instance.is_read_only is None +# assert instance.replication_mode is None +# assert instance.template_name is None + + +# def test_fabric_details_by_nv_pair_v2_00400(fabric_details_by_nv_pair_v2) -> None: +# """ +# ### Classes and Methods +# - FabricDetailsByNvPair() +# - __init__() +# - refresh() + +# ### Summary +# - Verify refresh() raises ``ValueError`` if +# ``FabricDetails().refresh_super()`` raises ``ValueError``. +# - RETURN_CODE is 200. +# - Controller response contains one fabric (f1). + +# ### Setup - Code +# - FabricDetailsByNvPair() is instantiated +# - FabricDetailsByNvPair().RestSend() is instantiated +# - FabricDetailsByNvPair().Results() is NOT instantiated. + +# ### Setup - Data +# - None + +# ### Expected Result +# - ``ValueException`` is raised by ``refresh_super()`` and caught by +# ``refresh()``. +# """ +# with does_not_raise(): +# instance = fabric_details_by_nv_pair_v2 +# instance.rest_send = RestSend(PARAMS) +# instance.filter = "f1" + +# match = r"Failed to refresh fabric details:\s+" +# match += r"Error detail:\s+" +# match += r"FabricDetailsByNvPair\.validate_refresh_parameters:\s+" +# match += r"FabricDetailsByNvPair\.results must be set before calling\s+" +# match += r"FabricDetailsByNvPair\.refresh\(\)\..*" +# with pytest.raises(ValueError, match=match): +# instance.refresh() + + +# def test_fabric_details_by_nv_pair_v2_00500(fabric_details_by_nv_pair_v2) -> None: +# """ +# ### Classes and Methods +# - FabricDetailsByNvPair() +# - __init__() +# - refresh() +# - _get_nv_pair() +# - bgp_as.getter + +# ### Summary +# - Verify that property getters for ``nvPairs`` items return ``None`` +# when ``_get_nv_pair()`` raises ``ValueError`` because ``filter`` +# is not set prior to accessing a property. + +# ### Setup - Code +# - Sender() is instantiated and configured. +# - RestSend() is instantiated and configured. +# - Results() is instantiated. +# - FabricDetailsByNvPair() is instantiated and configured. +# - FabricDetailsByNvPair().refresh() is called. + +# ### Setup - Data +# - responses() yields a 200 response. + +# ### Trigger +# ``bgp_as`` is accessed before setting ``filter``. + +# ### Expected Result +# - ``_get_nv_pair()`` raises ``ValueError``. +# - ``bgp_as.getter`` catches ``ValueError`` and returns ``None``. +# """ +# method_name = inspect.stack()[0][3] +# key = f"{method_name}a" + +# def responses(): +# yield responses_fabric_details_by_nv_pair_v2(key) + +# sender = Sender() +# sender.gen = ResponseGenerator(responses()) +# rest_send = RestSend(PARAMS) +# rest_send.sender = sender +# rest_send.response_handler = ResponseHandler() +# with does_not_raise(): +# instance = fabric_details_by_nv_pair_v2 +# instance.rest_send = rest_send +# instance.results = Results() +# instance.refresh() +# bgp_as = instance.bgp_as +# assert bgp_as is None + + +# def test_fabric_details_by_nv_pair_v2_00510(fabric_details_by_nv_pair_v2) -> None: +# """ +# ### Classes and Methods +# - FabricDetailsByNvPair() +# - __init__() +# - refresh() +# - _get_nv_pair() +# - bgp_as.getter + +# ### Summary +# - Verify that property getters for ``nvPairs`` items return ``None`` +# when ``_get_nv_pair()`` raises ``ValueError`` because fabric +# does not exist. + +# ### Setup - Code +# - Sender() is instantiated and configured. +# - RestSend() is instantiated and configured. +# - Results() is instantiated. +# - FabricDetailsByNvPair() is instantiated and configured. +# - FabricDetailsByNvPair().refresh() is called. + +# ### Setup - Data +# - responses() yields a 200 response that does not contain any fabrics. + +# ### Trigger +# ``bgp_as`` is accessed. + +# ### Expected Result +# - ``_get_nv_pair()`` raises ``ValueError``. +# - ``bgp_as.getter`` catches ``ValueError`` and returns ``None``. +# """ +# method_name = inspect.stack()[0][3] +# key = f"{method_name}a" + +# def responses(): +# yield responses_fabric_details_by_nv_pair_v2(key) + +# sender = Sender() +# sender.gen = ResponseGenerator(responses()) +# rest_send = RestSend(PARAMS) +# rest_send.sender = sender +# rest_send.response_handler = ResponseHandler() +# with does_not_raise(): +# instance = fabric_details_by_nv_pair_v2 +# instance.rest_send = rest_send +# instance.results = Results() +# instance.refresh() +# instance.filter = "FABRIC_DOES_NOT_EXIST" +# bgp_as = instance.bgp_as +# assert bgp_as is None + + +# def test_fabric_details_by_nv_pair_v2_00600(fabric_details_by_nv_pair_v2) -> None: +# """ +# ### Classes and Methods +# - FabricDetailsByNvPair() +# - __init__() +# - refresh() +# - filtered_data.getter + +# ### Summary +# - Verify that ``filtered_data`` property getter raises ``ValueError`` +# when ``filter`` is not set. + +# ### Setup - Code +# - Sender() is instantiated and configured. +# - RestSend() is instantiated and configured. +# - Results() is instantiated. +# - FabricDetailsByNvPair() is instantiated and configured. +# - FabricDetailsByNvPair().refresh() is called. + +# ### Setup - Data +# - responses() yields a 200 response. + +# ### Trigger +# ``filtered_data.getter`` is accessed. + +# ### Expected Result +# - ``filtered_data.getter`` raises ``ValueError``. +# """ +# method_name = inspect.stack()[0][3] +# key = f"{method_name}a" + +# def responses(): +# yield responses_fabric_details_by_nv_pair_v2(key) + +# sender = Sender() +# sender.gen = ResponseGenerator(responses()) +# rest_send = RestSend(PARAMS) +# rest_send.sender = sender +# rest_send.response_handler = ResponseHandler() +# with does_not_raise(): +# instance = fabric_details_by_nv_pair_v2 +# instance.rest_send = rest_send +# instance.results = Results() +# instance.refresh() +# match = r"FabricDetailsByNvPair\.filtered_data:\s+" +# match += r"FabricDetailsByNvPair\.filter must be set\s+" +# match += r"before accessing FabricDetailsByNvPair\.filtered_data\." +# with pytest.raises(ValueError, match=match): +# instance.filtered_data # pylint: disable=pointless-statement + + +# def test_fabric_details_by_nv_pair_v2_00610(fabric_details_by_nv_pair_v2) -> None: +# """ +# ### Classes and Methods +# - FabricDetailsByNvPair() +# - __init__() +# - refresh() +# - filtered_data.getter + +# ### Summary +# - Verify that ``filtered_data`` property returns expected values +# when ``filter`` is set and matches a fabric on the controller. + +# ### Setup - Code +# - Sender() is instantiated and configured. +# - RestSend() is instantiated and configured. +# - Results() is instantiated. +# - FabricDetailsByNvPair() is instantiated and configured. +# - FabricDetailsByNvPair().refresh() is called. + +# ### Setup - Data +# - responses() yields a 200 response with a matching fabric. + +# ### Trigger +# ``filtered_data.getter`` is accessed. + +# ### Expected Result +# - ``filtered_data.getter`` returns expected value. +# """ +# method_name = inspect.stack()[0][3] +# key = f"{method_name}a" + +# def responses(): +# yield responses_fabric_details_by_nv_pair_v2(key) + +# sender = Sender() +# sender.gen = ResponseGenerator(responses()) +# rest_send = RestSend(PARAMS) +# rest_send.sender = sender +# rest_send.response_handler = ResponseHandler() +# with does_not_raise(): +# instance = fabric_details_by_nv_pair_v2 +# instance.rest_send = rest_send +# instance.results = Results() +# instance.refresh() +# instance.filter = "MATCHING_FABRIC" +# data = instance.filtered_data +# assert data.get("nvPairs", {}).get("BGP_AS") == "65001" +# assert data.get("nvPairs", {}).get("ENABLE_NETFLOW") == "false" + + +# def test_fabric_details_by_nv_pair_v2_00700(fabric_details_by_nv_pair_v2) -> None: +# """ +# ### Classes and Methods +# - FabricDetailsByNvPair() +# - __init__() +# - refresh() +# - _get() +# - template_name.getter + +# ### Summary +# - Verify that property getters for top-level items return ``None`` +# when ``_get()`` raises ``ValueError`` because ``filter`` +# is not set prior to accessing a property. + +# ### Setup - Code +# - Sender() is instantiated and configured. +# - RestSend() is instantiated and configured. +# - Results() is instantiated. +# - FabricDetailsByNvPair() is instantiated and configured. +# - FabricDetailsByNvPair().refresh() is called. + +# ### Setup - Data +# - responses() yields a 200 response. + +# ### Trigger +# ``template_name`` is accessed before setting ``filter``. + +# ### Expected Result +# - ``_get()`` raises ``ValueError``. +# - ``template_name.getter`` catches ``ValueError`` and returns ``None``. +# """ +# method_name = inspect.stack()[0][3] +# key = f"{method_name}a" + +# def responses(): +# yield responses_fabric_details_by_nv_pair_v2(key) + +# sender = Sender() +# sender.gen = ResponseGenerator(responses()) +# rest_send = RestSend(PARAMS) +# rest_send.sender = sender +# rest_send.response_handler = ResponseHandler() +# with does_not_raise(): +# instance = fabric_details_by_nv_pair_v2 +# instance.rest_send = rest_send +# instance.results = Results() +# instance.refresh() +# template_name = instance.template_name +# assert template_name is None + + +# def test_fabric_details_by_nv_pair_v2_00710(fabric_details_by_nv_pair_v2) -> None: +# """ +# ### Classes and Methods +# - FabricDetailsByNvPair() +# - __init__() +# - refresh() +# - _get() +# - template_name.getter + +# ### Summary +# - Verify that property getters for top-level items return ``None`` +# when ``_get()`` raises ``ValueError`` because fabric +# does not exist. + +# ### Setup - Code +# - Sender() is instantiated and configured. +# - RestSend() is instantiated and configured. +# - Results() is instantiated. +# - FabricDetailsByNvPair() is instantiated and configured. +# - FabricDetailsByNvPair().refresh() is called. + +# ### Setup - Data +# - responses() yields a 200 response that does not contain any fabrics. + +# ### Trigger +# ``template_name.getter`` is accessed. + +# ### Expected Result +# - ``_get()`` raises ``ValueError``. +# - ``template_name.getter`` catches ``ValueError`` and returns ``None``. +# """ +# method_name = inspect.stack()[0][3] +# key = f"{method_name}a" + +# def responses(): +# yield responses_fabric_details_by_nv_pair_v2(key) + +# sender = Sender() +# sender.gen = ResponseGenerator(responses()) +# rest_send = RestSend(PARAMS) +# rest_send.sender = sender +# rest_send.response_handler = ResponseHandler() +# with does_not_raise(): +# instance = fabric_details_by_nv_pair_v2 +# instance.rest_send = rest_send +# instance.results = Results() +# instance.refresh() +# instance.filter = "FABRIC_DOES_NOT_EXIST" +# template_name = instance.template_name +# assert template_name is None diff --git a/tests/unit/modules/dcnm/dcnm_fabric/utils.py b/tests/unit/modules/dcnm/dcnm_fabric/utils.py index 5178234cc..4abd24cad 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/utils.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/utils.py @@ -41,6 +41,8 @@ FabricDetails as FabricDetailsV2 from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ FabricDetailsByName as FabricDetailsByNameV2 +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ + FabricDetailsByNvPair as FabricDetailsByNvPairV2 from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_summary import \ FabricSummary from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_types import \ @@ -270,6 +272,16 @@ def fabric_details_by_nv_pair_fixture(): return FabricDetailsByNvPair(instance.params) +@pytest.fixture(name="fabric_details_by_nv_pair_v2") +def fabric_details_by_nv_pair_v2_fixture(): + """ + mock FabricDetailsByNvPair version 2 + """ + instance = MockAnsibleModule() + instance.state = "merged" + return FabricDetailsByNvPairV2(instance.params) + + @pytest.fixture(name="fabric_query") def fabric_query_fixture(): """ @@ -542,7 +554,7 @@ def responses_fabric_details_by_name(key: str) -> Dict[str, str]: def responses_fabric_details_by_name_v2(key: str) -> Dict[str, str]: """ - Return responses for FabricDetailsByName + Return responses for FabricDetailsByName version 2 """ data_file = "responses_FabricDetailsByName_V2" data = load_fixture(data_file).get(key) @@ -560,6 +572,16 @@ def responses_fabric_details_by_nv_pair(key: str) -> Dict[str, str]: return data +def responses_fabric_details_by_nv_pair_v2(key: str) -> Dict[str, str]: + """ + Return responses for FabricDetailsByNvPair version 2 + """ + data_file = "responses_FabricDetailsByNvPair_V2" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + def responses_fabric_query(key: str) -> Dict[str, str]: """ Return responses for FabricQuery From 8e66375e4c194cea81bb8fb1598b1fe1082219a2 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 11 Jun 2024 15:25:56 -0700 Subject: [PATCH 153/230] fabric_details_v2.py: 100% unit test coverage 1. FabricDetailsByNvPair(): If no fabrics exist (len self.data == 0), set results before returning. 2. Add the following test cases. - test_fabric_details_by_nv_pair_v2_00210a Verify behavior when FABRIC_NAME is missing from nvPairs. (negative test case) - test_fabric_details_by_nv_pair_v2_00400 Verify refresh() raises ``ValueError`` if ``FabricDetails().refresh_super()`` raises. - test_fabric_details_by_nv_pair_v2_00600 Verify that ``refresh()`` raises ``ValueError`` when ``filter_key`` is not set. test_fabric_details_by_nv_pair_v2_00610 Verify that ``refresh()`` raises ``ValueError`` when ``filter_value`` is not set. --- .../module_utils/fabric/fabric_details_v2.py | 9 +- .../responses_FabricDetailsByNvPair_V2.json | 20 + .../test_fabric_details_by_nv_pair_v2.py | 608 ++++++------------ 3 files changed, 232 insertions(+), 405 deletions(-) diff --git a/plugins/module_utils/fabric/fabric_details_v2.py b/plugins/module_utils/fabric/fabric_details_v2.py index 79f3f2c9f..04d596a43 100644 --- a/plugins/module_utils/fabric/fabric_details_v2.py +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -752,9 +752,16 @@ def refresh(self): self.refresh_super() except ValueError as error: msg = "Failed to refresh fabric details: " - msg += f"Error detail: {error}." + msg += f"Error detail: {error}" raise ValueError(msg) from error + if len(self.data) == 0: + self.results.diff = {} + self.results.response = self.rest_send.response_current + self.results.result = self.rest_send.result_current + self.results.failed = True + self.results.changed = False + return for item, value in self.data.items(): if value.get("nvPairs", {}).get(self.filter_key) == self.filter_value: self.data_subclass[item] = value diff --git a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByNvPair_V2.json b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByNvPair_V2.json index 76683a3e4..14bd8e3fd 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByNvPair_V2.json +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByNvPair_V2.json @@ -38,6 +38,26 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", "RETURN_CODE": 200 }, + "test_fabric_details_by_nv_pair_v2_00210a": { + "TEST_NOTES": [ + "Negative test case.", + "Verify behavior when FABRIC_NAME is missing from nvPairs.", + "DATA[0] contains one fabric dict.", + "DATA[0].nvPairs.FABRIC_NAME is missing", + "RETURN_CODE == 200." + ], + "DATA": [ + { + "nvPairs": { + "FABRIC_NAME_MISSING": "NOT_A_FABRIC_NAME" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/rest/control/fabrics", + "RETURN_CODE": 200 + }, "test_fabric_details_by_nv_pair_v2_00300a": { "TEST_NOTES": [ "Verify properties missing in the controller response return None.", diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py index 487ff2916..ae9c6efd9 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py @@ -173,407 +173,207 @@ def responses(): ) -# def test_fabric_details_by_nv_pair_v2_00300(fabric_details_by_nv_pair_v2) -> None: -# """ -# ### Classes and Methods -# - FabricDetailsByNvPair() -# - __init__() -# - refresh() - -# ### Summary -# - Verify missing nvPairs items ``None``. - -# ### Setup - Code -# - FabricDetailsByNvPair() is instantiated -# - FabricDetailsByNvPair().RestSend() is instantiated -# - FabricDetailsByNvPair().Results() is instantiated -# - FabricDetailsByNvPair().refresh() is called - -# ### Setup - Data -# - responses_FabricDetailsByNvPair_V2 contains a dict with: -# - RETURN_CODE == 200 -# - DATA[0].nvPairs.FABRIC_NAME == "f1" -# - DATA[0].nvPairs - -# ### Trigger -# - All supported properties are accessed and verified. - -# ### Expected Result -# - All supported properties return ``None``. -# """ -# method_name = inspect.stack()[0][3] -# key = f"{method_name}a" - -# def responses(): -# yield responses_fabric_details_by_nv_pair_v2(key) - -# sender = Sender() -# sender.gen = ResponseGenerator(responses()) -# rest_send = RestSend(PARAMS) -# rest_send.response_handler = ResponseHandler() -# rest_send.sender = sender -# rest_send.unit_test = True -# rest_send.timeout = 1 - -# with does_not_raise(): -# instance = fabric_details_by_nv_pair_v2 -# instance.rest_send = rest_send -# instance.results = Results() -# instance.filter = "f1" -# instance.refresh() - -# assert instance.asn is None -# assert instance.bgp_as is None -# assert instance.deployment_freeze is None -# assert instance.enable_pbr is None -# assert instance.fabric_id is None -# assert instance.fabric_type is None -# assert instance.is_read_only is None -# assert instance.replication_mode is None -# assert instance.template_name is None - - -# def test_fabric_details_by_nv_pair_v2_00400(fabric_details_by_nv_pair_v2) -> None: -# """ -# ### Classes and Methods -# - FabricDetailsByNvPair() -# - __init__() -# - refresh() - -# ### Summary -# - Verify refresh() raises ``ValueError`` if -# ``FabricDetails().refresh_super()`` raises ``ValueError``. -# - RETURN_CODE is 200. -# - Controller response contains one fabric (f1). - -# ### Setup - Code -# - FabricDetailsByNvPair() is instantiated -# - FabricDetailsByNvPair().RestSend() is instantiated -# - FabricDetailsByNvPair().Results() is NOT instantiated. - -# ### Setup - Data -# - None - -# ### Expected Result -# - ``ValueException`` is raised by ``refresh_super()`` and caught by -# ``refresh()``. -# """ -# with does_not_raise(): -# instance = fabric_details_by_nv_pair_v2 -# instance.rest_send = RestSend(PARAMS) -# instance.filter = "f1" - -# match = r"Failed to refresh fabric details:\s+" -# match += r"Error detail:\s+" -# match += r"FabricDetailsByNvPair\.validate_refresh_parameters:\s+" -# match += r"FabricDetailsByNvPair\.results must be set before calling\s+" -# match += r"FabricDetailsByNvPair\.refresh\(\)\..*" -# with pytest.raises(ValueError, match=match): -# instance.refresh() - - -# def test_fabric_details_by_nv_pair_v2_00500(fabric_details_by_nv_pair_v2) -> None: -# """ -# ### Classes and Methods -# - FabricDetailsByNvPair() -# - __init__() -# - refresh() -# - _get_nv_pair() -# - bgp_as.getter - -# ### Summary -# - Verify that property getters for ``nvPairs`` items return ``None`` -# when ``_get_nv_pair()`` raises ``ValueError`` because ``filter`` -# is not set prior to accessing a property. - -# ### Setup - Code -# - Sender() is instantiated and configured. -# - RestSend() is instantiated and configured. -# - Results() is instantiated. -# - FabricDetailsByNvPair() is instantiated and configured. -# - FabricDetailsByNvPair().refresh() is called. - -# ### Setup - Data -# - responses() yields a 200 response. - -# ### Trigger -# ``bgp_as`` is accessed before setting ``filter``. - -# ### Expected Result -# - ``_get_nv_pair()`` raises ``ValueError``. -# - ``bgp_as.getter`` catches ``ValueError`` and returns ``None``. -# """ -# method_name = inspect.stack()[0][3] -# key = f"{method_name}a" - -# def responses(): -# yield responses_fabric_details_by_nv_pair_v2(key) - -# sender = Sender() -# sender.gen = ResponseGenerator(responses()) -# rest_send = RestSend(PARAMS) -# rest_send.sender = sender -# rest_send.response_handler = ResponseHandler() -# with does_not_raise(): -# instance = fabric_details_by_nv_pair_v2 -# instance.rest_send = rest_send -# instance.results = Results() -# instance.refresh() -# bgp_as = instance.bgp_as -# assert bgp_as is None - - -# def test_fabric_details_by_nv_pair_v2_00510(fabric_details_by_nv_pair_v2) -> None: -# """ -# ### Classes and Methods -# - FabricDetailsByNvPair() -# - __init__() -# - refresh() -# - _get_nv_pair() -# - bgp_as.getter - -# ### Summary -# - Verify that property getters for ``nvPairs`` items return ``None`` -# when ``_get_nv_pair()`` raises ``ValueError`` because fabric -# does not exist. - -# ### Setup - Code -# - Sender() is instantiated and configured. -# - RestSend() is instantiated and configured. -# - Results() is instantiated. -# - FabricDetailsByNvPair() is instantiated and configured. -# - FabricDetailsByNvPair().refresh() is called. - -# ### Setup - Data -# - responses() yields a 200 response that does not contain any fabrics. - -# ### Trigger -# ``bgp_as`` is accessed. - -# ### Expected Result -# - ``_get_nv_pair()`` raises ``ValueError``. -# - ``bgp_as.getter`` catches ``ValueError`` and returns ``None``. -# """ -# method_name = inspect.stack()[0][3] -# key = f"{method_name}a" - -# def responses(): -# yield responses_fabric_details_by_nv_pair_v2(key) - -# sender = Sender() -# sender.gen = ResponseGenerator(responses()) -# rest_send = RestSend(PARAMS) -# rest_send.sender = sender -# rest_send.response_handler = ResponseHandler() -# with does_not_raise(): -# instance = fabric_details_by_nv_pair_v2 -# instance.rest_send = rest_send -# instance.results = Results() -# instance.refresh() -# instance.filter = "FABRIC_DOES_NOT_EXIST" -# bgp_as = instance.bgp_as -# assert bgp_as is None - - -# def test_fabric_details_by_nv_pair_v2_00600(fabric_details_by_nv_pair_v2) -> None: -# """ -# ### Classes and Methods -# - FabricDetailsByNvPair() -# - __init__() -# - refresh() -# - filtered_data.getter - -# ### Summary -# - Verify that ``filtered_data`` property getter raises ``ValueError`` -# when ``filter`` is not set. - -# ### Setup - Code -# - Sender() is instantiated and configured. -# - RestSend() is instantiated and configured. -# - Results() is instantiated. -# - FabricDetailsByNvPair() is instantiated and configured. -# - FabricDetailsByNvPair().refresh() is called. - -# ### Setup - Data -# - responses() yields a 200 response. - -# ### Trigger -# ``filtered_data.getter`` is accessed. - -# ### Expected Result -# - ``filtered_data.getter`` raises ``ValueError``. -# """ -# method_name = inspect.stack()[0][3] -# key = f"{method_name}a" - -# def responses(): -# yield responses_fabric_details_by_nv_pair_v2(key) - -# sender = Sender() -# sender.gen = ResponseGenerator(responses()) -# rest_send = RestSend(PARAMS) -# rest_send.sender = sender -# rest_send.response_handler = ResponseHandler() -# with does_not_raise(): -# instance = fabric_details_by_nv_pair_v2 -# instance.rest_send = rest_send -# instance.results = Results() -# instance.refresh() -# match = r"FabricDetailsByNvPair\.filtered_data:\s+" -# match += r"FabricDetailsByNvPair\.filter must be set\s+" -# match += r"before accessing FabricDetailsByNvPair\.filtered_data\." -# with pytest.raises(ValueError, match=match): -# instance.filtered_data # pylint: disable=pointless-statement - - -# def test_fabric_details_by_nv_pair_v2_00610(fabric_details_by_nv_pair_v2) -> None: -# """ -# ### Classes and Methods -# - FabricDetailsByNvPair() -# - __init__() -# - refresh() -# - filtered_data.getter - -# ### Summary -# - Verify that ``filtered_data`` property returns expected values -# when ``filter`` is set and matches a fabric on the controller. - -# ### Setup - Code -# - Sender() is instantiated and configured. -# - RestSend() is instantiated and configured. -# - Results() is instantiated. -# - FabricDetailsByNvPair() is instantiated and configured. -# - FabricDetailsByNvPair().refresh() is called. - -# ### Setup - Data -# - responses() yields a 200 response with a matching fabric. - -# ### Trigger -# ``filtered_data.getter`` is accessed. - -# ### Expected Result -# - ``filtered_data.getter`` returns expected value. -# """ -# method_name = inspect.stack()[0][3] -# key = f"{method_name}a" - -# def responses(): -# yield responses_fabric_details_by_nv_pair_v2(key) - -# sender = Sender() -# sender.gen = ResponseGenerator(responses()) -# rest_send = RestSend(PARAMS) -# rest_send.sender = sender -# rest_send.response_handler = ResponseHandler() -# with does_not_raise(): -# instance = fabric_details_by_nv_pair_v2 -# instance.rest_send = rest_send -# instance.results = Results() -# instance.refresh() -# instance.filter = "MATCHING_FABRIC" -# data = instance.filtered_data -# assert data.get("nvPairs", {}).get("BGP_AS") == "65001" -# assert data.get("nvPairs", {}).get("ENABLE_NETFLOW") == "false" - - -# def test_fabric_details_by_nv_pair_v2_00700(fabric_details_by_nv_pair_v2) -> None: -# """ -# ### Classes and Methods -# - FabricDetailsByNvPair() -# - __init__() -# - refresh() -# - _get() -# - template_name.getter - -# ### Summary -# - Verify that property getters for top-level items return ``None`` -# when ``_get()`` raises ``ValueError`` because ``filter`` -# is not set prior to accessing a property. - -# ### Setup - Code -# - Sender() is instantiated and configured. -# - RestSend() is instantiated and configured. -# - Results() is instantiated. -# - FabricDetailsByNvPair() is instantiated and configured. -# - FabricDetailsByNvPair().refresh() is called. - -# ### Setup - Data -# - responses() yields a 200 response. - -# ### Trigger -# ``template_name`` is accessed before setting ``filter``. - -# ### Expected Result -# - ``_get()`` raises ``ValueError``. -# - ``template_name.getter`` catches ``ValueError`` and returns ``None``. -# """ -# method_name = inspect.stack()[0][3] -# key = f"{method_name}a" - -# def responses(): -# yield responses_fabric_details_by_nv_pair_v2(key) - -# sender = Sender() -# sender.gen = ResponseGenerator(responses()) -# rest_send = RestSend(PARAMS) -# rest_send.sender = sender -# rest_send.response_handler = ResponseHandler() -# with does_not_raise(): -# instance = fabric_details_by_nv_pair_v2 -# instance.rest_send = rest_send -# instance.results = Results() -# instance.refresh() -# template_name = instance.template_name -# assert template_name is None - - -# def test_fabric_details_by_nv_pair_v2_00710(fabric_details_by_nv_pair_v2) -> None: -# """ -# ### Classes and Methods -# - FabricDetailsByNvPair() -# - __init__() -# - refresh() -# - _get() -# - template_name.getter - -# ### Summary -# - Verify that property getters for top-level items return ``None`` -# when ``_get()`` raises ``ValueError`` because fabric -# does not exist. - -# ### Setup - Code -# - Sender() is instantiated and configured. -# - RestSend() is instantiated and configured. -# - Results() is instantiated. -# - FabricDetailsByNvPair() is instantiated and configured. -# - FabricDetailsByNvPair().refresh() is called. - -# ### Setup - Data -# - responses() yields a 200 response that does not contain any fabrics. - -# ### Trigger -# ``template_name.getter`` is accessed. - -# ### Expected Result -# - ``_get()`` raises ``ValueError``. -# - ``template_name.getter`` catches ``ValueError`` and returns ``None``. -# """ -# method_name = inspect.stack()[0][3] -# key = f"{method_name}a" - -# def responses(): -# yield responses_fabric_details_by_nv_pair_v2(key) - -# sender = Sender() -# sender.gen = ResponseGenerator(responses()) -# rest_send = RestSend(PARAMS) -# rest_send.sender = sender -# rest_send.response_handler = ResponseHandler() -# with does_not_raise(): -# instance = fabric_details_by_nv_pair_v2 -# instance.rest_send = rest_send -# instance.results = Results() -# instance.refresh() -# instance.filter = "FABRIC_DOES_NOT_EXIST" -# template_name = instance.template_name -# assert template_name is None +def test_fabric_details_by_nv_pair_v2_00210(fabric_details_by_nv_pair_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByNvPair() + - __init__() + - refresh_super() + + ### Summary + - Negative test case. + - Verify behavior when FABRIC_NAME is missing from nvPairs. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - FabricDetailsByNvPair() is instantiated and configured. + - FabricDetailsByNvPair().refresh() is called. + + ### Setup - Data + - responses_FabricDetailsByNvPair_V2 contains a response with + - 1x fabrics + - RETURN_CODE == 200 + - DATA[0].nvPairs is missing FABRIC_NAME key/value. + + ### Trigger + - FabricDetailsByNvPair().refresh() is called. + + ### Expected Result + - Exception is not raised. + - All fabrics matching ``filter_key`` and ``filter_value`` + are returned in ``filtered_data``. + - ``Results()`` are updated. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_fabric_details_by_nv_pair_v2(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = fabric_details_by_nv_pair_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.filter_key = "SOME_KEY" + instance.filter_value = "SOME_VALUE" + instance.refresh() + + assert isinstance(instance.results.diff, list) + assert isinstance(instance.results.result, list) + assert isinstance(instance.results.response, list) + + assert len(instance.results.diff) == 1 + assert len(instance.results.result) == 1 + assert len(instance.results.response) == 1 + + assert True in instance.results.failed + assert False not in instance.results.failed + assert False in instance.results.changed + assert True not in instance.results.changed + + +def test_fabric_details_by_nv_pair_v2_00400(fabric_details_by_nv_pair_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByNvPair() + - __init__() + - refresh() + + ### Summary + - Verify refresh() raises ``ValueError`` if + ``FabricDetails().refresh_super()`` raises ``ValueError``. + - RETURN_CODE is 200. + - Controller response contains one fabric (f1). + + ### Setup - Code + - FabricDetailsByNvPair() is instantiated + - FabricDetailsByNvPair().RestSend() is instantiated + - FabricDetailsByNvPair().Results() is NOT instantiated. + + ### Setup - Data + - None + + ### Expected Result + - ``ValueException`` is raised by ``refresh_super()`` and caught by + ``refresh()``. + """ + with does_not_raise(): + instance = fabric_details_by_nv_pair_v2 + instance.rest_send = RestSend(PARAMS) + instance.filter_key = "SOME_KEY" + instance.filter_value = "SOME_VALUE" + + match = r"Failed to refresh fabric details:\s+" + match += r"Error detail:\s+" + match += r"FabricDetailsByNvPair\.validate_refresh_parameters:\s+" + match += r"FabricDetailsByNvPair\.results must be set before\s+" + match += r"calling FabricDetailsByNvPair\.refresh\(\)\." + with pytest.raises(ValueError, match=match): + instance.refresh() + + +def test_fabric_details_by_nv_pair_v2_00600(fabric_details_by_nv_pair_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByNvPair() + - __init__() + - refresh() + + ### Summary + - Verify that ``refresh()`` raises ``ValueError`` + when ``filter_key`` is not set. + + ### Setup - Code + - Sender() is instantiated and configured. + - RestSend() is instantiated and configured. + - Results() is instantiated. + - FabricDetailsByNvPair() is instantiated and configured. + - FabricDetailsByNvPair().filter_value is set + + ### Setup - Data + - responses() yields empty dict (i.e. a noop) + + ### Trigger + - FabricDetailsByNvPair().refresh() is called. + + ### Expected Result + - ``refresh()`` raises ``ValueError`` because ``filter_key`` is not set. + """ + + def responses(): + yield {} + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + with does_not_raise(): + instance = fabric_details_by_nv_pair_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.filter_value = "SOME_VALUE" + match = r"FabricDetailsByNvPair\.refresh:\s+" + match += r"set FabricDetailsByNvPair\.filter_key\s+" + match += r"to a nvPair key before calling\s+" + match += r"FabricDetailsByNvPair\.refresh\(\)\." + with pytest.raises(ValueError, match=match): + instance.refresh() + + +def test_fabric_details_by_nv_pair_v2_00610(fabric_details_by_nv_pair_v2) -> None: + """ + ### Classes and Methods + - FabricDetailsByNvPair() + - __init__() + - refresh() + + ### Summary + - Verify that ``refresh()`` raises ``ValueError`` + when ``filter_value`` is not set. + + ### Setup - Code + - Sender() is instantiated and configured. + - RestSend() is instantiated and configured. + - Results() is instantiated. + - FabricDetailsByNvPair() is instantiated and configured. + - FabricDetailsByNvPair().filter_key is set + + ### Setup - Data + - responses() yields empty dict (i.e. a noop) + + ### Trigger + - FabricDetailsByNvPair().refresh() is called. + + ### Expected Result + - ``refresh()`` raises ``ValueError`` because ``filter_value`` is not set. + """ + + def responses(): + yield {} + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + with does_not_raise(): + instance = fabric_details_by_nv_pair_v2 + instance.rest_send = rest_send + instance.results = Results() + instance.filter_key = "SOME_KEY" + match = r"FabricDetailsByNvPair\.refresh:\s+" + match += r"set FabricDetailsByNvPair\.filter_value\s+" + match += r"to a nvPair value before calling\s+" + match += r"FabricDetailsByNvPair\.refresh\(\)\." + with pytest.raises(ValueError, match=match): + instance.refresh() From 7abe4218ddad33d77eaccd4e0861c08d6687af7f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 11 Jun 2024 21:33:20 -0700 Subject: [PATCH 154/230] sender_dcnm.py: 66% unit test coverage. 1. sender_dcnm.py: covert properties dict to individual private vars. 2. sender_dcnm.py: Sender().commit(): wrap _verify_commit_parameters() in try-exept block. 3. module_utils/common/common_utils.py: add fixtures and response functions for sender_dcnm and sender_file. 4. test_sender_dcnm.py: initial test cases. - test_sender_dcnm_00000 Class properties are initialized to expected values - test_sender_dcnm_00100 Verify ``commit()`` re-raises ``ValueError`` when ``_verify_commit_parameters()`` raises ``ValueError`` due to ``ansible_module`` not being set. - test_sender_dcnm_00110 Verify ``commit()`` re-raises ``ValueError`` when ``_verify_commit_parameters()`` raises ``ValueError`` due to ``path`` not being set. - test_sender_dcnm_00120 Verify ``commit()`` re-raises ``ValueError`` when ``_verify_commit_parameters()`` raises ``ValueError`` due to ``verb`` not being set. --- plugins/module_utils/common/sender_dcnm.py | 42 +++-- .../unit/module_utils/common/common_utils.py | 48 +++++ .../module_utils/common/test_sender_dcnm.py | 176 ++++++++++++++++++ 3 files changed, 248 insertions(+), 18 deletions(-) create mode 100644 tests/unit/module_utils/common/test_sender_dcnm.py diff --git a/plugins/module_utils/common/sender_dcnm.py b/plugins/module_utils/common/sender_dcnm.py index 5612f0c4b..bec381dba 100644 --- a/plugins/module_utils/common/sender_dcnm.py +++ b/plugins/module_utils/common/sender_dcnm.py @@ -65,12 +65,12 @@ def __init__(self): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.params = None - self.properties = {} - self.properties["ansible_module"] = None - self.properties["path"] = None - self.properties["payload"] = None - self.properties["response"] = None - self.properties["verb"] = None + self._ansible_module = None + self._path = None + self._payload = None + self._response = None + self._verb = None + self._valid_verbs = {"GET", "POST", "PUT", "DELETE"} msg = "ENTERED Sender(): " @@ -119,7 +119,13 @@ def commit(self): method_name = inspect.stack()[0][3] caller = inspect.stack()[1][3] - self._verify_commit_parameters() + try: + self._verify_commit_parameters() + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Not all mandatory parameters are set. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " msg += f"Calling dcnm_send: verb {self.verb}, path {self.path}" @@ -146,7 +152,7 @@ def ansible_module(self): ### Raises - ``TypeError`` if value is not an instance of AnsibleModule. """ - return self.properties["ansible_module"] + return self._ansible_module @ansible_module.setter def ansible_module(self, value): @@ -155,11 +161,11 @@ def ansible_module(self, value): self.params = value.params except AttributeError as error: msg = f"{self.class_name}.{method_name}: " - msg += "instance.ansible_module must be an instance of AnsibleModule. " + msg += "ansible_module must be an instance of AnsibleModule. " msg += f"Got type {type(value).__name__}, value {value}. " msg += f"Error detail: {error}." raise TypeError(msg) from error - self.properties["ansible_module"] = value + self._ansible_module = value @property def path(self): @@ -172,11 +178,11 @@ def path(self): ### Example ``/appcenter/cisco/ndfc/api/v1/...etc...`` """ - return self.properties.get("path") + return self._path @path.setter def path(self, value): - self.properties["path"] = value + self._path = value @property def payload(self): @@ -186,7 +192,7 @@ def payload(self): ### Raises - ``TypeError`` if value is not a ``dict``. """ - return self.properties["payload"] + return self._payload @payload.setter def payload(self, value): @@ -197,7 +203,7 @@ def payload(self, value): msg += f"Got type {type(value).__name__}, " msg += f"value {value}." raise TypeError(msg) - self.properties["payload"] = value + self._payload = value @property def response(self): @@ -211,7 +217,7 @@ def response(self): - getter: Return a copy of ``response`` - setter: Set ``response`` """ - return copy.deepcopy(self.properties.get("response")) + return copy.deepcopy(self._response) @response.setter def response(self, value): @@ -222,7 +228,7 @@ def response(self, value): msg += f"Got type {type(value).__name__}, " msg += f"value {value}." raise TypeError(msg) - self.properties["response"] = value + self._response = value @property def verb(self): @@ -235,7 +241,7 @@ def verb(self): ### Valid verbs ``GET``, ``POST``, ``PUT``, ``DELETE`` """ - return self.properties.get("verb") + return self._verb @verb.setter def verb(self, value): @@ -245,4 +251,4 @@ def verb(self, value): msg += f"{method_name} must be one of {sorted(self._valid_verbs)}. " msg += f"Got {value}." raise ValueError(msg) - self.properties["verb"] = value + self._verb = value diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index 27b8a761a..9786a3e4e 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -40,6 +40,10 @@ ParamsValidate from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate_v2 import \ ParamsValidate as ParamsValidateV2 +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import \ + Sender as SenderDcnm +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender as SenderFile from .fixture import load_fixture @@ -135,6 +139,30 @@ def controller_version_fixture(): return ControllerVersion(MockAnsibleModule) +@pytest.fixture(name="sender_dcnm") +def sender_dcnm_fixture(): + """ + return Send() imported from sender_dcnm.py + """ + instance = SenderDcnm() + instance.ansible_module = MockAnsibleModule + return instance + + +@pytest.fixture(name="sender_file") +def sender_file_fixture(): + """ + return Send() imported from sender_file.py + """ + + def responses(): + yield {} + + instance = SenderFile() + instance.gen = ResponseGenerator(responses()) + return instance + + @pytest.fixture(name="log") def log_fixture(): """ @@ -268,6 +296,26 @@ def responses_maintenance_mode(key: str) -> Dict[str, str]: return response +def responses_sender_dcnm(key: str) -> Dict[str, str]: + """ + Return data in responses_SenderDcnm.json + """ + response_file = "responses_SenderDcnm" + response = load_fixture(response_file).get(key) + print(f"responses_sender_dcnm: {key} : {response}") + return response + + +def responses_sender_file(key: str) -> Dict[str, str]: + """ + Return data in responses_SenderFile.json + """ + response_file = "responses_SenderFile" + response = load_fixture(response_file).get(key) + print(f"responses_sender_file: {key} : {response}") + return response + + def responses_switch_details(key: str) -> Dict[str, str]: """ Return data in responses_SwitchDetails.json diff --git a/tests/unit/module_utils/common/test_sender_dcnm.py b/tests/unit/module_utils/common/test_sender_dcnm.py new file mode 100644 index 000000000..b01a2647f --- /dev/null +++ b/tests/unit/module_utils/common/test_sender_dcnm.py @@ -0,0 +1,176 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +from typing import Any, Dict + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import \ + Sender +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + responses_sender_dcnm, sender_dcnm_fixture) + + +def test_sender_dcnm_00000() -> None: + """ + ### Classes and Methods + - Sender() + - __init__() + + ### Summary + - Class properties are initialized to expected values + """ + instance = Sender() + assert instance.params is None + assert instance._ansible_module is None + assert instance._path is None + assert instance.payload is None + assert instance._response is None + assert instance._valid_verbs == {"GET", "POST", "PUT", "DELETE"} + assert instance._verb is None + + +def test_sender_dcnm_00100() -> None: + """ + ### Classes and Methods + - Sender() + - _verify_commit_parameters() + - commit() + + ### Summary + Verify ``commit()`` re-raises ``ValueError`` when + ``_verify_commit_parameters()`` raises ``ValueError`` + due to ``ansible_module`` not being set. + + ### Setup - Code + - Sender() is initialized. + - Sender().path is set. + - Sender().verb is set. + - Sender().ansible_module is NOT set. + + ### Setup - Data + None + + ### Trigger + - Sender().commit() is called. + + ### Expected Result + - Sender().commit() re-raises ``ValueError``. + + + """ + instance = Sender() + instance.path = "/foo/path" + instance.verb = "GET" + + match = r"Sender\.commit:\s+" + match += r"Not all mandatory parameters are set\.\s+" + match += r"Error detail:\s+" + match += r"Sender\._verify_commit_parameters:\s+" + match += r"ansible_module must be set before calling commit\(\)\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_sender_dcnm_00110(sender_dcnm) -> None: + """ + ### Classes and Methods + - Sender() + - _verify_commit_parameters() + - commit() + + ### Summary + Verify ``commit()`` re-raises ``ValueError`` when + ``_verify_commit_parameters()`` raises ``ValueError`` + due to ``path`` not being set. + + ### Setup - Code + - Sender() is initialized. + - Sender().ansible_module is set. + - Sender().verb is set. + - Sender().path is NOT set. + + ### Setup - Data + None + + ### Trigger + - Sender().commit() is called. + + ### Expected Result + - Sender().commit() re-raises ``ValueError``. + + + """ + instance = sender_dcnm + instance.verb = "GET" + + match = r"Sender\.commit:\s+" + match += r"Not all mandatory parameters are set\.\s+" + match += r"Error detail:\s+" + match += r"Sender\._verify_commit_parameters:\s+" + match += r"path must be set before calling commit\(\)\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_sender_dcnm_00120(sender_dcnm) -> None: + """ + ### Classes and Methods + - Sender() + - _verify_commit_parameters() + - commit() + + ### Summary + Verify ``commit()`` re-raises ``ValueError`` when + ``_verify_commit_parameters()`` raises ``ValueError`` + due to ``verb`` not being set. + + ### Setup - Code + - Sender() is initialized. + - Sender().ansible_module is set. + - Sender().path is set. + - Sender().verb is NOT set. + + ### Setup - Data + None + + ### Trigger + - Sender().commit() is called. + + ### Expected Result + - Sender().commit() re-raises ``ValueError``. + """ + instance = sender_dcnm + instance.path = "/foo/path" + + match = r"Sender\.commit:\s+" + match += r"Not all mandatory parameters are set\.\s+" + match += r"Error detail:\s+" + match += r"Sender\._verify_commit_parameters:\s+" + match += r"verb must be set before calling commit\(\)\." + with pytest.raises(ValueError, match=match): + instance.commit() From 6cc6dc47d51108fab79e1cfebb5e6060cc2191da Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 11 Jun 2024 23:21:43 -0700 Subject: [PATCH 155/230] sender_dcnm.py: 100% unit test coverage. 1. Sender().__init__(): initialize self._dcnm_send for easier unit test patching. 2. Sender(): modify property error messages for consistency. 3. Add the following test cases. - test_sender_dcnm_00200 Verify ``commit()`` populates ``response`` with expected values for ``verb`` == POST and ``payload`` == None. - test_sender_dcnm_00210 Verify ``commit()`` populates ``response`` with expected values for ``verb`` == POST and ``payload`` != None. - test_sender_dcnm_00300 Verify ``ansible_module.setter`` raises ``TypeError`` if passed something other than an AnsibleModule() instance. - test_sender_dcnm_00400 Verify ``payload.setter`` raises ``TypeError`` if passed something other than a ``dict``. - test_sender_dcnm_00500 Verify ``response.setter`` raises ``TypeError`` if passed something other than a ``dict``. - test_sender_dcnm_00600 Verify ``verb.setter`` raises ``ValueError`` if passed an invalid value (not one of DELETE, GET, POST, PUT). --- plugins/module_utils/common/sender_dcnm.py | 11 +- .../common/fixtures/responses_SenderDcnm.json | 22 ++ .../module_utils/common/test_sender_dcnm.py | 240 +++++++++++++++++- 3 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 tests/unit/module_utils/common/fixtures/responses_SenderDcnm.json diff --git a/plugins/module_utils/common/sender_dcnm.py b/plugins/module_utils/common/sender_dcnm.py index bec381dba..fc8f1d1d2 100644 --- a/plugins/module_utils/common/sender_dcnm.py +++ b/plugins/module_utils/common/sender_dcnm.py @@ -66,6 +66,7 @@ def __init__(self): self.params = None self._ansible_module = None + self._dcnm_send = dcnm_send self._path = None self._payload = None self._response = None @@ -131,12 +132,12 @@ def commit(self): msg += f"Calling dcnm_send: verb {self.verb}, path {self.path}" if self.payload is None: self.log.debug(msg) - response = dcnm_send(self.ansible_module, self.verb, self.path) + response = self._dcnm_send(self.ansible_module, self.verb, self.path) else: msg += ", payload: " msg += f"{json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) - response = dcnm_send( + response = self._dcnm_send( self.ansible_module, self.verb, self.path, @@ -161,7 +162,7 @@ def ansible_module(self, value): self.params = value.params except AttributeError as error: msg = f"{self.class_name}.{method_name}: " - msg += "ansible_module must be an instance of AnsibleModule. " + msg += f"{method_name} must be an instance of AnsibleModule. " msg += f"Got type {type(value).__name__}, value {value}. " msg += f"Error detail: {error}." raise TypeError(msg) from error @@ -199,7 +200,7 @@ def payload(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "instance.response must be a dict. " + msg += f"{method_name} must be a dict. " msg += f"Got type {type(value).__name__}, " msg += f"value {value}." raise TypeError(msg) @@ -224,7 +225,7 @@ def response(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "instance.response must be a dict. " + msg += f"{method_name} must be a dict. " msg += f"Got type {type(value).__name__}, " msg += f"value {value}." raise TypeError(msg) diff --git a/tests/unit/module_utils/common/fixtures/responses_SenderDcnm.json b/tests/unit/module_utils/common/fixtures/responses_SenderDcnm.json new file mode 100644 index 000000000..a0b8b7b3b --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/responses_SenderDcnm.json @@ -0,0 +1,22 @@ +{ + "test_sender_dcnm_00200a": { + "DATA": { + "status": "Configuration deployment completed." + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_Fabric/config-deploy/FDO22180ASJ?forceShowRun=False", + "RETURN_CODE": 200 + }, + "test_sender_dcnm_00210a": { + "DATA": { + "nvPairs": { + "FABRIC_NAME": "VXLAN_Fabric" + } + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_Fabric/config-deploy/FDO22180ASJ?forceShowRun=False", + "RETURN_CODE": 200 + } +} \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_sender_dcnm.py b/tests/unit/module_utils/common/test_sender_dcnm.py index b01a2647f..731cdf17d 100644 --- a/tests/unit/module_utils/common/test_sender_dcnm.py +++ b/tests/unit/module_utils/common/test_sender_dcnm.py @@ -26,13 +26,17 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import inspect from typing import Any, Dict import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import ( + EpFabricConfigDeploy, EpFabricCreate) from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import \ Sender from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( - responses_sender_dcnm, sender_dcnm_fixture) + ResponseGenerator, does_not_raise, responses_sender_dcnm, + sender_dcnm_fixture) def test_sender_dcnm_00000() -> None: @@ -44,7 +48,8 @@ def test_sender_dcnm_00000() -> None: ### Summary - Class properties are initialized to expected values """ - instance = Sender() + with does_not_raise(): + instance = Sender() assert instance.params is None assert instance._ansible_module is None assert instance._path is None @@ -83,9 +88,10 @@ def test_sender_dcnm_00100() -> None: """ - instance = Sender() - instance.path = "/foo/path" - instance.verb = "GET" + with does_not_raise(): + instance = Sender() + instance.path = "/foo/path" + instance.verb = "GET" match = r"Sender\.commit:\s+" match += r"Not all mandatory parameters are set\.\s+" @@ -122,11 +128,10 @@ def test_sender_dcnm_00110(sender_dcnm) -> None: ### Expected Result - Sender().commit() re-raises ``ValueError``. - - """ - instance = sender_dcnm - instance.verb = "GET" + with does_not_raise(): + instance = sender_dcnm + instance.verb = "GET" match = r"Sender\.commit:\s+" match += r"Not all mandatory parameters are set\.\s+" @@ -164,8 +169,9 @@ def test_sender_dcnm_00120(sender_dcnm) -> None: ### Expected Result - Sender().commit() re-raises ``ValueError``. """ - instance = sender_dcnm - instance.path = "/foo/path" + with does_not_raise(): + instance = sender_dcnm + instance.path = "/foo/path" match = r"Sender\.commit:\s+" match += r"Not all mandatory parameters are set\.\s+" @@ -174,3 +180,215 @@ def test_sender_dcnm_00120(sender_dcnm) -> None: match += r"verb must be set before calling commit\(\)\." with pytest.raises(ValueError, match=match): instance.commit() + + +def test_sender_dcnm_00200(sender_dcnm, monkeypatch) -> None: + """ + ### Classes and Methods + - Sender() + - _verify_commit_parameters() + - commit() + + ### Summary + Verify ``commit()`` populates ``response`` with expected values + for ``verb`` == POST and ``payload`` == None. + + ### Setup - Code + - Sender() is initialized. + - Sender().ansible_module is set. + - Sender().path is set to /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_Fabric/config-deploy/FDO22180ASJ?forceShowRun=False. + - Sender().verb is set to POST. + + ### Setup - Data + responses_SenderDcnm.json: + - DATA.status: Configuration deployment completed. + - MESSAGE: OK + - METHOD: POST + - RETURN_CODE: 200 + + + ### Trigger + - Sender().commit() is called. + + ### Expected Result + - Sender().commit() sets Sender().response to expected value. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_sender_dcnm(key) + + gen = ResponseGenerator(responses()) + + def mock_dcnm_send(*args, **kwargs): # pylint: disable=unused-argument + item = gen.next + return item + + with does_not_raise(): + endpoint = EpFabricConfigDeploy() + endpoint.fabric_name = "VXLAN_Fabric" + endpoint.serial_number = "FDO22180ASJ" + endpoint.force_show_run = False + instance = sender_dcnm + monkeypatch.setattr(instance, "_dcnm_send", mock_dcnm_send) + instance.path = endpoint.path + instance.verb = endpoint.verb + instance.commit() + assert instance.response.get("MESSAGE", None) == "OK" + assert instance.response.get("METHOD", None) == "POST" + assert instance.response.get("RETURN_CODE", None) == 200 + assert ( + instance.response.get("DATA", {}).get("status") + == "Configuration deployment completed." + ) + + +def test_sender_dcnm_00210(sender_dcnm, monkeypatch) -> None: + """ + ### Classes and Methods + - Sender() + - _verify_commit_parameters() + - commit() + + ### Summary + Verify ``commit()`` populates ``response`` with expected values + for ``verb`` == POST and ``payload`` != None. + + ### Setup - Code + - Sender() is initialized. + - Sender().ansible_module is set. + - Sender().path is set to /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_Fabric/config-deploy/FDO22180ASJ?forceShowRun=False. + - Sender().verb is set to POST. + + ### Setup - Data + responses_SenderDcnm.json: + - DATA.status: Configuration deployment completed. + - MESSAGE: OK + - METHOD: POST + - RETURN_CODE: 200 + + + ### Trigger + - Sender().commit() is called. + + ### Expected Result + - Sender().commit() sets Sender().response to expected value. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_sender_dcnm(key) + + gen = ResponseGenerator(responses()) + + def mock_dcnm_send(*args, **kwargs): # pylint: disable=unused-argument + item = gen.next + return item + + payload = { + "BGP_AS": 65001, + "DEPLOY": True, + "FABRIC_NAME": "VXLAN_Fabric", + "FABRIC_TYPE": "VXLAN_EVPN", + } + + with does_not_raise(): + endpoint = EpFabricCreate() + endpoint.fabric_name = "VXLAN_Fabric" + endpoint.template_name = "Easy_Fabric" + instance = sender_dcnm + monkeypatch.setattr(instance, "_dcnm_send", mock_dcnm_send) + instance.path = endpoint.path + instance.verb = endpoint.verb + instance.payload = payload + instance.commit() + assert instance.response.get("MESSAGE", None) == "OK" + assert instance.response.get("METHOD", None) == "POST" + assert instance.response.get("RETURN_CODE", None) == 200 + assert ( + instance.response.get("DATA", {}).get("nvPairs").get("FABRIC_NAME", None) + == "VXLAN_Fabric" + ) + + +def test_sender_dcnm_00300() -> None: + """ + ### Classes and Methods + - Sender() + - ansible_module.setter + + ### Summary + Verify ``ansible_module.setter`` raises ``TypeError`` + if passed something other than an AnsibleModule() instance. + """ + with does_not_raise(): + instance = Sender() + + match = r"Sender\.ansible_module:\s+" + match += r"ansible_module must be an instance of AnsibleModule\.\s+" + match += r"Got type int, value 10\.\s+" + match += r"Error detail: 'int' object has no attribute 'params'\." + with pytest.raises(TypeError, match=match): + instance.ansible_module = 10 + + +def test_sender_dcnm_00400() -> None: + """ + ### Classes and Methods + - Sender() + - payload.setter + + ### Summary + Verify ``payload.setter`` raises ``TypeError`` + if passed something other than a ``dict``. + """ + with does_not_raise(): + instance = Sender() + + match = r"Sender\.payload:\s+" + match += r"payload must be a dict\.\s+" + match += r"Got type int, value 10\." + with pytest.raises(TypeError, match=match): + instance.payload = 10 + + +def test_sender_dcnm_00500() -> None: + """ + ### Classes and Methods + - Sender() + - response.setter + + ### Summary + Verify ``response.setter`` raises ``TypeError`` + if passed something other than a ``dict``. + """ + with does_not_raise(): + instance = Sender() + + match = r"Sender\.response:\s+" + match += r"response must be a dict\.\s+" + match += r"Got type int, value 10\." + with pytest.raises(TypeError, match=match): + instance.response = 10 + + +def test_sender_dcnm_00600() -> None: + """ + ### Classes and Methods + - Sender() + - verb.setter + + ### Summary + Verify ``verb.setter`` raises ``ValueError`` + if passed an invalid value (not one of DELETE, GET, POST, PUT). + """ + with does_not_raise(): + instance = Sender() + + match = r"Sender\.verb:\s+" + match += r"verb must be one of.*\.\s+" + match += r"Got 10\." + with pytest.raises(ValueError, match=match): + instance.verb = 10 From 94eec029a029df992ea465e4be6b6b7a02147efa Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 12 Jun 2024 14:46:56 -0700 Subject: [PATCH 156/230] sender_file.py: 100% unit test coverage. 1. Add unit tests for sender_file.py. 2. test_sender_dcnm.py: fix assert in test 00000. 3. test_response_handler.py: Align with ResponseHandler() changes. 4. ResponseHandler(): use dunder vars for private vars, rather than dict. 5. sender_dcnm.py: Minor error message cleanup. 6. sender_file.py: Sender().commit() catch ValueError raised by _validate_commit_parameters() 6. sender_file.py: Sender().gen() raise TypeError if input value does not support response_generator interface. 6. module_utils/common/common_utils.py: ResponseGenerator() add implements property that returns a string representing the interface that is implemented. --- .../module_utils/common/response_handler.py | 18 +- plugins/module_utils/common/sender_dcnm.py | 7 +- plugins/module_utils/common/sender_file.py | 34 ++- .../unit/module_utils/common/common_utils.py | 10 + .../common/test_response_handler.py | 4 +- .../module_utils/common/test_sender_dcnm.py | 2 +- .../module_utils/common/test_sender_file.py | 270 ++++++++++++++++++ 7 files changed, 323 insertions(+), 22 deletions(-) create mode 100644 tests/unit/module_utils/common/test_sender_file.py diff --git a/plugins/module_utils/common/response_handler.py b/plugins/module_utils/common/response_handler.py index c96f3dbf7..66d56d2fe 100644 --- a/plugins/module_utils/common/response_handler.py +++ b/plugins/module_utils/common/response_handler.py @@ -96,9 +96,9 @@ def __init__(self): self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._properties = {} - self._properties["response"] = None - self._properties["result"] = None + self._response = None + self._result = None + self._verb = None self.return_codes_success = {200, 404} self.valid_verbs = {"DELETE", "GET", "POST", "PUT"} @@ -233,7 +233,7 @@ def response(self): Set response. External interface to set the response from the controller. """ - return self._properties.get("response", None) + return self._response @response.setter def response(self, value): @@ -253,7 +253,7 @@ def response(self, value): msg += "response must have a RETURN_CODE key. " msg += f"Got: {value}." raise ValueError(msg) - self._properties["response"] = value + self._response = value @property def result(self): @@ -262,7 +262,7 @@ def result(self): - setter: Set result. - setter: Raise ``TypeError`` if result is not a dict. """ - return self._properties.get("result", None) + return self._result @result.setter def result(self, value): @@ -272,7 +272,7 @@ def result(self, value): msg += f"{self.class_name}.{method_name} must be a dict. " msg += f"Got {value}." raise TypeError(msg) - self._properties["result"] = value + self._result = value @property def verb(self): @@ -291,7 +291,7 @@ def verb(self): ### setter External interface to set the request verb. """ - return self._properties.get("verb", None) + return self._verb @verb.setter def verb(self, value): @@ -302,4 +302,4 @@ def verb(self, value): msg += f"{', '.join(sorted(self.valid_verbs))}. " msg += f"Got {value}." raise ValueError(msg) - self._properties["verb"] = value + self._verb = value diff --git a/plugins/module_utils/common/sender_dcnm.py b/plugins/module_utils/common/sender_dcnm.py index fc8f1d1d2..edd186c18 100644 --- a/plugins/module_utils/common/sender_dcnm.py +++ b/plugins/module_utils/common/sender_dcnm.py @@ -86,16 +86,17 @@ def _verify_commit_parameters(self): - ``ValueError`` if ``verb`` is not set - ``ValueError`` if ``path`` is not set """ + method_name = inspect.stack()[0][3] if self.ansible_module is None: - msg = f"{self.class_name}._verify_commit_parameters: " + msg = f"{self.class_name}.{method_name}: " msg += "ansible_module must be set before calling commit()." raise ValueError(msg) if self.path is None: - msg = f"{self.class_name}._verify_commit_parameters: " + msg = f"{self.class_name}.{method_name}: " msg += "path must be set before calling commit()." raise ValueError(msg) if self.verb is None: - msg = f"{self.class_name}._verify_commit_parameters: " + msg = f"{self.class_name}.{method_name}: " msg += "verb must be set before calling commit()." raise ValueError(msg) diff --git a/plugins/module_utils/common/sender_file.py b/plugins/module_utils/common/sender_file.py index 1c3a8f27d..653cef02b 100644 --- a/plugins/module_utils/common/sender_file.py +++ b/plugins/module_utils/common/sender_file.py @@ -81,8 +81,9 @@ def _verify_commit_parameters(self): - ``ValueError`` if ``verb`` is not set - ``ValueError`` if ``path`` is not set """ + method_name = inspect.stack()[0][3] if self.gen is None: - msg = f"{self.class_name}._verify_commit_parameters: " + msg = f"{self.class_name}.{method_name}: " msg += "gen must be set before calling commit()." raise ValueError(msg) @@ -94,7 +95,14 @@ def commit(self): ### Raises - ```ValueError`` if ``gen`` is not set. """ - self._verify_commit_parameters() + method_name = inspect.stack()[0][3] + try: + self._verify_commit_parameters() + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Not all mandatory parameters are set. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error method_name = inspect.stack()[0][3] caller = inspect.stack()[1][3] @@ -117,14 +125,30 @@ def ansible_module(self, value): @property def gen(self): """ + ### Summary - getter: Return the ``ResponseGenerator()`` instance. - setter: Set the ``ResponseGenerator()`` instance that provides simulated responses. + + ### Raises + ``TypeError`` if value is not a class implementing the + response_generator interface. """ return self._gen @gen.setter def gen(self, value): + method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += "Expected a class implementing the " + msg += "response_generator interface. " + msg += f"Got {value}." + try: + implements = value.implements + except AttributeError as error: + raise TypeError(msg) from error + if implements != "response_generator": + raise TypeError(msg) self._gen = value @property @@ -167,17 +191,13 @@ def response(self): The simulated response from a file. ### Raises - - ``TypeError`` if value is not a ``dict``. + None - getter: Return a copy of ``response`` - setter: Set ``response`` """ return self.gen.next - @response.setter - def response(self, value): - self._response = value - @property def verb(self): """ diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index 9786a3e4e..42c856b89 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -85,6 +85,16 @@ def next(self): """ return next(self.gen) + @property + def implements(self): + """ + ### Summary + Used by Sender() classes to verify Sender().gen is a + response generator which implements the response_generator + interfacee. + """ + return "response_generator" + def public_method_for_pylint(self) -> Any: """ Add one public method to appease pylint diff --git a/tests/unit/module_utils/common/test_response_handler.py b/tests/unit/module_utils/common/test_response_handler.py index be4df011d..67ef65070 100644 --- a/tests/unit/module_utils/common/test_response_handler.py +++ b/tests/unit/module_utils/common/test_response_handler.py @@ -50,8 +50,8 @@ def test_response_handler_00010(response_handler) -> None: """ with does_not_raise(): instance = response_handler - assert instance._properties["response"] is None - assert instance._properties["result"] is None + assert instance._response is None + assert instance._result is None assert instance.return_codes_success == {200, 404} assert instance.valid_verbs == {"DELETE", "GET", "POST", "PUT"} diff --git a/tests/unit/module_utils/common/test_sender_dcnm.py b/tests/unit/module_utils/common/test_sender_dcnm.py index 731cdf17d..f563fe07d 100644 --- a/tests/unit/module_utils/common/test_sender_dcnm.py +++ b/tests/unit/module_utils/common/test_sender_dcnm.py @@ -53,7 +53,7 @@ def test_sender_dcnm_00000() -> None: assert instance.params is None assert instance._ansible_module is None assert instance._path is None - assert instance.payload is None + assert instance._payload is None assert instance._response is None assert instance._valid_verbs == {"GET", "POST", "PUT", "DELETE"} assert instance._verb is None diff --git a/tests/unit/module_utils/common/test_sender_file.py b/tests/unit/module_utils/common/test_sender_file.py new file mode 100644 index 000000000..894f45295 --- /dev/null +++ b/tests/unit/module_utils/common/test_sender_file.py @@ -0,0 +1,270 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import inspect +from typing import Any, Dict + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + ResponseGenerator, does_not_raise, responses_sender_file, + sender_file_fixture) + + +def responses(): + """ + ### Summary + Co-routine for any unit tests below using ResponseGenerator() class. + """ + yield {} + + +def test_sender_file_00000() -> None: + """ + ### Classes and Methods + - Sender() + - __init__() + + ### Summary + - Class properties are initialized to expected values + """ + with does_not_raise(): + instance = Sender() + assert instance._ansible_module is None + assert instance._gen is None + assert instance._path is None + assert instance._payload is None + assert instance._response is None + assert instance._verb is None + + +def test_sender_file_00100() -> None: + """ + ### Classes and Methods + - Sender() + - _verify_commit_parameters() + - commit() + + ### Summary + Verify ``commit()`` re-raises ``ValueError`` when + ``_verify_commit_parameters()`` raises ``ValueError`` + due to ``gen`` not being set. + + ### Setup - Code + - Sender() is initialized. + - Sender().gen is NOT set. + + ### Setup - Data + None + + ### Trigger + - Sender().commit() is called. + + ### Expected Result + - Sender().commit() re-raises ``ValueError``. + + + """ + with does_not_raise(): + instance = Sender() + + match = r"Sender\.commit:\s+" + match += r"Not all mandatory parameters are set\.\s+" + match += r"Error detail: Sender\._verify_commit_parameters:\s+" + match += r"gen must be set before calling commit\(\)\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_sender_file_00200() -> None: + """ + ### Classes and Methods + - Sender() + - ansible_module.setter + + ### Summary + Verify ``ansible_module.setter`` does not raise exceptions + and that ``ansible_module.getter`` returns whatever is passed + to ``ansible_module.setter``. + + ### NOTES + ``ansible_module`` property is basically a noop, included only to satisfy + the external interface. + """ + with does_not_raise(): + instance = Sender() + instance.ansible_module = 10 + assert instance.ansible_module == 10 + + +def test_sender_file_00210() -> None: + """ + ### Classes and Methods + - Sender() + - path.setter + + ### Summary + Verify ``path.setter`` does not raise exceptions + and that ``path.getter`` returns whatever is passed + to ``path.setter``. + + ### NOTES + ``path`` property is basically a noop, included only to satisfy + the external interface. + """ + with does_not_raise(): + instance = Sender() + instance.path = 10 + assert instance.path == 10 + + +def test_sender_file_00220() -> None: + """ + ### Classes and Methods + - Sender() + - payload.setter + + ### Summary + Verify ``payload.setter`` does not raise exceptions + and that ``payload.getter`` returns whatever is passed + to ``payload.setter``. + + ### NOTES + ``payload`` property is basically a noop, included only to satisfy + the external interface. + """ + with does_not_raise(): + instance = Sender() + instance.payload = 10 + assert instance.payload == 10 + + +def test_sender_file_00230() -> None: + """ + ### Classes and Methods + - Sender() + - response.setter + + ### Summary + Verify ``response.getter`` returns whatever is yielded + by the coroutine passed to ResponseGenerator() + + ### NOTES + ``response`` has no setter. + """ + with does_not_raise(): + instance = Sender() + instance.gen = ResponseGenerator(responses()) + assert instance.response == {} + + +def test_sender_file_00240() -> None: + """ + ### Classes and Methods + - Sender() + - verb.setter + + ### Summary + Verify ``verb.setter`` does not raise exceptions + and that ``verb.getter`` returns whatever is passed + to ``verb.setter``. + + ### NOTES + ``verb`` property is basically a noop, included only to satisfy + the external interface. + """ + with does_not_raise(): + instance = Sender() + instance.verb = 10 + assert instance.verb == 10 + + +MATCH_00300 = r"Sender.gen:\s+" +MATCH_00300 += r"Expected a class implementing the response_generator\s+" +MATCH_00300 += r"interface\. Got.*" + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (10, True, pytest.raises(TypeError, match=MATCH_00300)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_00300)), + (ResponseGenerator(responses()), False, does_not_raise()), + ], +) +def test_sender_file_00300(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - Sender() + - gen.setter + + ### Summary + Verify ``gen.setter`` raises ``TypeError`` if the value + passed to it does not implement expected response_generator + interface. + """ + with expected: + instance = Sender() + instance.gen = value + if not does_raise: + assert isinstance(instance.gen, ResponseGenerator) + + +def test_sender_file_00310() -> None: + """ + ### Classes and Methods + - Sender() + - gen.setter + + ### Summary + Verify ``gen.setter`` raises ``TypeError`` if the value + passed to it is a class that exposes an ``implements`` + property, but that does not implement expected + response_generator interface. + """ + + class ResponseGenerator2: # pylint: disable=too-few-public-methods + """ + A class that does not implement the response_generator interface. + """ + + @property + def implements(self): + """ + Return unexpected value. + """ + return "not_response_generator" + + with does_not_raise(): + instance = Sender() + match = r"Sender\.gen:\s+" + match += r"Expected a class implementing the\s+" + match += r"response_generator interface\. Got.*" + with pytest.raises(TypeError, match=match): + instance.gen = ResponseGenerator2() From 305ca3d682c81636dc8b44da9d8b4a620636f802 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 12 Jun 2024 15:11:35 -0700 Subject: [PATCH 157/230] Remove duplicate ResponseGenerator class ResponseGenerator() was located in both the following locations: - tests/unit/modules/dcnm/dcnm_fabric/utils.py - tests/unit/module_utils/common/common_utils.py We changed RsponseGenerator() to include an "implements" property, which broke all the unit tests that were using the copy that didn't include this property. Modified all the unit test file imports to point to the copy in common_utils.py and removed the other copy in utils.py. --- tests/unit/module_utils/common/test_log.py | 2 +- .../dcnm_fabric/test_fabric_config_deploy.py | 10 +++-- .../dcnm_fabric/test_fabric_config_save.py | 6 ++- .../dcnm/dcnm_fabric/test_fabric_create.py | 9 +++-- .../dcnm_fabric/test_fabric_create_bulk.py | 9 +++-- .../dcnm/dcnm_fabric/test_fabric_delete.py | 9 +++-- .../dcnm/dcnm_fabric/test_fabric_details.py | 6 ++- .../test_fabric_details_by_name.py | 6 ++- .../test_fabric_details_by_name_v2.py | 4 +- .../test_fabric_details_by_nv_pair.py | 6 ++- .../test_fabric_details_by_nv_pair_v2.py | 4 +- .../dcnm_fabric/test_fabric_details_v2.py | 5 ++- .../dcnm/dcnm_fabric/test_fabric_query.py | 6 ++- .../dcnm_fabric/test_fabric_replaced_bulk.py | 11 +++--- .../dcnm/dcnm_fabric/test_fabric_summary.py | 6 ++- .../dcnm_fabric/test_fabric_update_bulk.py | 11 +++--- .../dcnm/dcnm_fabric/test_template_get.py | 6 ++- .../dcnm/dcnm_fabric/test_template_get_all.py | 6 ++- tests/unit/modules/dcnm/dcnm_fabric/utils.py | 37 ------------------- 19 files changed, 75 insertions(+), 84 deletions(-) diff --git a/tests/unit/module_utils/common/test_log.py b/tests/unit/module_utils/common/test_log.py index f63115088..c3c771109 100644 --- a/tests/unit/module_utils/common/test_log.py +++ b/tests/unit/module_utils/common/test_log.py @@ -36,7 +36,7 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( - does_not_raise, log_fixture, MockAnsibleModule) + MockAnsibleModule, does_not_raise, log_fixture) def test_log_00010(tmp_path, log) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py index a097a4c92..8cf26e2d7 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py @@ -42,11 +42,13 @@ Results from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.config_deploy import \ FabricConfigDeploy +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - fabric_config_deploy_fixture, fabric_details_by_name_fixture, - fabric_summary_fixture, params, responses_fabric_config_deploy, - responses_fabric_details_by_name, responses_fabric_summary) + MockAnsibleModule, does_not_raise, fabric_config_deploy_fixture, + fabric_details_by_name_fixture, fabric_summary_fixture, params, + responses_fabric_config_deploy, responses_fabric_details_by_name, + responses_fabric_summary) def test_fabric_config_deploy_00010(fabric_config_deploy) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py index 7766e25bf..6638169f6 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_save.py @@ -42,9 +42,11 @@ Results from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.config_save import \ FabricConfigSave +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - fabric_config_save_fixture, params, responses_fabric_config_save) + MockAnsibleModule, does_not_raise, fabric_config_save_fixture, params, + responses_fabric_config_save) def test_fabric_config_save_00010(fabric_config_save) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create.py index 4c075ae21..668379ecf 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create.py @@ -38,11 +38,12 @@ Results from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ FabricDetailsByName +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - fabric_create_fixture, params, payloads_fabric_create, - responses_fabric_create, responses_fabric_details_by_name, - rest_send_response_current) + MockAnsibleModule, does_not_raise, fabric_create_fixture, params, + payloads_fabric_create, responses_fabric_create, + responses_fabric_details_by_name, rest_send_response_current) def test_fabric_create_00010(fabric_create) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_bulk.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_bulk.py index d25156852..b088542e5 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_create_bulk.py @@ -38,11 +38,12 @@ Results from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ FabricDetailsByName +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - fabric_create_bulk_fixture, params, payloads_fabric_create_bulk, - responses_fabric_create_bulk, responses_fabric_details_by_name, - rest_send_response_current) + MockAnsibleModule, does_not_raise, fabric_create_bulk_fixture, params, + payloads_fabric_create_bulk, responses_fabric_create_bulk, + responses_fabric_details_by_name, rest_send_response_current) def test_fabric_create_bulk_00010(fabric_create_bulk) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py index 079ad6f94..f5123b2c6 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_delete.py @@ -42,11 +42,12 @@ FabricDetailsByName from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_summary import \ FabricSummary +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - fabric_delete_fixture, params, responses_fabric_delete, - responses_fabric_details_by_name, responses_fabric_summary, - rest_send_response_current) + MockAnsibleModule, does_not_raise, fabric_delete_fixture, params, + responses_fabric_delete, responses_fabric_details_by_name, + responses_fabric_summary, rest_send_response_current) def test_fabric_delete_00010(fabric_delete) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py index 356b3eb75..86d46e847 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details.py @@ -40,9 +40,11 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - fabric_details_fixture, responses_fabric_details) + MockAnsibleModule, does_not_raise, fabric_details_fixture, + responses_fabric_details) def test_fabric_details_00010(fabric_details) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py index a54e9c8f0..93f642f21 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name.py @@ -40,9 +40,11 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - fabric_details_by_name_fixture, responses_fabric_details_by_name) + MockAnsibleModule, does_not_raise, fabric_details_by_name_fixture, + responses_fabric_details_by_name) def test_fabric_details_by_name_00010(fabric_details_by_name) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py index fccf1c0e2..70d8b03bf 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py @@ -43,8 +43,10 @@ Sender from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ FabricDetailsByName +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - ResponseGenerator, does_not_raise, fabric_details_by_name_v2_fixture, + does_not_raise, fabric_details_by_name_v2_fixture, responses_fabric_details_by_name_v2) PARAMS = {"state": "query", "check_mode": False} diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py index a31f7a19b..cfc474f36 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair.py @@ -40,9 +40,11 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - fabric_details_by_nv_pair_fixture, responses_fabric_details_by_nv_pair) + MockAnsibleModule, does_not_raise, fabric_details_by_nv_pair_fixture, + responses_fabric_details_by_nv_pair) def test_fabric_details_by_nv_pair_00010(fabric_details_by_nv_pair) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py index ae9c6efd9..8d3e97700 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py @@ -43,8 +43,10 @@ Sender from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ FabricDetailsByNvPair +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - ResponseGenerator, does_not_raise, fabric_details_by_nv_pair_v2_fixture, + does_not_raise, fabric_details_by_nv_pair_v2_fixture, responses_fabric_details_by_nv_pair_v2) PARAMS = {"state": "query", "check_mode": False} diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py index 102c975a7..9cff01137 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py @@ -46,9 +46,10 @@ Sender from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ FabricDetails +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - ResponseGenerator, does_not_raise, fabric_details_v2_fixture, - responses_fabric_details_v2) + does_not_raise, fabric_details_v2_fixture, responses_fabric_details_v2) def test_fabric_details_v2_00000(fabric_details_v2) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_query.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_query.py index 6f07f3e65..80f10ea8b 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_query.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_query.py @@ -38,9 +38,11 @@ Results from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details import \ FabricDetailsByName +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, fabric_query_fixture, - params, responses_fabric_query) + MockAnsibleModule, does_not_raise, fabric_query_fixture, params, + responses_fabric_query) def test_fabric_query_00010(fabric_query) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py index e25b79013..e2fdd82b4 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_replaced_bulk.py @@ -52,12 +52,13 @@ TemplateGet from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.verify_playbook_params import \ VerifyPlaybookParams +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - fabric_replaced_bulk_fixture, params, payloads_fabric_replaced_bulk, - responses_config_deploy, responses_config_save, - responses_fabric_details_by_name, responses_fabric_replaced_bulk, - responses_fabric_summary) + MockAnsibleModule, does_not_raise, fabric_replaced_bulk_fixture, params, + payloads_fabric_replaced_bulk, responses_config_deploy, + responses_config_save, responses_fabric_details_by_name, + responses_fabric_replaced_bulk, responses_fabric_summary) def test_fabric_replaced_bulk_00010(fabric_replaced_bulk) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py index dcc6ec8fd..ead8b2649 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_summary.py @@ -42,9 +42,11 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - fabric_summary_fixture, responses_fabric_summary) + MockAnsibleModule, does_not_raise, fabric_summary_fixture, + responses_fabric_summary) def test_fabric_summary_00010(fabric_summary) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_update_bulk.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_update_bulk.py index 35a71cb75..e24bf777e 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_update_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_update_bulk.py @@ -40,12 +40,13 @@ FabricDetailsByName from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_summary import \ FabricSummary +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - fabric_update_bulk_fixture, params, payloads_fabric_update_bulk, - responses_config_deploy, responses_config_save, - responses_fabric_details_by_name, responses_fabric_summary, - responses_fabric_update_bulk) + MockAnsibleModule, does_not_raise, fabric_update_bulk_fixture, params, + payloads_fabric_update_bulk, responses_config_deploy, + responses_config_save, responses_fabric_details_by_name, + responses_fabric_summary, responses_fabric_update_bulk) def test_fabric_update_bulk_00010(fabric_update_bulk) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py index 176cdaae2..83b907066 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py @@ -40,9 +40,11 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - responses_template_get, template_get_fixture) + MockAnsibleModule, does_not_raise, responses_template_get, + template_get_fixture) def test_template_get_00010(template_get) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py index bc1f28cdc..aa3cf96f2 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get_all.py @@ -40,9 +40,11 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( - MockAnsibleModule, ResponseGenerator, does_not_raise, - responses_template_get_all, template_get_all_fixture) + MockAnsibleModule, does_not_raise, responses_template_get_all, + template_get_all_fixture) def test_template_get_all_00010(template_get_all) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_fabric/utils.py b/tests/unit/modules/dcnm/dcnm_fabric/utils.py index 4abd24cad..20a52ea6a 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/utils.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/utils.py @@ -67,43 +67,6 @@ } -class ResponseGenerator: - """ - Given a generator, return the items in the generator with - each call to the next property - - For usage in the context of dcnm_image_policy unit tests, see: - test: test_image_policy_create_bulk_00037 - file: tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py - - Simplified usage example below. - - def responses(): - yield {"key1": "value1"} - yield {"key2": "value2"} - - gen = ResponseGenerator(responses()) - - print(gen.next) # {"key1": "value1"} - print(gen.next) # {"key2": "value2"} - """ - - def __init__(self, gen): - self.gen = gen - - @property - def next(self): - """ - Return the next item in the generator - """ - return next(self.gen) - - def public_method_for_pylint(self) -> Any: - """ - Add one public method to appease pylint - """ - - class MockAnsibleModule: """ Mock the AnsibleModule class From 69ea49498919662a7c9cf606d16b87662e5005ae Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 12 Jun 2024 15:45:15 -0700 Subject: [PATCH 158/230] RestSend() v2: 73% unit test coverage Add the following unit tests: - test_rest_send_v2_00000 Verify class properties are initialized to expected values - test_rest_send_v2_00100 Verify ``_verify_commit_parameters()`` raises ``ValueError`` due to ``path`` not being set. - test_rest_send_v2_00110 Verify ``_verify_commit_parameters()`` raises ``ValueError`` due to ``response_handler`` not being set. - test_rest_send_v2_00120 Verify ``_verify_commit_parameters()`` raises ``ValueError`` due to ``response_handler`` not being set. - test_rest_send_v2_00130 Verify ``_verify_commit_parameters()`` raises ``ValueError`` due to ``response_handler`` not being set. --- .../module_utils/common/test_rest_send_v2.py | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 tests/unit/module_utils/common/test_rest_send_v2.py diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py new file mode 100644 index 000000000..7f000fbb0 --- /dev/null +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -0,0 +1,233 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + ResponseGenerator, does_not_raise) + +PARAMS = {"state": "merged", "check_mode": False} + + +def test_rest_send_v2_00000() -> None: + """ + ### Classes and Methods + - RestSend() + - __init__() + + ### Summary + - Verify class properties are initialized to expected values + """ + with does_not_raise(): + instance = RestSend(PARAMS) + assert instance.params == PARAMS + assert instance.properties["check_mode"] is False + assert instance.properties["path"] is None + assert instance.properties["payload"] is None + assert instance.properties["response"] == [] + assert instance.properties["response_current"] == {} + assert instance.properties["response_handler"] is None + assert instance.properties["result"] == [] + assert instance.properties["result_current"] == {} + assert instance.properties["send_interval"] == 5 + assert instance.properties["sender"] is None + assert instance.properties["timeout"] == 300 + assert instance.properties["unit_test"] is False + assert instance.properties["verb"] is None + + assert instance.saved_check_mode is None + assert instance.saved_timeout is None + assert instance._valid_verbs == {"GET", "POST", "PUT", "DELETE"} + assert instance.check_mode == PARAMS.get("check_mode", None) + assert instance.state == PARAMS.get("state", None) + + +def test_rest_send_v2_00100() -> None: + """ + ### Classes and Methods + - RestSend() + - _verify_commit_parameters() + - commit_normal_mode() + - commit() + + ### Summary + Verify ``_verify_commit_parameters()`` raises ``ValueError`` + due to ``path`` not being set. + + ### Setup - Code + - RestSend() is initialized. + - RestSend().path is NOT set. + - RestSend().response_handler is set. + - RestSend().sender is set. + - RestSend().verb is set. + + ### Setup - Data + None + + ### Trigger + - Sender().commit() is called. + + ### Expected Result + - RestSend().commit() re-raises ``ValueError``. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + instance.sender = Sender() + instance.response_handler = ResponseHandler() + instance.verb = "GET" + + match = r"RestSend\._verify_commit_parameters:\s+" + match += r"path must be set before calling commit\(\)." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_rest_send_v2_00110() -> None: + """ + ### Classes and Methods + - RestSend() + - _verify_commit_parameters() + - commit_normal_mode() + - commit() + + ### Summary + Verify ``_verify_commit_parameters()`` raises ``ValueError`` + due to ``response_handler`` not being set. + + ### Setup - Code + - RestSend() is initialized. + - RestSend().path is set. + - RestSend().response_handler is NOT set. + - RestSend().sender is set. + - RestSend().verb is set. + + ### Setup - Data + None + + ### Trigger + - Sender().commit() is called. + + ### Expected Result + - RestSend().commit() re-raises ``ValueError``. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + instance.path = "/foo/path" + instance.sender = Sender() + instance.verb = "GET" + + match = r"RestSend\._verify_commit_parameters:\s+" + match += r"response_handler must be set before calling commit\(\)." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_rest_send_v2_00120() -> None: + """ + ### Classes and Methods + - RestSend() + - _verify_commit_parameters() + - commit_normal_mode() + - commit() + + ### Summary + Verify ``_verify_commit_parameters()`` raises ``ValueError`` + due to ``response_handler`` not being set. + + ### Setup - Code + - RestSend() is initialized. + - RestSend().path is set. + - RestSend().response_handler is set. + - RestSend().sender is NOT set. + - RestSend().verb is set. + + ### Setup - Data + None + + ### Trigger + - Sender().commit() is called. + + ### Expected Result + - RestSend().commit() re-raises ``ValueError``. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + instance.path = "/foo/path" + instance.response_handler = ResponseHandler() + instance.verb = "GET" + + match = r"RestSend\._verify_commit_parameters:\s+" + match += r"sender must be set before calling commit\(\)." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_rest_send_v2_00130() -> None: + """ + ### Classes and Methods + - RestSend() + - _verify_commit_parameters() + - commit_normal_mode() + - commit() + + ### Summary + Verify ``_verify_commit_parameters()`` raises ``ValueError`` + due to ``response_handler`` not being set. + + ### Setup - Code + - RestSend() is initialized. + - RestSend().path is set. + - RestSend().response_handler is set. + - RestSend().sender is set. + - RestSend().verb is NOT set. + + ### Setup - Data + None + + ### Trigger + - Sender().commit() is called. + + ### Expected Result + - RestSend().commit() re-raises ``ValueError``. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + instance.path = "/foo/path" + instance.response_handler = ResponseHandler() + instance.sender = Sender() + + match = r"RestSend\._verify_commit_parameters:\s+" + match += r"verb must be set before calling commit\(\)." + with pytest.raises(ValueError, match=match): + instance.commit() From 92190d2cfaf3916f45d702ac7f407b65f94967b5 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 12 Jun 2024 16:27:22 -0700 Subject: [PATCH 159/230] RestSend() v2: 81% unit test coverage Add the following test case: - test_rest_send_v2_00200 Verify ``commit_check_mode()`` happy path. --- .../module_utils/common/test_rest_send_v2.py | 68 ++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index 7f000fbb0..8e1cb40da 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -26,6 +26,7 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import copy import pytest from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ @@ -96,10 +97,10 @@ def test_rest_send_v2_00100() -> None: None ### Trigger - - Sender().commit() is called. + - RestSend().commit() is called. ### Expected Result - - RestSend().commit() re-raises ``ValueError``. + - RestSend()._verify_commit_parameters() raises ``ValueError``. """ with does_not_raise(): instance = RestSend(PARAMS) @@ -136,10 +137,10 @@ def test_rest_send_v2_00110() -> None: None ### Trigger - - Sender().commit() is called. + - RestSend().commit() is called. ### Expected Result - - RestSend().commit() re-raises ``ValueError``. + - RestSend()._verify_commit_parameters() raises ``ValueError``. """ with does_not_raise(): instance = RestSend(PARAMS) @@ -176,10 +177,10 @@ def test_rest_send_v2_00120() -> None: None ### Trigger - - Sender().commit() is called. + - RestSend().commit() is called. ### Expected Result - - RestSend().commit() re-raises ``ValueError``. + - RestSend()._verify_commit_parameters() raises ``ValueError``. """ with does_not_raise(): instance = RestSend(PARAMS) @@ -216,10 +217,10 @@ def test_rest_send_v2_00130() -> None: None ### Trigger - - Sender().commit() is called. + - RestSend().commit() is called. ### Expected Result - - RestSend().commit() re-raises ``ValueError``. + - RestSend()._verify_commit_parameters() raises ``ValueError``. """ with does_not_raise(): instance = RestSend(PARAMS) @@ -231,3 +232,54 @@ def test_rest_send_v2_00130() -> None: match += r"verb must be set before calling commit\(\)." with pytest.raises(ValueError, match=match): instance.commit() + + +def test_rest_send_v2_00200() -> None: + """ + ### Classes and Methods + - RestSend() + - commit_check_mode() + - commit() + + ### Summary + Verify ``commit_check_mode()`` happy path. + + ### Setup - Code + - PARAMS["check_mode"] is set to True + - RestSend() is initialized. + - RestSend().path is set. + - RestSend().response_handler is set. + - RestSend().sender is set. + - RestSend().verb is set. + + ### Setup - Data + None + + ### Trigger + - RestSend().commit() is called. + + ### Expected Result + - RestSend().commit() re-raises ``ValueError``. + """ + params = copy.copy(PARAMS) + params["check_mode"] = True + + with does_not_raise(): + instance = RestSend(params) + instance.path = "/foo/path" + instance.response_handler = ResponseHandler() + instance.sender = Sender() + instance.verb = "GET" + instance.commit() + assert instance.response_current["CHECK_MODE"] == instance.check_mode + assert ( + instance.response_current["DATA"] == "[simulated-check-mode-response:Success]" + ) + assert instance.response_current["MESSAGE"] == "OK" + assert instance.response_current["METHOD"] == instance.verb + assert instance.response_current["REQUEST_PATH"] == instance.path + assert instance.response_current["RETURN_CODE"] == 200 + assert instance.result_current["success"] is True + assert instance.result_current["found"] is True + assert instance.response == [instance.response_current] + assert instance.result == [instance.result_current] From 48c5547d9a4de9f870e987b6f4ca24bc62142730 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 12 Jun 2024 17:10:02 -0700 Subject: [PATCH 160/230] RestSend().commit(): Catch and re-raise exceptions 1. RestSend().commit(): v2. Catch exceptions thrown by commit_check_mode() and commit_normal_mode() and re-raise them as ValueError with message indicating commit() is in the call stack. 2. Update unit tests to reflect the modified error message. --- plugins/module_utils/common/rest_send_v2.py | 18 ++++++++++++----- .../module_utils/common/test_rest_send_v2.py | 20 +++++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index f6a0cb7d6..795d90f7f 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -252,13 +252,21 @@ def commit(self): - ``unit_test`` is not a ``bool`` """ - msg = f"{self.class_name}.commit: " + method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " msg += f"check_mode: {self.check_mode}." self.log.debug(msg) - if self.check_mode is True: - self.commit_check_mode() - else: - self.commit_normal_mode() + + try: + if self.check_mode is True: + self.commit_check_mode() + else: + self.commit_normal_mode() + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error during commit. " + msg += f"Error details: {error}" + raise ValueError(msg) from error def commit_check_mode(self): """ diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index 8e1cb40da..3e5a676ef 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -108,7 +108,10 @@ def test_rest_send_v2_00100() -> None: instance.response_handler = ResponseHandler() instance.verb = "GET" - match = r"RestSend\._verify_commit_parameters:\s+" + match = r"RestSend\.commit:\s+" + match += r"Error during commit\.\s+" + match += r"Error details:\s+" + match += r"RestSend\._verify_commit_parameters:\s+" match += r"path must be set before calling commit\(\)." with pytest.raises(ValueError, match=match): instance.commit() @@ -148,7 +151,10 @@ def test_rest_send_v2_00110() -> None: instance.sender = Sender() instance.verb = "GET" - match = r"RestSend\._verify_commit_parameters:\s+" + match = r"RestSend\.commit:\s+" + match += r"Error during commit\.\s+" + match += r"Error details:\s+" + match += r"RestSend\._verify_commit_parameters:\s+" match += r"response_handler must be set before calling commit\(\)." with pytest.raises(ValueError, match=match): instance.commit() @@ -188,7 +194,10 @@ def test_rest_send_v2_00120() -> None: instance.response_handler = ResponseHandler() instance.verb = "GET" - match = r"RestSend\._verify_commit_parameters:\s+" + match = r"RestSend\.commit:\s+" + match += r"Error during commit\.\s+" + match += r"Error details:\s+" + match += r"RestSend\._verify_commit_parameters:\s+" match += r"sender must be set before calling commit\(\)." with pytest.raises(ValueError, match=match): instance.commit() @@ -228,8 +237,11 @@ def test_rest_send_v2_00130() -> None: instance.response_handler = ResponseHandler() instance.sender = Sender() + match = r"RestSend\.commit:\s+" + match += r"Error during commit\.\s+" + match += r"Error details:\s+" match = r"RestSend\._verify_commit_parameters:\s+" - match += r"verb must be set before calling commit\(\)." + match += r"verb must be set before calling commit\(\)\." with pytest.raises(ValueError, match=match): instance.commit() From f7a520d71bc424b5c2c3a270190800e3e2817c7f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 12 Jun 2024 17:40:05 -0700 Subject: [PATCH 161/230] RestSend() v2: 83% unit test coverage. Add the following test cases. - test_rest_send_v2_00210 Verify ``commit_check_mode()`` happy path when ``verb`` is "POST". - test_rest_send_v2_00500 Verify ``check_mode.setter`` raises ``TypeError`` when set to inappropriate types, and does not raise when set to boolean. 2. RestSend(): Tweak check_mode error message. --- plugins/module_utils/common/rest_send_v2.py | 2 +- .../module_utils/common/test_rest_send_v2.py | 118 +++++++++++++++++- 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 795d90f7f..1f52f14a0 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -452,7 +452,7 @@ def check_mode(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, bool): msg = f"{self.class_name}.{method_name}: " - msg += f"{method_name} must be a bool(). Got {value}." + msg += f"{method_name} must be a boolean. Got {value}." raise TypeError(msg) self.properties["check_mode"] = value diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index 3e5a676ef..f8b5ee4c8 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -254,7 +254,8 @@ def test_rest_send_v2_00200() -> None: - commit() ### Summary - Verify ``commit_check_mode()`` happy path. + Verify ``commit_check_mode()`` happy path when + ``verb`` is "GET". ### Setup - Code - PARAMS["check_mode"] is set to True @@ -271,7 +272,12 @@ def test_rest_send_v2_00200() -> None: - RestSend().commit() is called. ### Expected Result - - RestSend().commit() re-raises ``ValueError``. + - The following are updated to expected values: + - ``response`` + - ``response_current`` + - ``result`` + - ``result_current`` + - result_current["found"] is True """ params = copy.copy(PARAMS) params["check_mode"] = True @@ -295,3 +301,111 @@ def test_rest_send_v2_00200() -> None: assert instance.result_current["found"] is True assert instance.response == [instance.response_current] assert instance.result == [instance.result_current] + + +def test_rest_send_v2_00210() -> None: + """ + ### Classes and Methods + - RestSend() + - commit_check_mode() + - commit() + + ### Summary + Verify ``commit_check_mode()`` happy path when + ``verb`` is "POST". + + ### Setup - Code + - PARAMS["check_mode"] is set to True + - RestSend() is initialized. + - RestSend().path is set. + - RestSend().response_handler is set. + - RestSend().sender is set. + - RestSend().verb is set. + + ### Setup - Data + None + + ### Trigger + - RestSend().commit() is called. + + ### Expected Result + - The following are updated to expected values: + - ``response`` + - ``response_current`` + - ``result`` + - ``result_current`` + - result_current["changed"] is True + """ + params = copy.copy(PARAMS) + params["check_mode"] = True + + with does_not_raise(): + instance = RestSend(params) + instance.path = "/foo/path" + instance.response_handler = ResponseHandler() + instance.sender = Sender() + instance.verb = "POST" + instance.commit() + assert instance.response_current["CHECK_MODE"] == instance.check_mode + assert ( + instance.response_current["DATA"] == "[simulated-check-mode-response:Success]" + ) + assert instance.response_current["MESSAGE"] == "OK" + assert instance.response_current["METHOD"] == instance.verb + assert instance.response_current["REQUEST_PATH"] == instance.path + assert instance.response_current["RETURN_CODE"] == 200 + assert instance.result_current["success"] is True + assert instance.result_current["changed"] is True + assert instance.response == [instance.response_current] + assert instance.result == [instance.result_current] + + +MATCH_00500 = r"RestSend\.check_mode:\s+" +MATCH_00500 += r"check_mode must be a boolean\.\s+" +MATCH_00500 += r"Got.*\." + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (10, True, pytest.raises(TypeError, match=MATCH_00500)), + ([10], True, pytest.raises(TypeError, match=MATCH_00500)), + ({10}, True, pytest.raises(TypeError, match=MATCH_00500)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_00500)), + (None, True, pytest.raises(TypeError, match=MATCH_00500)), + (False, False, does_not_raise()), + (True, False, does_not_raise()), + ], +) +def test_rest_send_v2_00500(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - RestSend() + - check_mode.setter + + ### Summary + Verify ``check_mode.setter`` raises ``TypeError`` + when set to inappropriate types, and does not raise + when set to boolean. + + ### Setup - Code + - PARAMS["check_mode"] is set to True + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().check_mode is reset using various types. + + ### Expected Result + - ``check_mode`` raises TypeError for non-boolean inputs. + - ``check_mode`` accepts boolean values. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + + with expected: + instance.check_mode = value + if does_raise is False: + assert instance.check_mode == value From b0eb953a7df31d798bf232e129cdafcbb7e379dc Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 12 Jun 2024 21:34:26 -0700 Subject: [PATCH 162/230] RestSend() v2: 84% unit test coverage. 1. Added the following testcase. - test_rest_send_v2_00600 Verify ``response_current.setter`` raises ``TypeError`` when set to inappropriate types, and does not raise when set to dict. 2. RestSend().response_current.setter: tweaked error message to use method_name rather than hardcoded string. --- plugins/module_utils/common/rest_send_v2.py | 2 +- .../module_utils/common/test_rest_send_v2.py | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 1f52f14a0..ca2e2af07 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -517,7 +517,7 @@ def response_current(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "instance.response_current must be a dict. " + msg += f"{method_name} must be a dict. " msg += f"Got type {type(value).__name__}, " msg += f"Value: {value}." raise TypeError(msg) diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index f8b5ee4c8..1aed8a9d8 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -409,3 +409,54 @@ def test_rest_send_v2_00500(value, does_raise, expected) -> None: instance.check_mode = value if does_raise is False: assert instance.check_mode == value + + +MATCH_00600 = r"RestSend\.response_current:\s+" +MATCH_00600 += r"response_current must be a dict\.\s+" +MATCH_00600 += r"Got.*\." + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (10, True, pytest.raises(TypeError, match=MATCH_00600)), + ([10], True, pytest.raises(TypeError, match=MATCH_00600)), + ({10}, True, pytest.raises(TypeError, match=MATCH_00600)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_00600)), + (None, True, pytest.raises(TypeError, match=MATCH_00600)), + (False, True, pytest.raises(TypeError, match=MATCH_00600)), + (True, True, pytest.raises(TypeError, match=MATCH_00600)), + ({"RESULT_CODE": 200}, False, does_not_raise()), + ], +) +def test_rest_send_v2_00600(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - RestSend() + - response_current.setter + + ### Summary + Verify ``response_current.setter`` raises ``TypeError`` + when set to inappropriate types, and does not raise + when set to dict. + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().response_current is reset using various types. + + ### Expected Result + - ``response_current`` raises TypeError for non-dict inputs. + - ``response_current`` accepts dict values. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + + with expected: + instance.response_current = value + if does_raise is False: + assert instance.response_current == value From a297e40f13805be64562f199f74abbca65823ecf Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 12 Jun 2024 22:04:11 -0700 Subject: [PATCH 163/230] RestSend() v2: 88% unit test coverage. 1. Added the following test cases - test_rest_send_v2_00700 Verify ``response.setter`` raises ``TypeError`` when set to inappropriate types, and does not raise when set to dict. - test_rest_send_v2_00800 Verify ``result_current.setter`` raises ``TypeError`` when set to inappropriate types, and does not raise when set to dict. - test_rest_send_v2_00900 Verify ``result.setter`` raises ``TypeError`` when set to inappropriate types, and does not raise when set to dict. 2. RestSend() v2: Tweak error messages. --- plugins/module_utils/common/rest_send_v2.py | 9 +- .../module_utils/common/test_rest_send_v2.py | 157 ++++++++++++++++++ 2 files changed, 162 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index ca2e2af07..99dfe12e6 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -547,7 +547,7 @@ def response(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "instance.response must be a dict. " + msg += f"{method_name} must be a dict. " msg += f"Got type {type(value).__name__}, " msg += f"Value: {value}." raise TypeError(msg) @@ -619,8 +619,9 @@ 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}." + msg += f"{method_name} must be a dict. " + msg += f"Got type {type(value).__name__}, " + msg += f"Value: {value}." raise TypeError(msg) self.properties["result"].append(value) @@ -650,7 +651,7 @@ def result_current(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "instance.result_current must be a dict. " + msg += f"{method_name} must be a dict. " msg += f"Got {value}." raise TypeError(msg) self.properties["result_current"] = value diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index 1aed8a9d8..a3faeedc8 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -460,3 +460,160 @@ def test_rest_send_v2_00600(value, does_raise, expected) -> None: instance.response_current = value if does_raise is False: assert instance.response_current == value + + +MATCH_00700 = r"RestSend\.response:\s+" +MATCH_00700 += r"response must be a dict\.\s+" +MATCH_00700 += r"Got type.*,\s+" +MATCH_00700 += r"Value:\s+.*\." + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (10, True, pytest.raises(TypeError, match=MATCH_00700)), + ([10], True, pytest.raises(TypeError, match=MATCH_00700)), + ({10}, True, pytest.raises(TypeError, match=MATCH_00700)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_00700)), + (None, True, pytest.raises(TypeError, match=MATCH_00700)), + (False, True, pytest.raises(TypeError, match=MATCH_00700)), + (True, True, pytest.raises(TypeError, match=MATCH_00700)), + ({"RESULT_CODE": 200}, False, does_not_raise()), + ], +) +def test_rest_send_v2_00700(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - RestSend() + - response.setter + + ### Summary + Verify ``response.setter`` raises ``TypeError`` + when set to inappropriate types, and does not raise + when set to dict. + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().response is reset using various types. + + ### Expected Result + - ``response`` raises TypeError for non-dict inputs. + - ``response`` accepts dict values. + - ``response`` returns a list of dict in the happy path. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + + with expected: + instance.response = value + if does_raise is False: + assert instance.response == [value] + + +MATCH_00800 = r"RestSend\.result_current:\s+" +MATCH_00800 += r"result_current must be a dict\.\s+" +MATCH_00800 += r"Got.*\." + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (10, True, pytest.raises(TypeError, match=MATCH_00800)), + ([10], True, pytest.raises(TypeError, match=MATCH_00800)), + ({10}, True, pytest.raises(TypeError, match=MATCH_00800)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_00800)), + (None, True, pytest.raises(TypeError, match=MATCH_00800)), + (False, True, pytest.raises(TypeError, match=MATCH_00800)), + (True, True, pytest.raises(TypeError, match=MATCH_00800)), + ({"failed": False}, False, does_not_raise()), + ], +) +def test_rest_send_v2_00800(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - RestSend() + - result_current.setter + + ### Summary + Verify ``result_current.setter`` raises ``TypeError`` + when set to inappropriate types, and does not raise + when set to dict. + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().result_current is reset using various types. + + ### Expected Result + - ``result_current`` raises TypeError for non-dict inputs. + - ``result_current`` accepts dict values. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + + with expected: + instance.result_current = value + if does_raise is False: + assert instance.result_current == value + + +MATCH_00900 = r"RestSend\.result:\s+" +MATCH_00900 += r"result must be a dict\.\s+" +MATCH_00900 += r"Got type.*,\s+" +MATCH_00900 += r"Value:\s+.*\." + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (10, True, pytest.raises(TypeError, match=MATCH_00900)), + ([10], True, pytest.raises(TypeError, match=MATCH_00900)), + ({10}, True, pytest.raises(TypeError, match=MATCH_00900)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_00900)), + (None, True, pytest.raises(TypeError, match=MATCH_00900)), + (False, True, pytest.raises(TypeError, match=MATCH_00900)), + (True, True, pytest.raises(TypeError, match=MATCH_00900)), + ({"RESULT_CODE": 200}, False, does_not_raise()), + ], +) +def test_rest_send_v2_00900(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - RestSend() + - result.setter + + ### Summary + Verify ``result.setter`` raises ``TypeError`` + when set to inappropriate types, and does not raise + when set to dict. + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().result is reset using various types. + + ### Expected Result + - ``result`` raises TypeError for non-dict inputs. + - ``result`` accepts dict values. + - ``result`` returns a list of dict in the happy path. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + + with expected: + instance.result = value + if does_raise is False: + assert instance.result == [value] From a8199d928fb968dc6f04399a45accc24d8d59d26 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 09:09:56 -0700 Subject: [PATCH 164/230] RestSend() v2: 89% unit test coverage, more... 1. ResponseHandler().implements: Property to return the implemented interface string. 2. RestSend().response_hendler: Modify property to check that the correct interface is implemented. 3. test_rest_send_v2.py: Renumber test cases: test_rest_send_v2_00800 -> test_rest_send_v2_00900 test_rest_send_v2_00900 -> test_rest_send_v2_01000 Add test cases: - test_rest_send_v2_00800 Verify ``response_handler.setter`` raises ``TypeError`` when set to inappropriate types, and does not raise when set to a class that implements the response_handler_v1 interface. --- .../module_utils/common/response_handler.py | 7 + plugins/module_utils/common/rest_send_v2.py | 17 ++- .../module_utils/common/test_rest_send_v2.py | 121 ++++++++++++++---- 3 files changed, 113 insertions(+), 32 deletions(-) diff --git a/plugins/module_utils/common/response_handler.py b/plugins/module_utils/common/response_handler.py index 66d56d2fe..6795c7a94 100644 --- a/plugins/module_utils/common/response_handler.py +++ b/plugins/module_utils/common/response_handler.py @@ -209,6 +209,13 @@ def commit(self): raise ValueError(msg) self._handle_response() + @property + def implements(self): + """ + Return the interface this class implements. + """ + return "response_handler_v1" + @property def response(self): """ diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 99dfe12e6..773ef2dca 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -318,6 +318,9 @@ def commit_check_mode(self): self.response = copy.deepcopy(self.response_current) self.result = copy.deepcopy(self.result_current) except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error building response/result. " + msg += f"Error detail: {error}" raise ValueError(error) from error def commit_normal_mode(self): @@ -579,18 +582,18 @@ def response_handler(self): @response_handler.setter def response_handler(self, value): method_name = inspect.stack()[0][3] - _class_have = None - _class_need = "ResponseHandler" - + _implements_need = "response_handler_v1" + _implements_have = None msg = f"{self.class_name}.{method_name}: " - msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." + msg += f"{method_name} must implement {_implements_need}. " + msg += f"Got type {type(value).__name__}, " + msg += f"implementing {_implements_have}. " try: - _class_have = value.class_name + _implements_have = value.implements except AttributeError as error: msg += f"Error detail: {error}." raise TypeError(msg) from error - if _class_have != _class_need: + if _implements_have != _implements_need: raise TypeError(msg) self.properties["response_handler"] = value diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index a3faeedc8..d9d701ad0 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -41,6 +41,15 @@ PARAMS = {"state": "merged", "check_mode": False} +def responses(): + """ + Dummy coroutine for ResponseGenerator() + + See e.g. test_rest_send_v2_00800 + """ + yield {} + + def test_rest_send_v2_00000() -> None: """ ### Classes and Methods @@ -170,7 +179,7 @@ def test_rest_send_v2_00120() -> None: ### Summary Verify ``_verify_commit_parameters()`` raises ``ValueError`` - due to ``response_handler`` not being set. + due to ``sender`` not being set. ### Setup - Code - RestSend() is initialized. @@ -213,7 +222,7 @@ def test_rest_send_v2_00130() -> None: ### Summary Verify ``_verify_commit_parameters()`` raises ``ValueError`` - due to ``response_handler`` not being set. + due to ``verb`` not being set. ### Setup - Code - RestSend() is initialized. @@ -515,25 +524,87 @@ def test_rest_send_v2_00700(value, does_raise, expected) -> None: assert instance.response == [value] -MATCH_00800 = r"RestSend\.result_current:\s+" -MATCH_00800 += r"result_current must be a dict\.\s+" -MATCH_00800 += r"Got.*\." +MATCH_00800 = r"RestSend\.response_handler:\s+" +MATCH_00800 += r"response_handler must implement response_handler_v1\.\s+" +MATCH_00800 += r"Got type\s+.*,\s+" +MATCH_00800 += r"implementing\s+.*\." +MATCH_00800_A = rf"{MATCH_00800} Error detail:\s+.*" +MATCH_00800_B = MATCH_00800 @pytest.mark.parametrize( "value, does_raise, expected", [ - (10, True, pytest.raises(TypeError, match=MATCH_00800)), - ([10], True, pytest.raises(TypeError, match=MATCH_00800)), - ({10}, True, pytest.raises(TypeError, match=MATCH_00800)), - ("FOO", True, pytest.raises(TypeError, match=MATCH_00800)), - (None, True, pytest.raises(TypeError, match=MATCH_00800)), - (False, True, pytest.raises(TypeError, match=MATCH_00800)), - (True, True, pytest.raises(TypeError, match=MATCH_00800)), - ({"failed": False}, False, does_not_raise()), + (10, True, pytest.raises(TypeError, match=MATCH_00800_A)), + ([10], True, pytest.raises(TypeError, match=MATCH_00800_A)), + ({10}, True, pytest.raises(TypeError, match=MATCH_00800_A)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_00800_A)), + (None, True, pytest.raises(TypeError, match=MATCH_00800_A)), + (False, True, pytest.raises(TypeError, match=MATCH_00800_A)), + (True, True, pytest.raises(TypeError, match=MATCH_00800_A)), + ( + ResponseGenerator(responses()), + True, + pytest.raises(TypeError, match=MATCH_00800_B), + ), + (ResponseHandler(), False, does_not_raise()), ], ) def test_rest_send_v2_00800(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - RestSend() + - response_handler.setter + + ### Summary + Verify ``response_handler.setter`` raises ``TypeError`` + when set to inappropriate types, and does not raise + when set to a class that implements the response_handler_v1 + interface. + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().response_handler is reset using various types. + + ### Expected Result + - ``response_handler`` raises TypeError for inappropriate inputs. + - ``response_handler`` accepts appropriate inputs. + - ``response_handler`` happy path returns a class that implements the + response_handler_v1 interface. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + + with expected: + instance.response_handler = value + if does_raise is False: + assert isinstance(instance.response_handler, ResponseHandler) + + +MATCH_00900 = r"RestSend\.result_current:\s+" +MATCH_00900 += r"result_current must be a dict\.\s+" +MATCH_00900 += r"Got.*\." + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (10, True, pytest.raises(TypeError, match=MATCH_00900)), + ([10], True, pytest.raises(TypeError, match=MATCH_00900)), + ({10}, True, pytest.raises(TypeError, match=MATCH_00900)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_00900)), + (None, True, pytest.raises(TypeError, match=MATCH_00900)), + (False, True, pytest.raises(TypeError, match=MATCH_00900)), + (True, True, pytest.raises(TypeError, match=MATCH_00900)), + ({"failed": False}, False, does_not_raise()), + ], +) +def test_rest_send_v2_00900(value, does_raise, expected) -> None: """ ### Classes and Methods - RestSend() @@ -566,26 +637,26 @@ def test_rest_send_v2_00800(value, does_raise, expected) -> None: assert instance.result_current == value -MATCH_00900 = r"RestSend\.result:\s+" -MATCH_00900 += r"result must be a dict\.\s+" -MATCH_00900 += r"Got type.*,\s+" -MATCH_00900 += r"Value:\s+.*\." +MATCH_01000 = r"RestSend\.result:\s+" +MATCH_01000 += r"result must be a dict\.\s+" +MATCH_01000 += r"Got type.*,\s+" +MATCH_01000 += r"Value:\s+.*\." @pytest.mark.parametrize( "value, does_raise, expected", [ - (10, True, pytest.raises(TypeError, match=MATCH_00900)), - ([10], True, pytest.raises(TypeError, match=MATCH_00900)), - ({10}, True, pytest.raises(TypeError, match=MATCH_00900)), - ("FOO", True, pytest.raises(TypeError, match=MATCH_00900)), - (None, True, pytest.raises(TypeError, match=MATCH_00900)), - (False, True, pytest.raises(TypeError, match=MATCH_00900)), - (True, True, pytest.raises(TypeError, match=MATCH_00900)), + (10, True, pytest.raises(TypeError, match=MATCH_01000)), + ([10], True, pytest.raises(TypeError, match=MATCH_01000)), + ({10}, True, pytest.raises(TypeError, match=MATCH_01000)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_01000)), + (None, True, pytest.raises(TypeError, match=MATCH_01000)), + (False, True, pytest.raises(TypeError, match=MATCH_01000)), + (True, True, pytest.raises(TypeError, match=MATCH_01000)), ({"RESULT_CODE": 200}, False, does_not_raise()), ], ) -def test_rest_send_v2_00900(value, does_raise, expected) -> None: +def test_rest_send_v2_01000(value, does_raise, expected) -> None: """ ### Classes and Methods - RestSend() From 198c31441ebc8880c4d9f79376b94260b674620e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 09:44:52 -0700 Subject: [PATCH 165/230] RestSend v2: 90% unit test coverage. 1. Add the following test cases - test_rest_send_v2_00220 Verify ``commit_check_mode()`` sad path when ``response_handler.commit()`` raises ``ValueError``. --- .../module_utils/common/test_rest_send_v2.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index d9d701ad0..83ab8dc86 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -369,6 +369,87 @@ def test_rest_send_v2_00210() -> None: assert instance.result == [instance.result_current] +def test_rest_send_v2_00220(monkeypatch) -> None: + """ + ### Classes and Methods + - RestSend() + - commit_check_mode() + - commit() + + ### Summary + Verify ``commit_check_mode()`` sad path when + ``response_handler.commit()`` raises ``ValueError``. + + ### Setup - Code + - PARAMS["check_mode"] is set to True + - RestSend() is initialized. + - RestSend().path is set. + - RestSend().response_handler is set. + - RestSend().sender is set. + - RestSend().verb is set. + - ResponseHandler().commit() is patched to raise ``ValueError``. + + ### Setup - Data + None + + ### Trigger + - RestSend().commit() is called. + + ### Expected Result + - response_handler.commit() raises ``ValueError`` + - commit_check_mode() re-raises ``ValueError`` + - commit() re-raises ``ValueError`` + """ + params = copy.copy(PARAMS) + params["check_mode"] = True + + class MockResponseHandler: + """ + Mock ``ResponseHandler().commit()`` to raise ``ValueError``. + """ + + def __init__(self): + self._verb = "GET" + + def commit(self): + """ + Raise ``ValueError``. + """ + raise ValueError("Error in ResponseHandler.") + + @property + def implements(self): + """ + Return expected interface string. + """ + return "response_handler_v1" + + @property + def verb(self): + """ + get/set verb. + """ + return self._verb + + @verb.setter + def verb(self, value): + self._verb = value + + with does_not_raise(): + instance = RestSend(params) + instance.path = "/foo/path" + instance.response_handler = ResponseHandler() + instance.sender = Sender() + instance.verb = "POST" + + monkeypatch.setattr(instance, "response_handler", MockResponseHandler()) + match = r"RestSend\.commit:\s+" + match += r"Error during commit\.\s+" + match += r"Error details: Error in ResponseHandler\." + with pytest.raises(ValueError, match=match): + instance.commit() + + MATCH_00500 = r"RestSend\.check_mode:\s+" MATCH_00500 += r"check_mode must be a boolean\.\s+" MATCH_00500 += r"Got.*\." From 76ee63bc3738e7089d9b5f490f00dc76f2c154b2 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 11:26:19 -0700 Subject: [PATCH 166/230] sender_file.py: Add ability to simulate exceptions --- plugins/module_utils/common/sender_file.py | 58 +++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/common/sender_file.py b/plugins/module_utils/common/sender_file.py index 653cef02b..66dee1872 100644 --- a/plugins/module_utils/common/sender_file.py +++ b/plugins/module_utils/common/sender_file.py @@ -69,6 +69,9 @@ def __init__(self): self._response = None self._verb = None + self._raise_method = None + self._raise_exception = None + msg = "ENTERED Sender(): " self.log.debug(msg) @@ -93,9 +96,17 @@ def commit(self): Dummy commit ### Raises - - ```ValueError`` if ``gen`` is not set. + - ``ValueError`` if ``gen`` is not set. + - ``self.raise_exception`` if set and + ``self.raise_method`` == "commit" """ method_name = inspect.stack()[0][3] + + if self.raise_method == method_name: + msg = f"{self.class_name}.{method_name}: " + msg += f"Simulated {self.raise_exception.__name__}." + raise self.raise_exception(msg) # pylint: disable=not-callable + try: self._verify_commit_parameters() except ValueError as error: @@ -184,6 +195,51 @@ def payload(self): def payload(self, value): self._payload = value + @property + def raise_exception(self): + """ + ### Summary + The exception to raise. + + ### Raises + - ``TypeError`` if value is not a subclass of + ``BaseException``. + + ### Usage + ```python + instance = Sender() + instance.raise_method = "commit" + instance.raise_exception = ValueError + instance.commit() # will raise a simulated ValueError + ``` + + ### NOTES + - No error checking is done on the input to this property. + """ + return self._raise_exception + + @raise_exception.setter + def raise_exception(self, value): + self._raise_exception = value + + @property + def raise_method(self): + """ + ### Summary + The method in which to raise ``raise_exception``. + + ### Raises + None + + ### Usage + See ``raise_exception``. + """ + return self._raise_method + + @raise_method.setter + def raise_method(self, value): + self._raise_method = value + @property def response(self): """ From 1f9be346729f6079d3ee29d9d6ea38d43d40fb10 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 11:33:38 -0700 Subject: [PATCH 167/230] RestSend() v2: 92% unit test coverage. Added the following test cases. - test_rest_send_v2_00300 Verify ``commit_normal_mode()`` happy path when ``verb`` is "POST" and ``payload`` is set. - test_rest_send_v2_00310 Verify ``commit_normal_mode()`` sad path when ``Sender().commit()`` raises ``ValueError``. --- .../module_utils/common/test_rest_send_v2.py | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index 83ab8dc86..7ec964ce4 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -27,6 +27,7 @@ __author__ = "Allen Robel" import copy +import inspect import pytest from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ @@ -450,6 +451,139 @@ def verb(self, value): instance.commit() +def test_rest_send_v2_00300() -> None: + """ + ### Classes and Methods + - RestSend() + - commit_normal_mode() + - commit() + + ### Summary + Verify ``commit_normal_mode()`` happy path when + ``verb`` is "POST" and ``payload`` is set. + + ### Setup - Code + - PARAMS["check_mode"] is set to False + - RestSend() is initialized. + - RestSend().path is set. + - RestSend().response_handler is set. + - RestSend().sender is set. + - RestSend().verb is set. + + ### Setup - Data + None + + ### Trigger + - RestSend().commit() is called. + + ### Expected Result + - The following are updated to expected values: + - ``response`` + - ``response_current`` + - ``result`` + - ``result_current`` + - result_current["changed"] is True + """ + params = copy.copy(PARAMS) + params["check_mode"] = False + + def responses_00300(): + yield { + "METHOD": "POST", + "MESSAGE": "OK", + "REQUEST_PATH": "/foo/path", + "RETURN_CODE": 200, + "DATA": "simulated_data", + "CHECK_MODE": False, + } + + sender = Sender() + sender.gen = ResponseGenerator(responses_00300()) + with does_not_raise(): + instance = RestSend(params) + instance.path = "/foo/path" + instance.response_handler = ResponseHandler() + instance.sender = sender + instance.verb = "POST" + instance.payload = {} + instance.commit() + assert instance.response_current["CHECK_MODE"] == instance.check_mode + assert instance.response_current["DATA"] == "simulated_data" + assert instance.response_current["MESSAGE"] == "OK" + assert instance.response_current["METHOD"] == instance.verb + assert instance.response_current["REQUEST_PATH"] == instance.path + assert instance.response_current["RETURN_CODE"] == 200 + assert instance.result_current["success"] is True + assert instance.result_current["changed"] is True + assert instance.response == [instance.response_current] + assert instance.result == [instance.result_current] + + +def test_rest_send_v2_00310() -> None: + """ + ### Classes and Methods + - RestSend() + - commit_normal_mode() + - commit() + + ### Summary + Verify ``commit_normal_mode()`` sad path when + ``Sender().commit()`` raises ``ValueError``. + + ### Setup - Code + - PARAMS["check_mode"] is set to False + - RestSend() is initialized. + - RestSend().path is set. + - RestSend().response_handler is set. + - Sender().raise_method is set to "commit". + - Sender().raise_exception is set to ValueError. + - RestSend().sender is set. + - RestSend().verb is set. + + + ### Setup - Data + None + + ### Trigger + - RestSend().commit() is called. + + ### Expected Result + - Sender().commit() raises ``ValueError`` + - commit_normal_mode() re-raises ``ValueError`` + - commit() re-raises ``ValueError`` + """ + params = copy.copy(PARAMS) + params["check_mode"] = False + + def responses_00300(): + yield { + "METHOD": "POST", + "MESSAGE": "OK", + "REQUEST_PATH": "/foo/path", + "RETURN_CODE": 200, + "DATA": "simulated_data", + "CHECK_MODE": False, + } + + sender = Sender() + sender.gen = ResponseGenerator(responses_00300()) + sender.raise_method = "commit" + sender.raise_exception = ValueError + + with does_not_raise(): + instance = RestSend(params) + instance.path = "/foo/path" + instance.response_handler = ResponseHandler() + instance.sender = sender + instance.verb = "POST" + instance.payload = {} + match = r"RestSend\.commit:\s+" + match += r"Error during commit\.\s+" + match += r"Error details: Sender\.commit: Simulated ValueError\." + with pytest.raises(ValueError, match=match): + instance.commit() + + MATCH_00500 = r"RestSend\.check_mode:\s+" MATCH_00500 += r"check_mode must be a boolean\.\s+" MATCH_00500 += r"Got.*\." From b3ece9fbf63d25c0d354563f09a84539a6ed6991 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 11:50:13 -0700 Subject: [PATCH 168/230] RestSend().send_interval: Need to check for bool RestSend().send_interval: In validating the input, we need to check for bool type first. --- plugins/module_utils/common/rest_send_v2.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 773ef2dca..95821f3d9 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -684,9 +684,13 @@ def send_interval(self): @send_interval.setter def send_interval(self, value): method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be an integer. " + msg += f"Got type {type(value).__name__}, " + msg += f"value {value}\." + if isinstance(value, bool): + raise TypeError(msg) if not isinstance(value, int): - msg = f"{self.class_name}.{method_name}: " - msg += f"{method_name} must be an int(). Got {value}." raise TypeError(msg) self.properties["send_interval"] = value From ccfe2da532ef517f927edf3d3580917020fbb3f2 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 11:55:01 -0700 Subject: [PATCH 169/230] Fix invalid escape. --- plugins/module_utils/common/rest_send_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 95821f3d9..bc267f350 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -687,7 +687,7 @@ def send_interval(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be an integer. " msg += f"Got type {type(value).__name__}, " - msg += f"value {value}\." + msg += f"value {value}." if isinstance(value, bool): raise TypeError(msg) if not isinstance(value, int): From 3cbe5283c6b9f528e6209fdfad8b8df12f58d2ce Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 12:00:35 -0700 Subject: [PATCH 170/230] RestSend() v2: 93% unit test coverage. Added the following test cases: - test_rest_send_v2_01100 Verify ``send_interval.setter`` raises ``TypeError`` when set to inappropriate types, and does not raise when set to integer. --- .../module_utils/common/test_rest_send_v2.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index 7ec964ce4..a0c8a3716 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -903,3 +903,56 @@ def test_rest_send_v2_01000(value, does_raise, expected) -> None: instance.result = value if does_raise is False: assert instance.result == [value] + + +MATCH_01100 = r"RestSend\.send_interval:\s+" +MATCH_01100 += r"send_interval must be an integer\.\s+" +MATCH_01100 += r"Got type.*,\s+" +MATCH_01100 += r"value\s+.*\." + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (200, False, does_not_raise()), + ([10], True, pytest.raises(TypeError, match=MATCH_01100)), + ({10}, True, pytest.raises(TypeError, match=MATCH_01100)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_01100)), + (None, True, pytest.raises(TypeError, match=MATCH_01100)), + (False, True, pytest.raises(TypeError, match=MATCH_01100)), + (True, True, pytest.raises(TypeError, match=MATCH_01100)), + ], +) +def test_rest_send_v2_01100(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - RestSend() + - send_interval.setter + + ### Summary + Verify ``send_interval.setter`` raises ``TypeError`` + when set to inappropriate types, and does not raise + when set to integer. + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().send_interval is reset using various types. + + ### Expected Result + - ``send_interval`` raises TypeError for non-integer inputs. + - ``send_interval`` accepts integer inputs. + - ``send_interval`` returns an integer in the happy path. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + + with expected: + instance.send_interval = value + if does_raise is False: + assert isinstance(instance.send_interval, int) + assert instance.send_interval == value From 8c81d7028d7cb4231964200f8f3d025141fa597a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 14:14:43 -0700 Subject: [PATCH 171/230] Add "implements" property to several classes. Add an "implements" property to the following classes: - Response_Handler() - RestSend() v2 - Sender() (sender_dcnm.py) - Sender() (sender_file.py) --- plugins/module_utils/common/response_handler.py | 9 +++++++-- plugins/module_utils/common/rest_send_v2.py | 13 +++++++++++++ plugins/module_utils/common/sender_dcnm.py | 12 ++++++++++++ plugins/module_utils/common/sender_file.py | 12 ++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/common/response_handler.py b/plugins/module_utils/common/response_handler.py index 6795c7a94..08efc5aaf 100644 --- a/plugins/module_utils/common/response_handler.py +++ b/plugins/module_utils/common/response_handler.py @@ -93,6 +93,7 @@ class ResponseHandler: def __init__(self): self.class_name = self.__class__.__name__ method_name = inspect.stack()[0][3] + self._implements = "response_handler_v1" self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -212,9 +213,13 @@ def commit(self): @property def implements(self): """ - Return the interface this class implements. + ### Summary + The interface implemented by this class. + + ### Raises + None """ - return "response_handler_v1" + return self._implements @property def response(self): diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index bc267f350..eda469185 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -113,6 +113,8 @@ class RestSend: def __init__(self, params): self.class_name = self.__class__.__name__ + self._implements = "rest_send_v2" + self.log = logging.getLogger(f"dcnm.{self.class_name}") self.params = params @@ -466,6 +468,17 @@ def failed_result(self): """ return Results().failed_result + @property + def implements(self): + """ + ### Summary + The interface implemented by this class. + + ### Raises + None + """ + return self._implements + @property def path(self): """ diff --git a/plugins/module_utils/common/sender_dcnm.py b/plugins/module_utils/common/sender_dcnm.py index edd186c18..bc98f1841 100644 --- a/plugins/module_utils/common/sender_dcnm.py +++ b/plugins/module_utils/common/sender_dcnm.py @@ -61,6 +61,7 @@ class Sender: def __init__(self): self.class_name = self.__class__.__name__ + self._implements = "sender_v1" self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -169,6 +170,17 @@ def ansible_module(self, value): raise TypeError(msg) from error self._ansible_module = value + @property + def implements(self): + """ + ### Summary + The interface implemented by this class. + + ### Raises + None + """ + return self._implements + @property def path(self): """ diff --git a/plugins/module_utils/common/sender_file.py b/plugins/module_utils/common/sender_file.py index 66dee1872..35b804c3f 100644 --- a/plugins/module_utils/common/sender_file.py +++ b/plugins/module_utils/common/sender_file.py @@ -64,6 +64,7 @@ def __init__(self): self._ansible_module = None self._gen = None + self._implements = "sender_v1" self._path = None self._payload = None self._response = None @@ -162,6 +163,17 @@ def gen(self, value): raise TypeError(msg) self._gen = value + @property + def implements(self): + """ + ### Summary + The interface implemented by this class. + + ### Raises + None + """ + return self._implements + @property def path(self): """ From 60b2e0c793ef31a655967d7385319233340ca2cc Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 14:38:34 -0700 Subject: [PATCH 172/230] RestSend() v2: property assignment modifications RestSend() v2: remove self.properties in favor of _property. --- plugins/module_utils/common/rest_send_v2.py | 79 +++++++++---------- .../module_utils/common/test_rest_send_v2.py | 27 ++++--- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index eda469185..8a135413f 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -122,20 +122,19 @@ def __init__(self, params): msg += f"params: {self.params}" self.log.debug(msg) - self.properties = {} - self.properties["check_mode"] = False - self.properties["path"] = None - self.properties["payload"] = None - self.properties["response"] = [] - self.properties["response_current"] = {} - self.properties["response_handler"] = None - self.properties["result"] = [] - self.properties["result_current"] = {} - self.properties["send_interval"] = 5 - self.properties["sender"] = None - self.properties["timeout"] = 300 - self.properties["unit_test"] = False - self.properties["verb"] = None + self._check_mode = False + self._path = None + self._payload = None + self._response = [] + self._response_current = {} + self._response_handler = None + self._result = [] + self._result_current = {} + self._send_interval = 5 + self._sender = None + self._timeout = 300 + self._unit_test = False + self._verb = None # See save_settings() and restore_settings() self.saved_timeout = None @@ -450,7 +449,7 @@ def check_mode(self): is a read-only operation, and we want to be able to read this data to provide a real controller response to the user. """ - return self.properties.get("check_mode") + return self._check_mode @check_mode.setter def check_mode(self, value): @@ -459,7 +458,7 @@ def check_mode(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be a boolean. Got {value}." raise TypeError(msg) - self.properties["check_mode"] = value + self._check_mode = value @property def failed_result(self): @@ -490,11 +489,11 @@ def path(self): ### Example ``/appcenter/cisco/ndfc/api/v1/...etc...`` """ - return self.properties.get("path") + return self._path @path.setter def path(self, value): - self.properties["path"] = value + self._path = value @property def payload(self): @@ -504,11 +503,11 @@ def payload(self): ### Raises None """ - return self.properties["payload"] + return self._payload @payload.setter def payload(self, value): - self.properties["payload"] = value + self._payload = value @property def response_current(self): @@ -526,7 +525,7 @@ def response_current(self): ### setter Set ``response_current`` """ - return copy.deepcopy(self.properties.get("response_current")) + return copy.deepcopy(self._response_current) @response_current.setter def response_current(self, value): @@ -537,7 +536,7 @@ def response_current(self, value): msg += f"Got type {type(value).__name__}, " msg += f"Value: {value}." raise TypeError(msg) - self.properties["response_current"] = value + self._response_current = value @property def response(self): @@ -556,7 +555,7 @@ def response(self): ### setter Append value to ``response`` """ - return copy.deepcopy(self.properties.get("response")) + return copy.deepcopy(self._response) @response.setter def response(self, value): @@ -567,7 +566,7 @@ def response(self, value): msg += f"Got type {type(value).__name__}, " msg += f"Value: {value}." raise TypeError(msg) - self.properties["response"].append(value) + self._response.append(value) @property def response_handler(self): @@ -590,7 +589,7 @@ def response_handler(self): - See module_utils/common/response_handler.py for details about implementing a ``ResponseHandler`` class. """ - return self.properties.get("response_handler") + return self._response_handler @response_handler.setter def response_handler(self, value): @@ -608,7 +607,7 @@ def response_handler(self, value): raise TypeError(msg) from error if _implements_have != _implements_need: raise TypeError(msg) - self.properties["response_handler"] = value + self._response_handler = value @property def result(self): @@ -628,7 +627,7 @@ def result(self): ### setter Append value to ``result`` """ - return copy.deepcopy(self.properties.get("result")) + return copy.deepcopy(self._result) @result.setter def result(self, value): @@ -639,7 +638,7 @@ def result(self, value): msg += f"Got type {type(value).__name__}, " msg += f"Value: {value}." raise TypeError(msg) - self.properties["result"].append(value) + self._result.append(value) @property def result_current(self): @@ -660,7 +659,7 @@ def result_current(self): ### setter Set ``current_result`` """ - return copy.deepcopy(self.properties.get("result_current")) + return copy.deepcopy(self._result_current) @result_current.setter def result_current(self, value): @@ -670,7 +669,7 @@ def result_current(self, value): msg += f"{method_name} must be a dict. " msg += f"Got {value}." raise TypeError(msg) - self.properties["result_current"] = value + self._result_current = value @property def send_interval(self): @@ -692,7 +691,7 @@ def send_interval(self): ### setter Sets ``send_interval`` """ - return self.properties.get("send_interval") + return self._send_interval @send_interval.setter def send_interval(self, value): @@ -705,7 +704,7 @@ def send_interval(self, value): raise TypeError(msg) if not isinstance(value, int): raise TypeError(msg) - self.properties["send_interval"] = value + self._send_interval = value @property def sender(self): @@ -733,7 +732,7 @@ def sender(self): ### Raises - ``TypeError`` if value is not an instance of ``Sender`` """ - return self.properties.get("sender") + return self._sender @sender.setter def sender(self, value): @@ -751,7 +750,7 @@ def sender(self, value): raise TypeError(msg) from error if _class_have != _class_need: raise TypeError(msg) - self.properties["sender"] = value + self._sender = value @property def timeout(self): @@ -774,7 +773,7 @@ def timeout(self): ### setter Sets ``timeout`` """ - return self.properties.get("timeout") + return self._timeout @timeout.setter def timeout(self, value): @@ -783,7 +782,7 @@ def timeout(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be an int(). Got {value}." raise TypeError(msg) - self.properties["timeout"] = value + self._timeout = value @property def unit_test(self): @@ -804,7 +803,7 @@ def unit_test(self): ### setter Sets ``unit_test`` """ - return self.properties.get("unit_test") + return self._unit_test @unit_test.setter def unit_test(self, value): @@ -813,7 +812,7 @@ def unit_test(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"{method_name} must be a bool(). Got {value}." raise TypeError(msg) - self.properties["unit_test"] = value + self._unit_test = value @property def verb(self): @@ -826,7 +825,7 @@ def verb(self): ### Valid verbs ``GET``, ``POST``, ``PUT``, ``DELETE`` """ - return self.properties.get("verb") + return self._verb @verb.setter def verb(self, value): @@ -836,4 +835,4 @@ def verb(self, value): msg += f"{method_name} must be one of {sorted(self._valid_verbs)}. " msg += f"Got {value}." raise ValueError(msg) - self.properties["verb"] = value + self._verb = value diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index a0c8a3716..310509886 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -60,22 +60,23 @@ def test_rest_send_v2_00000() -> None: ### Summary - Verify class properties are initialized to expected values """ + # pylint: disable=use-implicit-booleaness-not-comparison with does_not_raise(): instance = RestSend(PARAMS) assert instance.params == PARAMS - assert instance.properties["check_mode"] is False - assert instance.properties["path"] is None - assert instance.properties["payload"] is None - assert instance.properties["response"] == [] - assert instance.properties["response_current"] == {} - assert instance.properties["response_handler"] is None - assert instance.properties["result"] == [] - assert instance.properties["result_current"] == {} - assert instance.properties["send_interval"] == 5 - assert instance.properties["sender"] is None - assert instance.properties["timeout"] == 300 - assert instance.properties["unit_test"] is False - assert instance.properties["verb"] is None + assert instance._check_mode is False + assert instance._path is None + assert instance._payload is None + assert instance._response == [] + assert instance._response_current == {} + assert instance._response_handler is None + assert instance._result == [] + assert instance._result_current == {} + assert instance._send_interval == 5 + assert instance._sender is None + assert instance._timeout == 300 + assert instance._unit_test is False + assert instance._verb is None assert instance.saved_check_mode is None assert instance.saved_timeout is None From b62097c487867e605a3e06a609ba3ec925e0de91 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 14:57:08 -0700 Subject: [PATCH 173/230] RestSend().commit_normal_mode(): remove unneeded try-except --- plugins/module_utils/common/rest_send_v2.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 8a135413f..edbd7f960 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -351,10 +351,7 @@ def commit_normal_mode(self): except ValueError as error: raise ValueError(error) from error - try: - timeout = self.timeout - except AttributeError: - timeout = 300 + timeout = copy.copy(self.timeout) success = False msg = f"{caller}: Entering commit loop. " From 498f1986d9643e7e3860227a9a6b384293656879 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 15:28:01 -0700 Subject: [PATCH 174/230] RestSend() v2: 95% unit test coverage. 1. RestSend(): Fix two error messages. 2. Add the following test cases. - test_rest_send_v2_00320 Verify ``commit_normal_mode()`` sad path when ``response_handler.commit()`` raises ``ValueError``. - test_rest_send_v2_01200 Verify ``failed_result.getter`` returns dictionary with expected key/values. - test_rest_send_v2_01300 Verify ``implements.getter`` returns expected string. 3. Modify the following testcase. - test_rest_send_v2_00320 Modify match to reflect change in RestSend() error message. --- plugins/module_utils/common/rest_send_v2.py | 7 +- .../module_utils/common/test_rest_send_v2.py | 151 +++++++++++++++++- 2 files changed, 155 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index edbd7f960..4429f56b2 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -322,7 +322,7 @@ def commit_check_mode(self): msg = f"{self.class_name}.{method_name}: " msg += "Error building response/result. " msg += f"Error detail: {error}" - raise ValueError(error) from error + raise ValueError(msg) from error def commit_normal_mode(self): """ @@ -380,7 +380,10 @@ def commit_normal_mode(self): self.response_handler.commit() self.result_current = self.response_handler.result except (TypeError, ValueError) as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Error building response/result. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index 310509886..6ac987939 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -447,7 +447,9 @@ def verb(self, value): monkeypatch.setattr(instance, "response_handler", MockResponseHandler()) match = r"RestSend\.commit:\s+" match += r"Error during commit\.\s+" - match += r"Error details: Error in ResponseHandler\." + match += r"Error details:\s+" + match += r"RestSend\.commit_check_mode:\s+" + match += r"Error building response\/result\." with pytest.raises(ValueError, match=match): instance.commit() @@ -585,6 +587,90 @@ def responses_00300(): instance.commit() +def test_rest_send_v2_00320(monkeypatch) -> None: + """ + ### Classes and Methods + - RestSend() + - commit_normal_mode() + - commit() + + ### Summary + Verify ``commit_normal_mode()`` sad path when + ``response_handler.commit()`` raises ``ValueError``. + + ### Setup - Code + - PARAMS["check_mode"] is set to False + - RestSend() is initialized. + - RestSend().path is set. + - RestSend().response_handler is set. + - RestSend().sender is set. + - RestSend().verb is set. + - ResponseHandler().commit() is patched to raise ``ValueError``. + + ### Setup - Data + None + + ### Trigger + - RestSend().commit() is called. + + ### Expected Result + - response_handler.commit() raises ``ValueError`` + - commit_normal_mode() re-raises ``ValueError`` + - commit() re-raises ``ValueError`` + """ + params = copy.copy(PARAMS) + params["check_mode"] = False + + class MockResponseHandler: + """ + Mock ``ResponseHandler().commit()`` to raise ``ValueError``. + """ + + def __init__(self): + self._verb = "GET" + + def commit(self): + """ + Raise ``ValueError``. + """ + raise ValueError("Error in ResponseHandler.") + + @property + def implements(self): + """ + Return expected interface string. + """ + return "response_handler_v1" + + @property + def verb(self): + """ + get/set verb. + """ + return self._verb + + @verb.setter + def verb(self, value): + self._verb = value + + with does_not_raise(): + instance = RestSend(params) + instance.path = "/foo/path" + instance.response_handler = ResponseHandler() + instance.sender = Sender() + instance.sender.gen = ResponseGenerator(responses()) + instance.verb = "POST" + + monkeypatch.setattr(instance, "response_handler", MockResponseHandler()) + match = r"RestSend\.commit:\s+" + match += r"Error during commit\.\s+" + match += r"Error details:\s+" + match += r"RestSend\.commit_normal_mode:\s+" + match += r"Error building response\/result\." + with pytest.raises(ValueError, match=match): + instance.commit() + + MATCH_00500 = r"RestSend\.check_mode:\s+" MATCH_00500 += r"check_mode must be a boolean\.\s+" MATCH_00500 += r"Got.*\." @@ -957,3 +1043,66 @@ def test_rest_send_v2_01100(value, does_raise, expected) -> None: if does_raise is False: assert isinstance(instance.send_interval, int) assert instance.send_interval == value + + +def test_rest_send_v2_01200() -> None: + """ + ### Classes and Methods + - RestSend() + - failed_result.getter + + ### Summary + Verify ``failed_result.getter`` returns dictionary with + expected key/values. + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().failed_result accessed. + + ### Expected Result + - ``failed_result`` returns dictionary with expected key/values. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + failed_result = instance.failed_result + + assert isinstance(failed_result, dict) + assert failed_result == { + "changed": False, + "failed": True, + "diff": [{}], + "response": [{}], + "result": [{}], + } + + +def test_rest_send_v2_01300() -> None: + """ + ### Classes and Methods + - RestSend() + - implements.getter + + ### Summary + Verify ``implements.getter`` returns expected string. + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().implements accessed. + + ### Expected Result + - ``implements`` returns string with expected value. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + implements = instance.implements + assert implements == "rest_send_v2" From 4504d15d143622937d9a4df6c5386a145a33297b Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 16:30:25 -0700 Subject: [PATCH 175/230] RestSend() remove unneeded method RestSend()._strip_invalid_json_from_response_data() didn't work and wasn't all that useful. Removing it for now. --- plugins/module_utils/common/rest_send_v2.py | 20 ------------------- .../module_utils/common/test_rest_send_v2.py | 1 - 2 files changed, 21 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 4429f56b2..06aa78272 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -22,7 +22,6 @@ import inspect import json import logging -import re from time import sleep # Using only for its failed_result property @@ -395,9 +394,6 @@ def commit_normal_mode(self): sleep(self.send_interval) timeout -= self.send_interval - self.response_current = self._strip_invalid_json_from_response_data( - self.response_current - ) msg = f"{self.class_name}.{method_name}: " msg += f"caller: {caller}. " msg += "response_current: " @@ -407,22 +403,6 @@ def commit_normal_mode(self): self.response = copy.deepcopy(self.response_current) self.result = copy.deepcopy(self.result_current) - @staticmethod - def _strip_invalid_json_from_response_data(response: dict) -> dict: - """ - ### Summary - Strip "Invalid JSON response:" from response["DATA"] if present - - This string in the response clutters up the output and is not - useful to the user. - """ - if "DATA" not in response: - return response - if not isinstance(response["DATA"], str): - return response - response["DATA"] = re.sub(r"Invalid JSON response:\s*", "", response["DATA"]) - return response - @property def check_mode(self): """ diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index 6ac987939..427362ddc 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -27,7 +27,6 @@ __author__ = "Allen Robel" import copy -import inspect import pytest from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ From 81bf5312e757818b482c087e53b8ef01af112c92 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 17:37:38 -0700 Subject: [PATCH 176/230] RestSend() v2: 96% unit test coverage. 1. RestSend().sender: Validate based on "implements" property. 2. Add the following test cases. - test_rest_send_v2_01400 - Verify ``sender.setter`` raises ``TypeError`` when set to anything other than a class that implements sender_v1. - Verify that ``sender.getter`` returns Sender() class when properly set. --- plugins/module_utils/common/rest_send_v2.py | 13 ++--- .../module_utils/common/test_rest_send_v2.py | 52 +++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 06aa78272..72ea7c449 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -717,18 +717,19 @@ def sender(self): @sender.setter def sender(self, value): method_name = inspect.stack()[0][3] - _class_have = None - _class_need = "Sender" + _implements_have = None + _implements_need = "sender_v1" msg = f"{self.class_name}.{method_name}: " - msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." + msg += f"value must be a class that implements {_implements_need}. " + msg += f"Got type {type(value).__name__}, " + msg += f"value {value}. " try: - _class_have = value.class_name + _implements_have = value.implements except AttributeError as error: msg += f"Error detail: {error}." raise TypeError(msg) from error - if _class_have != _class_need: + if _implements_have != _implements_need: raise TypeError(msg) self._sender = value diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index 427362ddc..03798a611 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -1105,3 +1105,55 @@ def test_rest_send_v2_01300() -> None: instance = RestSend(PARAMS) implements = instance.implements assert implements == "rest_send_v2" + + +MATCH_01400 = r"RestSend.sender:\s+" +MATCH_01400 += r"value must be a class that implements sender_v1\.\s+" +MATCH_01400 += r"Got type .*, value .*\.\s+" +MATCH_01400_A = rf"{MATCH_01400}Error detail:.*" +MATCH_01400_B = MATCH_01400 + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (10, True, pytest.raises(TypeError, match=MATCH_01400_A)), + (True, True, pytest.raises(TypeError, match=MATCH_01400_A)), + (False, True, pytest.raises(TypeError, match=MATCH_01400_A)), + ([10], True, pytest.raises(TypeError, match=MATCH_01400_A)), + ({10}, True, pytest.raises(TypeError, match=MATCH_01400_A)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_01400_A)), + (ResponseHandler(), True, pytest.raises(TypeError, match=MATCH_01400_B)), + (Sender(), False, does_not_raise()), + ], +) +def test_rest_send_v2_01400(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - RestSend() + - sender + + ### Summary + - Verify ``sender.setter`` raises ``TypeError`` when set to + anything other than a class that implements sender_v1. + - Verify that ``sender.getter`` returns Sender() class when + properly set. + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().sender is set to various values. + + ### Expected Result + - ``sender.setter`` raises ``TypeError`` when expected. + - ``sender.getter`` returns Sender() class if set properly. + """ + with expected: + instance = RestSend(PARAMS) + instance.sender = value + if not does_raise: + assert instance.sender.implements == "sender_v1" From bb2d513fac676cb2608d8130b6ed188dcd2a56ae Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 18:15:57 -0700 Subject: [PATCH 177/230] RestSend() v2: 99% unit test coverage. 1. RestSend(): Tweak validations for the following properties: - timeout - unit_test - verb 2. Add the following test cases: - test_rest_send_v2_01500 Verify ``timeout.setter`` raises ``TypeError`` when set to inappropriate types, and does not raise when set to integer. - test_rest_send_v2_01600 Verify ``unit_test.setter`` raises ``TypeError`` when set to inappropriate types, and does not raise when set to boolean. - test_rest_send_v2_01700 - Verify ``verb.setter`` raises ``TypeError`` when set to non-string types. - Verify ``verb.setter`` raises ``ValueError`` when set to inappropriate values. - Verify that ``verb.setter`` does not raise when set to one of "DELETE", "GET", "POST", or "PUT". --- plugins/module_utils/common/rest_send_v2.py | 21 ++- .../module_utils/common/test_rest_send_v2.py | 165 ++++++++++++++++++ 2 files changed, 180 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/common/rest_send_v2.py b/plugins/module_utils/common/rest_send_v2.py index 72ea7c449..19404230f 100644 --- a/plugins/module_utils/common/rest_send_v2.py +++ b/plugins/module_utils/common/rest_send_v2.py @@ -759,9 +759,13 @@ def timeout(self): @timeout.setter def timeout(self, value): method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be an integer. " + msg += f"Got type {type(value).__name__}, " + msg += f"value {value}." + if isinstance(value, bool): + raise TypeError(msg) if not isinstance(value, int): - msg = f"{self.class_name}.{method_name}: " - msg += f"{method_name} must be an int(). Got {value}." raise TypeError(msg) self._timeout = value @@ -791,7 +795,9 @@ 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}." + msg += f"{method_name} must be a boolean. " + msg += f"Got type {type(value).__name__}, " + msg += f"value {value}." raise TypeError(msg) self._unit_test = value @@ -801,6 +807,7 @@ def verb(self): Verb for the REST request. ### Raises + - setter: ``TypeError`` if value is not a string. - setter: ``ValueError`` if value is not a valid verb. ### Valid verbs @@ -811,9 +818,11 @@ def verb(self): @verb.setter def verb(self, value): method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must be one of {sorted(self._valid_verbs)}. " + msg += f"Got {value}." + if not isinstance(value, str): + raise TypeError(msg) if value not in self._valid_verbs: - msg = f"{self.class_name}.{method_name}: " - msg += f"{method_name} must be one of {sorted(self._valid_verbs)}. " - msg += f"Got {value}." raise ValueError(msg) self._verb = value diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index 03798a611..53036f42e 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -1157,3 +1157,168 @@ def test_rest_send_v2_01400(value, does_raise, expected) -> None: instance.sender = value if not does_raise: assert instance.sender.implements == "sender_v1" + + +MATCH_01500 = r"RestSend\.timeout:\s+" +MATCH_01500 += r"timeout must be an integer\.\s+" +MATCH_01500 += r"Got type.*,\s+" +MATCH_01500 += r"value\s+.*\." + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (200, False, does_not_raise()), + ([10], True, pytest.raises(TypeError, match=MATCH_01500)), + ({10}, True, pytest.raises(TypeError, match=MATCH_01500)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_01500)), + (None, True, pytest.raises(TypeError, match=MATCH_01500)), + (False, True, pytest.raises(TypeError, match=MATCH_01500)), + (True, True, pytest.raises(TypeError, match=MATCH_01500)), + ], +) +def test_rest_send_v2_01500(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - RestSend() + - timeout.setter + + ### Summary + Verify ``timeout.setter`` raises ``TypeError`` + when set to inappropriate types, and does not raise + when set to integer. + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().timeout is reset using various types. + + ### Expected Result + - ``timeout`` raises TypeError for non-integer inputs. + - ``timeout`` accepts integer inputs. + - ``timeout`` returns an integer in the happy path. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + + with expected: + instance.timeout = value + if does_raise is False: + assert isinstance(instance.timeout, int) + assert instance.timeout == value + + +MATCH_01600 = r"RestSend\.unit_test:\s+" +MATCH_01600 += r"unit_test must be a boolean\.\s+" +MATCH_01600 += r"Got type.*,\s+" +MATCH_01600 += r"value\s+.*\." + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + (False, False, does_not_raise()), + (True, False, does_not_raise()), + (200, True, pytest.raises(TypeError, match=MATCH_01600)), + ([10], True, pytest.raises(TypeError, match=MATCH_01600)), + ({10}, True, pytest.raises(TypeError, match=MATCH_01600)), + ("FOO", True, pytest.raises(TypeError, match=MATCH_01600)), + (None, True, pytest.raises(TypeError, match=MATCH_01600)), + ], +) +def test_rest_send_v2_01600(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - RestSend() + - unit_test.setter + + ### Summary + Verify ``unit_test.setter`` raises ``TypeError`` + when set to inappropriate types, and does not raise + when set to boolean. + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().unit_test is reset using various types. + + ### Expected Result + - ``unit_test`` raises TypeError for non-boolean inputs. + - ``unit_test`` accepts boolean inputs. + - ``unit_test`` returns a boolean in the happy path. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + + with expected: + instance.unit_test = value + if does_raise is False: + assert isinstance(instance.unit_test, bool) + assert instance.unit_test == value + + +MATCH_01700 = r"RestSend\.verb:\s+" +MATCH_01700 += r"verb must be one of\s+" +MATCH_01700 += r"\['DELETE', 'GET', 'POST', 'PUT'\]\.\s+" +MATCH_01700 += r"Got.*\." + + +@pytest.mark.parametrize( + "value, does_raise, expected", + [ + ("DELETE", False, does_not_raise()), + ("GET", False, does_not_raise()), + ("POST", False, does_not_raise()), + ("PUT", False, does_not_raise()), + ("FOO", True, pytest.raises(ValueError, match=MATCH_01700)), + (200, True, pytest.raises(TypeError, match=MATCH_01700)), + ([10], True, pytest.raises(TypeError, match=MATCH_01700)), + ({10}, True, pytest.raises(TypeError, match=MATCH_01700)), + (None, True, pytest.raises(TypeError, match=MATCH_01700)), + ], +) +def test_rest_send_v2_01700(value, does_raise, expected) -> None: + """ + ### Classes and Methods + - RestSend() + - verb.setter + + ### Summary + - Verify ``verb.setter`` raises ``TypeError`` + when set to non-string types. + - Verify ``verb.setter`` raises ``ValueError`` + when set to inappropriate values. + - Verify that ``verb.setter`` does not raise + when set to one of "DELETE", "GET", "POST", or "PUT". + + ### Setup - Code + - RestSend() is initialized. + + ### Setup - Data + None + + ### Trigger + - RestSend().verb is reset using various values. + + ### Expected Result + - ``verb`` raises TypeError for invalid types. + - ``verb`` raises ValueError for invalid values. + - ``verb`` accepts valid inputs. + - ``verb`` returns valid input in the happy path. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + + with expected: + instance.verb = value + if does_raise is False: + assert isinstance(instance.verb, str) + assert instance.verb == value From fafffbf3c3bccb5e2f18ece4441ae6b1e8935ff1 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 13 Jun 2024 22:52:48 -0700 Subject: [PATCH 178/230] SwitchDetails(): 75% unit test coverage. Added the following test cases: - test_switch_details_00000 Verify class properties are initialized to expected values - test_switch_details_00100 Verify ``validate_refresh_parameters()`` raises ``ValueError`` due to ``rest_send`` not being set. - test_switch_details_00110 Verify ``validate_refresh_parameters()`` raises ``ValueError`` due to ``results`` not being set. --- .../common/test_switch_details.py | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/unit/module_utils/common/test_switch_details.py diff --git a/tests/unit/module_utils/common/test_switch_details.py b/tests/unit/module_utils/common/test_switch_details.py new file mode 100644 index 000000000..81bc0303b --- /dev/null +++ b/tests/unit/module_utils/common/test_switch_details.py @@ -0,0 +1,161 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.inventory.inventory import \ + EpAllSwitches +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ + SwitchDetails +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( + ResponseGenerator, does_not_raise) + +PARAMS = {"state": "merged", "check_mode": False} + + +def responses(): + """ + Dummy coroutine for ResponseGenerator() + + See e.g. test_switch_details_00800 + """ + yield {} + + +def test_switch_details_00000() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - __init__() + + ### Summary + - Verify class properties are initialized to expected values + """ + with does_not_raise(): + instance = SwitchDetails() + assert instance.action == "switch_details" + assert instance.class_name == "SwitchDetails" + assert isinstance(instance.conversion, ConversionUtils) + assert isinstance(instance.ep_all_switches, EpAllSwitches) + assert instance.path == EpAllSwitches().path + assert instance.verb == EpAllSwitches().verb + assert instance._filter is None + assert instance._info is None + assert instance._rest_send is None + assert instance._results is None + + +def test_switch_details_00100() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - validate_refresh_parameters() + - commit() + + ### Summary + Verify ``validate_refresh_parameters()`` raises ``ValueError`` + due to ``rest_send`` not being set. + + ### Setup - Code + - SwitchDetails() is initialized. + - SwitchDetails().rest_send is NOT set. + - SwitchDetails().results is set. + + ### Setup - Data + None + + ### Trigger + - SwitchDetails().refresh() is called. + + ### Expected Result + - SwitchDetails().validate_refresh_parameters() raises ``ValueError``. + - SwitchDetails().refresh() catches and re-raises ``ValueError``. + """ + with does_not_raise(): + instance = SwitchDetails() + instance.results = Results() + + match = r"SwitchDetails\.refresh:\s+" + match += r"Mandatory parameters need review\.\s+" + match += r"Error detail:\s+" + match += r"SwitchDetails\.validate_refresh_parameters:\s+" + match += r"SwitchDetails\.rest_send must be set before calling\s+" + match += r"SwitchDetails\.refresh\(\)\." + with pytest.raises(ValueError, match=match): + instance.refresh() + + +def test_switch_details_00110() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - validate_refresh_parameters() + - commit() + + ### Summary + Verify ``validate_refresh_parameters()`` raises ``ValueError`` + due to ``results`` not being set. + + ### Setup - Code + - SwitchDetails() is initialized. + - SwitchDetails().rest_send is set. + - SwitchDetails().results is NOT set. + + ### Setup - Data + None + + ### Trigger + - SwitchDetails().refresh() is called. + + ### Expected Result + - SwitchDetails().validate_refresh_parameters() raises ``ValueError``. + - SwitchDetails().refresh() catches and re-raises ``ValueError``. + """ + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = RestSend(PARAMS) + + match = r"SwitchDetails\.refresh:\s+" + match += r"Mandatory parameters need review\.\s+" + match += r"Error detail:\s+" + match += r"SwitchDetails\.validate_refresh_parameters:\s+" + match += r"SwitchDetails\.results must be set before calling\s+" + match += r"SwitchDetails\.refresh\(\)\." + with pytest.raises(ValueError, match=match): + instance.refresh() From 9cb64dcb57b9a035d05de4b824bc60e2ea78883f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 14 Jun 2024 10:12:56 -0700 Subject: [PATCH 179/230] SwitchDetails(): Fix potential KeyError on non-200 response 1. SwitchDetails().refresh(): For non-200 responses, if DATA is empty, a KeyError would be thrown when trying to access "ipAddress" for each switch. Fixed by testing for "ipAddress" existence before access. 2. SwitchDetails(): updated docstring Usage section. --- plugins/module_utils/common/switch_details.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 1d68222b0..7a4d54baf 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -50,11 +50,19 @@ class SwitchDetails: property values, etc. ### Usage + - Where ``ansible_module`` is an instance of ``AnsibleModule`` + ```python + # params could also be set to ansible_module.params + params = {"state": "merged", "check_mode": False} + sender = Sender() + sender.ansible_module = ansible_module + rest_send = RestSend(params) + rest_send.sender = sender try: instance = SwitchDetails() instance.results = Results() - instance.rest_send = RestSend(ansible_module) + instance.rest_send = rest_send instance.refresh() except (ControllerResponseError, ValueError) as error: # Handle error @@ -199,6 +207,8 @@ def refresh(self): data = self.results.response_current.get("DATA") self._info = {} for switch in data: + if switch.get("ipAddress", None) is None: + continue self._info[switch["ipAddress"]] = switch def _get(self, item): From a8989855ad8b8a96fdde136c764c1c8f98337fc0 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 14 Jun 2024 10:26:48 -0700 Subject: [PATCH 180/230] SwitchDetails(): 85% unit test coverage. Added the following test cases: - test_switch_details_00200 Verify ``refresh()`` happy path. - test_switch_details_00300 Verify ``refresh()`` sad path where 500 response is returned. --- .../fixtures/responses_SwitchDetails.json | 66 +++++++ .../common/test_switch_details.py | 176 ++++++++++++++++-- 2 files changed, 230 insertions(+), 12 deletions(-) diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json index 278719b17..d7c99866e 100644 --- a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -443,5 +443,71 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 + }, + "test_switch_details_00200a": { + "TEST_NOTES": [ + "DATA contains two switches", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "hostName": null, + "ipAddress": "192.168.1.2", + "isNonNexus": false, + "logicalName": "cvd-1314-leaf", + "model": "N9K-C93180YC-EX", + "operStatus": "Minor", + "managable": true, + "mode": "Normal", + "release": "10.2(5)", + "serialNumber": "FDO123456FV", + "sourceInterface": "mgmt0", + "sourceVrf": "management", + "status": "ok", + "switchDbID": 123456, + "switchRole": "leaf", + "swUUID":"DCNM-UUID-7654321", + "swUUIDId": 7654321, + "systemMode": "Maintenance" + }, + { + "fabricName": "LAN_Classic_Fabric", + "hostName": null, + "ipAddress": "192.168.2.2", + "isNonNexus": false, + "logicalName": "cvd-2314-spine", + "model": "N9K-C93180YC-FX", + "operStatus": "Major", + "managable": false, + "mode": "Normal", + "release": "10.2(4)", + "serialNumber": "FD6543210FV", + "sourceInterface": "Ethernet1/1", + "sourceVrf": "default", + "status": "ok", + "switchDbID": 654321, + "switchRole": "spine", + "swUUID":"DCNM-UUID-1234567", + "swUUIDId": 1234567, + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_switch_details_00300a": { + "TEST_NOTES": [ + "RETURN_CODE: 500", + "MESSAGE: Internal server error" + ], + "DATA": [{}], + "MESSAGE": "Internal server error", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 500 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_switch_details.py b/tests/unit/module_utils/common/test_switch_details.py index 81bc0303b..b42186368 100644 --- a/tests/unit/module_utils/common/test_switch_details.py +++ b/tests/unit/module_utils/common/test_switch_details.py @@ -27,6 +27,7 @@ __author__ = "Allen Robel" import copy +import inspect import pytest from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.inventory.inventory import \ @@ -44,20 +45,11 @@ from ansible_collections.cisco.dcnm.plugins.module_utils.common.switch_details import \ SwitchDetails from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( - ResponseGenerator, does_not_raise) + ResponseGenerator, does_not_raise, responses_switch_details) PARAMS = {"state": "merged", "check_mode": False} -def responses(): - """ - Dummy coroutine for ResponseGenerator() - - See e.g. test_switch_details_00800 - """ - yield {} - - def test_switch_details_00000() -> None: """ ### Classes and Methods @@ -86,7 +78,7 @@ def test_switch_details_00100() -> None: ### Classes and Methods - SwitchDetails() - validate_refresh_parameters() - - commit() + - refresh() ### Summary Verify ``validate_refresh_parameters()`` raises ``ValueError`` @@ -126,7 +118,7 @@ def test_switch_details_00110() -> None: ### Classes and Methods - SwitchDetails() - validate_refresh_parameters() - - commit() + - refresh() ### Summary Verify ``validate_refresh_parameters()`` raises ``ValueError`` @@ -159,3 +151,163 @@ def test_switch_details_00110() -> None: match += r"SwitchDetails\.refresh\(\)\." with pytest.raises(ValueError, match=match): instance.refresh() + + +def test_switch_details_00200() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - validate_refresh_parameters() + - refresh() + + ### Summary + Verify ``refresh()`` happy path. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - SwitchDetails() is initialized and configured. + + ### Setup - Data + responses_switch_details() returns a response with two switches. + + ### Trigger + - SwitchDetails().refresh() is called. + + ### Expected Result + - Results() contains the expected data. + """ + + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + # pylint: disable=unsupported-membership-test + assert False in instance.results.changed + assert False in instance.results.failed + # pylint: enable=unsupported-membership-test + assert instance.results.action == "switch_details" + assert instance.results.response_current["MESSAGE"] == "OK" + assert instance.results.response_current["RETURN_CODE"] == 200 + assert instance.results.response_current["DATA"][0]["ipAddress"] == "192.168.1.2" + assert instance.results.response_current["DATA"][1]["ipAddress"] == "192.168.2.2" + assert "192.168.1.2" in instance.info + assert "192.168.2.2" in instance.info + instance.filter = "192.168.1.2" + assert instance.fabric_name == "VXLAN_Fabric" + assert instance.hostname is None + assert instance.is_non_nexus is False + assert instance.logical_name == "cvd-1314-leaf" + assert instance.managable is True + assert instance.mode == "normal" + assert instance.model == "N9K-C93180YC-EX" + assert instance.oper_status == "Minor" + assert instance.platform == "N9K" + assert instance.release == "10.2(5)" + assert instance.role == "leaf" + assert instance.serial_number == "FDO123456FV" + assert instance.source_interface == "mgmt0" + assert instance.source_vrf == "management" + assert instance.status == "ok" + assert instance.switch_db_id == 123456 + assert instance.switch_role == "leaf" + assert instance.switch_uuid == "DCNM-UUID-7654321" + assert instance.switch_uuid_id == 7654321 + assert instance.system_mode == "Maintenance" + instance.filter = "192.168.2.2" + assert instance.fabric_name == "LAN_Classic_Fabric" + assert instance.hostname is None + assert instance.is_non_nexus is False + assert instance.logical_name == "cvd-2314-spine" + assert instance.managable is False + assert instance.mode == "normal" + assert instance.model == "N9K-C93180YC-FX" + assert instance.oper_status == "Major" + assert instance.platform == "N9K" + assert instance.release == "10.2(4)" + assert instance.role == "spine" + assert instance.serial_number == "FD6543210FV" + assert instance.source_interface == "Ethernet1/1" + assert instance.source_vrf == "default" + assert instance.status == "ok" + assert instance.switch_db_id == 654321 + assert instance.switch_role == "spine" + assert instance.switch_uuid == "DCNM-UUID-1234567" + assert instance.switch_uuid_id == 1234567 + assert instance.system_mode == "Normal" + + +def test_switch_details_00300() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - validate_refresh_parameters() + - refresh() + + ### Summary + Verify ``refresh()`` sad path where 500 response is returned. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - SwitchDetails() is initialized and configured. + + ### Setup - Data + responses_switch_details() returns a response with: + - RETURN_CODE: 500 + - MESSAGE: "Internal Server Error". + + ### Trigger + - SwitchDetails().refresh() is called. + + ### Expected Result + - Results() contains the expected data. + """ + + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + # pylint: disable=unsupported-membership-test + assert False in instance.results.changed + assert True in instance.results.failed + # pylint: enable=unsupported-membership-test + assert instance.results.result_current["sequence_number"] == 1 + assert instance.results.result_current["found"] is False + assert instance.results.result_current["success"] is False + assert instance.results.diff_current["sequence_number"] == 1 + assert instance.results.response_current["MESSAGE"] == "Internal server error" + assert instance.results.response_current["RETURN_CODE"] == 500 + assert instance.results.response == [instance.results.response_current] + assert instance.results.result == [instance.results.result_current] + assert instance.results.diff == [instance.results.diff_current] From d3efe3e0442ba1df32a25590db844eca2367b781 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 14 Jun 2024 14:47:37 -0700 Subject: [PATCH 181/230] SwitchDetails(): 91% unit test coverage. 1. SwitchDetails().update_results(): Fix conditional. results.failed is a set(), and so the conditional needed to be changed to test set() membership for True. This was causing ControllerResponseError() not to be raised in cases where it should be raised. 2. Add the following unit tests. - test_switch_details_00400 Verify ``refresh()`` catches ``ValueError`` raised by ``send_request()`` when ``Sender()`` is configured to raise ``ValueError``. 3. Modify the following unit tests. - test_switch_details_00300 Modify the expected error message. --- plugins/module_utils/common/switch_details.py | 11 +-- .../common/test_switch_details.py | 68 +++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 7a4d54baf..0ac0829db 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -163,7 +163,7 @@ def update_results(self) -> None: except TypeError as error: raise ValueError(error) from error - if self.results.failed is True: + if True in self.results.failed: msg = f"{self.class_name}.{method_name}: " msg += "Unable to retrieve switch information from the controller. " msg += f"Got response {self.results.response_current}" @@ -175,12 +175,12 @@ def refresh(self): the controller. ### Raises - - ``ControllerResponseError`` if: - - The controller RETURN_CODE is not 200. - ``ValueError`` if - Mandatory parameters are not set. - There was an error configuring RestSend() e.g. invalid property values, etc. + - There is an error sending the request to the controller. + - There is an error updatingcontroller results. """ method_name = inspect.stack()[0][3] try: @@ -202,7 +202,10 @@ def refresh(self): try: self.update_results() except ControllerResponseError as error: - raise ControllerResponseError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Error updating results. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error data = self.results.response_current.get("DATA") self._info = {} diff --git a/tests/unit/module_utils/common/test_switch_details.py b/tests/unit/module_utils/common/test_switch_details.py index b42186368..6c21f2c5f 100644 --- a/tests/unit/module_utils/common/test_switch_details.py +++ b/tests/unit/module_utils/common/test_switch_details.py @@ -34,6 +34,8 @@ EpAllSwitches from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ ResponseHandler from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ @@ -297,6 +299,12 @@ def responses(): instance = SwitchDetails() instance.rest_send = rest_send instance.results = Results() + match = r"SwitchDetails\.refresh:\s+" + match += r"Error updating results\.\s+" + match += r"Error detail: SwitchDetails\.update_results:\s+" + match += r"Unable to retrieve switch information from the controller\.\s+" + match += r"Got response.*" + with pytest.raises(ValueError, match=match): instance.refresh() # pylint: disable=unsupported-membership-test assert False in instance.results.changed @@ -311,3 +319,63 @@ def responses(): assert instance.results.response == [instance.results.response_current] assert instance.results.result == [instance.results.result_current] assert instance.results.diff == [instance.results.diff_current] + + +def test_switch_details_00400() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - send_request() + - refresh() + + ### Summary + Verify ``refresh()`` catches ``ValueError`` raised by + ``send_request()`` when ``Sender()`` is configured to raise + ``ValueError``. + + ### Setup - Code + - Sender() is initialized and configured to raise ``ValueError``. + in ``commit()``. + - RestSend() is initialized and configured. + - SwitchDetails() is initialized and configured. + + ### Setup - Data + responses_switch_details() returns a response with: + - RETURN_CODE: 500 + - MESSAGE: "Internal Server Error". + + ### Trigger + - SwitchDetails().refresh() is called. + + ### Expected Result + - ``refresh`` re-raises ``ValueError``. + """ + + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + sender.raise_exception = ValueError + sender.raise_method = "commit" + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = rest_send + instance.results = Results() + match = r"SwitchDetails\.refresh:\s+" + match += r"Error sending request to the controller\.\s+" + match += r"Error detail: RestSend\.commit:\s+" + match += r"Error during commit\.\s+" + match += r"Error details: Sender\.commit:\s+" + match += r"Simulated ValueError\." + with pytest.raises(ValueError, match=match): + instance.refresh() From c7b62ecf78065b3e89bbae4c7eb61d1a2b08879d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 14 Jun 2024 15:57:35 -0700 Subject: [PATCH 182/230] SwitchDetails(): 93% unit test coverage. Added the following test cases. - test_switch_details_00500 Verify ``_get()`` raises ``ValueError`` if ``filter`` is not set before accessing properties that use ``_get()``. --- .../common/test_switch_details.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/unit/module_utils/common/test_switch_details.py b/tests/unit/module_utils/common/test_switch_details.py index 6c21f2c5f..4a5220541 100644 --- a/tests/unit/module_utils/common/test_switch_details.py +++ b/tests/unit/module_utils/common/test_switch_details.py @@ -379,3 +379,35 @@ def responses(): match += r"Simulated ValueError\." with pytest.raises(ValueError, match=match): instance.refresh() + + +def test_switch_details_00500() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - _get() + - logical_name.getter + + ### Summary + Verify ``_get()`` raises ``ValueError`` if ``filter`` is not + set before accessing properties that use ``_get()``. + + ### Setup - Code + - SwitchDetails() is instantiated. + - SwitchDetails().filter is NOT set. + + ### Setup - Data + None + + ### Trigger + - SwitchDetails().logical_name is accessed. + + ### Expected Result + - ``_get()`` raises ``ValueError``. + """ + with does_not_raise(): + instance = SwitchDetails() + match = r"SwitchDetails\._get:\s+" + match += r"set instance\.filter before accessing property logicalName\." + with pytest.raises(ValueError, match=match): + instance.logical_name From c87ce87c2f91d7933e981d367f7b605c83dc12c3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 14 Jun 2024 19:56:18 -0700 Subject: [PATCH 183/230] SwitchDetails(): 94% unit test coverage. 1. SwitchDetails().update_results(): update error message to give better visibility into its origin. 2. Rename the following testcase: test_switch_details_00500 -> test_switch_details_00600 2. Add the following testcases: - test_switch_details_00500 Verify ``refresh()`` catches and re-raises ``ValueError`` raised by ``update_results()``. 3. RestSend() v2: run thorugh black and isort. --- plugins/module_utils/common/switch_details.py | 7 +- .../fixtures/responses_SwitchDetails.json | 22 +++++ .../module_utils/common/test_rest_send_v2.py | 19 ++-- .../common/test_switch_details.py | 86 ++++++++++++++++++- 4 files changed, 123 insertions(+), 11 deletions(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index 0ac0829db..cd18ab543 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -161,7 +161,10 @@ def update_results(self) -> None: self.results.failed = True self.results.register_task_result() except TypeError as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Error updating results. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error if True in self.results.failed: msg = f"{self.class_name}.{method_name}: " @@ -201,7 +204,7 @@ def refresh(self): try: self.update_results() - except ControllerResponseError as error: + except (ControllerResponseError, ValueError) as error: msg = f"{self.class_name}.{method_name}: " msg += "Error updating results. " msg += f"Error detail: {error}" diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json index d7c99866e..3bb9b7c0d 100644 --- a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -509,5 +509,27 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 500 + }, + "test_switch_details_00500a": { + "TEST_NOTES": [ + "DATA[0] contains valid content", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "mode": "Normal", + "serialNumber": "FDO123456FV", + "switchRole": "leaf", + "systemMode": "Maintenance" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_rest_send_v2.py b/tests/unit/module_utils/common/test_rest_send_v2.py index 53036f42e..308bea088 100644 --- a/tests/unit/module_utils/common/test_rest_send_v2.py +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -29,14 +29,19 @@ import copy import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ - ResponseHandler -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ - RestSend -from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ - Sender +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import ( + ResponseHandler, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import ( + RestSend, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import ( + Sender, +) from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( - ResponseGenerator, does_not_raise) + ResponseGenerator, + does_not_raise, +) PARAMS = {"state": "merged", "check_mode": False} diff --git a/tests/unit/module_utils/common/test_switch_details.py b/tests/unit/module_utils/common/test_switch_details.py index 4a5220541..196cfad13 100644 --- a/tests/unit/module_utils/common/test_switch_details.py +++ b/tests/unit/module_utils/common/test_switch_details.py @@ -381,7 +381,89 @@ def responses(): instance.refresh() -def test_switch_details_00500() -> None: +def test_switch_details_00500(monkeypatch) -> None: + """ + ### Classes and Methods + - SwitchDetails() + - update_results() + - refresh() + + ### Summary + Verify ``refresh()`` catches and re-raises ``ValueError`` + raised by ``update_results()``. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - SwitchDetails() is initialized and configured. + - Results() is mocked to raise ``TypeError`` in + ``action.setter``. + + ### Setup - Data + responses_switch_details() returns a response with: + - RETURN_CODE: 200 + - MESSAGE: "OK". + + ### Trigger + - SwitchDetails().refresh() is called. + + ### Expected Result + - ``update_results`` re-raises ``TypeError`` + as ``ValueError``. + - ``refresh`` re-raises ``ValueError``. + """ + + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + # pylint: disable=too-few-public-methods + class MockResults: + """ + mock + """ + + def __init__(self): + self.class_name = "Results" + self._action = None + + @property + def action(self): + """ + mock + """ + return self._action + + @action.setter + def action(self, value): + self._action = value + raise TypeError("Results().action: simulated TypeError.") + + def responses(): + yield responses_switch_details(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = rest_send + instance.results = Results() + + monkeypatch.setattr(instance, "results", MockResults()) + match = r"SwitchDetails\.update_results:\s+" + match += r"Error updating results\.\s+" + match += r"Error detail: Results\(\)\.action:\s+" + match += r"simulated TypeError\." + with pytest.raises(ValueError, match=match): + instance.refresh() + + +def test_switch_details_00600() -> None: """ ### Classes and Methods - SwitchDetails() @@ -410,4 +492,4 @@ def test_switch_details_00500() -> None: match = r"SwitchDetails\._get:\s+" match += r"set instance\.filter before accessing property logicalName\." with pytest.raises(ValueError, match=match): - instance.logical_name + instance.logical_name # pylint: disable=pointless-statement From 04670adb77df3895aad15d8db4fdf7a529316c7a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 14 Jun 2024 21:32:20 -0700 Subject: [PATCH 184/230] SwitchDetails(): 99% unit test coverage. Add the following test cases. - test_switch_details_00700 Verify ``maintenance_mode`` raises ``ValueError`` if ``mode`` is ``null`` in the controller response. - test_switch_details_00710 Verify ``maintenance_mode`` raises ``ValueError`` if system_mode is ``null`` in the controller response. - test_switch_details_00720 Verify ``maintenance_mode`` returns "migration" if mode == "Migration" in the controller response. - test_switch_details_00730 Verify ``maintenance_mode`` returns "inconsistent" if mode != system_mode in the controller response. - test_switch_details_00740 Verify ``maintenance_mode`` returns "maintenance" if ``mode == "Maintenance" and ``system_mode`` == "Maintenance" in the controller response. - test_switch_details_00750 Verify ``maintenance_mode`` returns "normal" if mode == "Normal" and system_mode == "Normal" in the controller response. - test_switch_details_00800 Verify ``platform`` returns ``None`` if model == ``null`` in the controller response. - SwitchDetails().maintenance_mode: Tweak error messages. --- plugins/module_utils/common/switch_details.py | 4 +- .../fixtures/responses_SwitchDetails.json | 145 +++++++ .../common/test_switch_details.py | 410 ++++++++++++++++++ 3 files changed, 557 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py index cd18ab543..33f410be5 100644 --- a/plugins/module_utils/common/switch_details.py +++ b/plugins/module_utils/common/switch_details.py @@ -411,12 +411,12 @@ def maintenance_mode(self): method_name = inspect.stack()[0][3] if self.mode is None: msg = f"{self.class_name}.{method_name}: " - msg += "mode is not set. Either ``filter`` has not been " + msg += "mode is not set. Either 'filter' has not been " msg += "set, or the controller response is invalid." raise ValueError(msg) if self.system_mode is None: msg = f"{self.class_name}.{method_name}: " - msg += "system_mode is not set. Either ``filter`` has not been " + msg += "system_mode is not set. Either 'filter' has not been " msg += "set, or the controller response is invalid." raise ValueError(msg) if self.mode.lower() == "migration": diff --git a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json index 3bb9b7c0d..0212edebb 100644 --- a/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -531,5 +531,150 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 + }, + "test_switch_details_00700a": { + "TEST_NOTES": [ + "DATA[0].mode is null", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "ipAddress": "192.168.1.2", + "mode": null, + "serialNumber": "FDO123456FV", + "systemMode": "Maintenance" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_switch_details_00710a": { + "TEST_NOTES": [ + "DATA[0].system_mode is null", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "ipAddress": "192.168.1.2", + "mode": "Normal", + "serialNumber": "FDO123456FV", + "systemMode": null + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_switch_details_00720a": { + "TEST_NOTES": [ + "DATA[0].mode == Migration", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "ipAddress": "192.168.1.2", + "mode": "Migration", + "serialNumber": "FDO123456FV", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_switch_details_00730a": { + "TEST_NOTES": [ + "DATA[0].mode == Maintenance", + "DATA[0].system_mode == Normal", + "mode != system_mode", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "ipAddress": "192.168.1.2", + "mode": "Maintenance", + "serialNumber": "FDO123456FV", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_switch_details_00740a": { + "TEST_NOTES": [ + "DATA[0].mode == Maintenance", + "DATA[0].system_mode == Maintenence", + "mode != system_mode", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "ipAddress": "192.168.1.2", + "mode": "Maintenance", + "serialNumber": "FDO123456FV", + "systemMode": "Maintenance" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_switch_details_00750a": { + "TEST_NOTES": [ + "DATA[0].mode == Normal", + "DATA[0].system_mode == Normal", + "mode != system_mode", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "ipAddress": "192.168.1.2", + "mode": "Normal", + "serialNumber": "FDO123456FV", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_switch_details_00800a": { + "TEST_NOTES": [ + "DATA[0].model == null", + "RETURN_CODE: 200", + "MESSAGE: OK" + ], + "DATA": [ + { + "fabricName": "VXLAN_Fabric", + "ipAddress": "192.168.1.2", + "model": null, + "serialNumber": "FDO123456FV" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_switch_details.py b/tests/unit/module_utils/common/test_switch_details.py index 196cfad13..4a9da7b39 100644 --- a/tests/unit/module_utils/common/test_switch_details.py +++ b/tests/unit/module_utils/common/test_switch_details.py @@ -493,3 +493,413 @@ def test_switch_details_00600() -> None: match += r"set instance\.filter before accessing property logicalName\." with pytest.raises(ValueError, match=match): instance.logical_name # pylint: disable=pointless-statement + + +def test_switch_details_00700() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - validate_refresh_parameters() + - refresh() + - filter.setter + - maintenance_mode + + ### Summary + Verify ``maintenance_mode`` raises ``ValueError`` if + ``mode`` is ``null`` in the controller response. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - SwitchDetails() is initialized and configured. + - SwitchDetails().refresh() is called. + - SwitchDetails().filter is set to the switch + ip_address in the response. + + ### Setup - Data + responses_switch_details() returns a response with one switch + for which the ``mode`` key is set to ``null``. + + ### Trigger + ``maintenance_mode.getter`` is accessed. + + ### Expected Result + - ``maintenance_mode.getter`` raises ``ValueError`` + because ``_get()`` returns None for ``mode``. + - + """ + + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "192.168.1.2" + + match = r"SwitchDetails\.maintenance_mode:\s+" + match += r"mode is not set\. Either 'filter' has not been set,\s+" + match += r"or the controller response is invalid\." + with pytest.raises(ValueError, match=match): + instance.maintenance_mode # pylint: disable=pointless-statement + + +def test_switch_details_00710() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - validate_refresh_parameters() + - refresh() + - filter.setter + - maintenance_mode + + ### Summary + Verify ``maintenance_mode`` raises ``ValueError`` if + system_mode is ``null`` in the controller response. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - SwitchDetails() is initialized and configured. + - SwitchDetails().refresh() is called. + - SwitchDetails().filter is set to the switch + ip_address in the response. + + ### Setup - Data + responses_switch_details() returns a response with one switch + for which the ``system_mode`` key is set to ``null``. + + ### Trigger + ``maintenance_mode.getter`` is accessed. + + ### Expected Result + - ``maintenance_mode.getter`` raises ``ValueError`` + because ``_get()`` returns None for ``system_mode``. + - + """ + + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "192.168.1.2" + + match = r"SwitchDetails\.maintenance_mode:\s+" + match += r"system_mode is not set\. Either 'filter' has not been set,\s+" + match += r"or the controller response is invalid\." + with pytest.raises(ValueError, match=match): + instance.maintenance_mode # pylint: disable=pointless-statement + + +def test_switch_details_00720() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - validate_refresh_parameters() + - refresh() + - filter.setter + - maintenance_mode + + ### Summary + Verify ``maintenance_mode`` returns "migration" if + mode == "Migration" in the controller response. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - SwitchDetails() is initialized and configured. + - SwitchDetails().refresh() is called. + - SwitchDetails().filter is set to the switch + ip_address in the response. + + ### Setup - Data + responses_switch_details() returns a response containing: + - 1x` switch + - ``mode`` == Migration + + ### Trigger + ``maintenance_mode.getter`` is accessed. + + ### Expected Result + - ``maintenance_mode.getter`` returns "migration" + """ + + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "192.168.1.2" + assert instance.maintenance_mode == "migration" + + +def test_switch_details_00730() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - validate_refresh_parameters() + - refresh() + - filter.setter + - maintenance_mode + + ### Summary + Verify ``maintenance_mode`` returns "inconsistent" if + mode != system_mode in the controller response. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - SwitchDetails() is initialized and configured. + - SwitchDetails().refresh() is called. + - SwitchDetails().filter is set to the switch + ip_address in the response. + + ### Setup - Data + responses_switch_details() returns a response containing: + - 1x switch + - ``mode`` == Normal + - ``system_mode`` == Maintenance + - i.e. ``mode`` != ``system_mode`` + + ### Trigger + ``maintenance_mode.getter`` is accessed. + + ### Expected Result + - ``maintenance_mode.getter`` returns "inconsistent" + """ + + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "192.168.1.2" + assert instance.maintenance_mode == "inconsistent" + + +def test_switch_details_00740() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - validate_refresh_parameters() + - refresh() + - filter.setter + - maintenance_mode + + ### Summary + Verify ``maintenance_mode`` returns "maintenance" if + ``mode == "Maintenance" and ``system_mode`` == "Maintenance" + in the controller response. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - SwitchDetails() is initialized and configured. + - SwitchDetails().refresh() is called. + - SwitchDetails().filter is set to the switch + ip_address in the response. + + ### Setup - Data + responses_switch_details() returns a response containing: + - 1x switch + - ``mode`` == Maintenance + - ``system_mode`` == Maintenance + + ### Trigger + ``maintenance_mode.getter`` is accessed. + + ### Expected Result + - ``maintenance_mode.getter`` returns "maintenance" + """ + + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "192.168.1.2" + assert instance.maintenance_mode == "maintenance" + + +def test_switch_details_00750() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - validate_refresh_parameters() + - refresh() + - filter.setter + - maintenance_mode + + ### Summary + Verify ``maintenance_mode`` returns "normal" if + mode == "Normal" and system_mode == "Normal" + in the controller response. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - SwitchDetails() is initialized and configured. + - SwitchDetails().refresh() is called. + - SwitchDetails().filter is set to the switch + ip_address in the response. + + ### Setup - Data + responses_switch_details() returns a response containing: + - 1x switch + - ``mode`` == Normal + - ``system_mode`` == Normal + + ### Trigger + ``maintenance_mode.getter`` is accessed. + + ### Expected Result + - ``maintenance_mode.getter`` returns "normal" + """ + + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "192.168.1.2" + assert instance.maintenance_mode == "normal" + + +def test_switch_details_00800() -> None: + """ + ### Classes and Methods + - SwitchDetails() + - validate_refresh_parameters() + - refresh() + - filter.setter + - platform.getter + + ### Summary + Verify ``platform`` returns ``None`` if model == ``null`` + in the controller response. + + ### Setup - Code + - Sender() is initialized and configured. + - RestSend() is initialized and configured. + - SwitchDetails() is initialized and configured. + - SwitchDetails().refresh() is called. + - SwitchDetails().filter is set to the switch + ip_address in the response. + + ### Setup - Data + responses_switch_details() returns a response containing: + - 1x switch + - ``model`` == null + + ### Trigger + ``platform.getter`` is accessed. + + ### Expected Result + - ``platform.getter`` returns ``None`` + """ + + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_switch_details(key) + + sender = Sender() + sender.gen = ResponseGenerator(responses()) + rest_send = RestSend(PARAMS) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + rest_send.timeout = 1 + + with does_not_raise(): + instance = SwitchDetails() + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() + instance.filter = "192.168.1.2" + assert instance.platform is None From 525299a21ee5726e3845fb4489076359543f5839 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 18 Jun 2024 05:50:25 -1000 Subject: [PATCH 185/230] Initial integration test DESCRIPTION - merged_normal_to_maintenance State: merged Test: Change normal mode switches to maintenance mode with config-deploy. --- .../dcnm_maintenance_mode/defaults/main.yaml | 2 + .../dcnm_maintenance_mode/meta/main.yaml | 1 + .../dcnm_maintenance_mode/tasks/dcnm.yaml | 20 ++ .../dcnm_maintenance_mode/tasks/main.yaml | 2 + .../tests/00_setup_fabrics_rw.yaml | 123 ++++++++++ ...1_merged_normal_to_maintenance_deploy.yaml | 229 ++++++++++++++++++ 6 files changed, 377 insertions(+) create mode 100644 tests/integration/targets/dcnm_maintenance_mode/defaults/main.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/meta/main.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tasks/dcnm.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tasks/main.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_rw.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_normal_to_maintenance_deploy.yaml diff --git a/tests/integration/targets/dcnm_maintenance_mode/defaults/main.yaml b/tests/integration/targets/dcnm_maintenance_mode/defaults/main.yaml new file mode 100644 index 000000000..55a93fc23 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_maintenance_mode/meta/main.yaml b/tests/integration/targets/dcnm_maintenance_mode/meta/main.yaml new file mode 100644 index 000000000..32cf5dda7 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/meta/main.yaml @@ -0,0 +1 @@ +dependencies: [] diff --git a/tests/integration/targets/dcnm_maintenance_mode/tasks/dcnm.yaml b/tests/integration/targets/dcnm_maintenance_mode/tasks/dcnm.yaml new file mode 100644 index 000000000..e419fc865 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tasks/dcnm.yaml @@ -0,0 +1,20 @@ +--- +- name: collect dcnm test cases + find: + paths: "{{ role_path }}/tests" + patterns: "{{ testcase }}.yaml" + connection: local + register: dcnm_cases + +- set_fact: + test_cases: + files: "{{ dcnm_cases.files }}" + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=httpapi) + include_tasks: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/dcnm_maintenance_mode/tasks/main.yaml b/tests/integration/targets/dcnm_maintenance_mode/tasks/main.yaml new file mode 100644 index 000000000..fbcfa5803 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include_tasks: dcnm.yaml, tags: ['dcnm'] } diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_rw.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_rw.yaml new file mode 100644 index 000000000..65d4bf8ed --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_rw.yaml @@ -0,0 +1,123 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 00:49.83 +################################################################################ +# DESCRIPTION +# Setup for dcnm_maintenance_mode integration tests using read-write fabrics. +# +# Create two read-write fabrics and 1x switch to each. +# - VXLAN_EVPN_Fabric with 1x leaf. +# - LAN_CLASSIC_Fabric with 1x leaf. +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 1. The following fabrics must be empty on the controller +# See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml +# - fabric_name_1 +# - fabric_type_1 # VXLAN_EVPN +# - fabric_name_3 +# - fabric_type_3 # LAN_Classic +# 2. Create fabrics if they do not exist +# - fabric_name_1 +# - fabric_name_3 +# 3. Add switch to fabric_name_1 if it doesn't exist. +# - leaf_1 +# 4. Add switch to fabric_name_3 if it doesn't exist. +# - leaf_2 +# CLEANUP +# 5. See 00_cleanup.yaml +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: merged_normal_to_maintenance +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# fabric_name_3: LAN_CLASSIC_Fabric +# fabric_type_3: LAN_CLASSIC +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 00_SETUP - Create fabrics if they do not exist. +################################################################################ +- name: 00_SETUP - Create fabrics + cisco.dcnm.dcnm_fabric: + state: merged + config: + - FABRIC_NAME: "{{ fabric_name_1 }}" + FABRIC_TYPE: "{{ fabric_type_1 }}" + BGP_AS: "65535.65534" + DEPLOY: true + - FABRIC_NAME: "{{ fabric_name_3 }}" + FABRIC_TYPE: "{{ fabric_type_3 }}" + BOOTSTRAP_ENABLE: false + IS_READ_ONLY: false + DEPLOY: true + register: result +- debug: + var: result +- assert: + that: + - result.failed == false + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' +################################################################################ +# 00_SETUP - Add one leaf switch to fabric_1 +################################################################################ +- name: Merge leaf_1 into fabric_1 + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name_1 }}" + state: merged + config: + - seed_ip: "{{ leaf_1 }}" + auth_proto: MD5 + user_name: "{{ nxos_username}}" + password: "{{ nxos_password }}" + max_hops: 0 + role: leaf + preserve_config: false + register: result +- debug: + var: result + +- assert: + that: + - 'result.failed == false' + +################################################################################ +# 00_SETUP - Add one leaf switch to fabric_3 +################################################################################ +- name: Merge leaf_2 into fabric_3 + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name_3 }}" + state: merged + config: + - seed_ip: "{{ leaf_2 }}" + auth_proto: MD5 + user_name: "{{ nxos_username}}" + password: "{{ nxos_password }}" + max_hops: 0 + role: leaf + # preserve_config must be True for LAN_CLASSIC + preserve_config: true + register: result +- debug: + var: result + +- assert: + that: + - 'result.failed == false' diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_normal_to_maintenance_deploy.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_normal_to_maintenance_deploy.yaml new file mode 100644 index 000000000..cad4a0cbf --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_normal_to_maintenance_deploy.yaml @@ -0,0 +1,229 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 05:51.85 +################################################################################ +# DESCRIPTION - merged_normal_to_maintenance +# +# State: merged +# Test: Change normal mode switches to maintenance mode with config-deploy. +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. +# 1. MERGED - SETUP - ensure switches are in normal mode +# TEST +# 3. Place leaf_1 and leaf_2 in maintenance mode using global config and verify. +# CLEANUP +# 4. Place leaf_1 and leaf_2 in normal mode using global config and verify. +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: merged_normal_to_maintenance +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# fabric_name_3: LAN_CLASSIC_Fabric +# fabric_type_3: LAN_CLASSIC +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 1. MERGED - SETUP - ensure switches are in normal mode +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "fabric_deployment_disabled": false, +# "fabric_freeze_mode": false, +# "fabric_name": "VXLAN_EVPN_Fabric", +# "fabric_read_only": false, +# "ip_address": "172.22.150.103", +# "mode": "normal", +# "role": "leaf", +# "serial_number": "FDO211218GC" +# }, +# "172.22.150.104": { +# "fabric_deployment_disabled": false, +# "fabric_freeze_mode": false, +# "fabric_name": "LAN_CLASSIC_Fabric", +# "fabric_read_only": false, +# "ip_address": "172.22.150.104", +# "mode": "normal", +# "role": "leaf", +# "serial_number": "FDO211218HH" +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - SETUP - Verify switches are in normal mode + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +################################################################################ +# MERGED - TEST - change switches to maintenance mode (global config) +################################################################################ +- name: MERGED - TEST - change switches to maintenance mode (global config) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: true + mode: maintenance + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result_maintenance_mode +- debug: + var: result_maintenance_mode + +################################################################################ +# 1. MERGED - TEST - verify switches changed to maintenance mode +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "fabric_deployment_disabled": false, +# "fabric_freeze_mode": false, +# "fabric_name": "VXLAN_EVPN_Fabric", +# "fabric_read_only": false, +# "ip_address": "172.22.150.103", +# "mode": "maintenance", +# "role": "leaf", +# "serial_number": "FDO211218GC" +# }, +# "172.22.150.104": { +# "fabric_deployment_disabled": false, +# "fabric_freeze_mode": false, +# "fabric_name": "LAN_CLASSIC_Fabric", +# "fabric_read_only": false, +# "ip_address": "172.22.150.104", +# "mode": "maintenance", +# "role": "leaf", +# "serial_number": "FDO211218HH" +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switches changed to maintenance mode + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "maintenance" + - result.diff[2][leaf_2].mode == "maintenance" + +# Pause is needed here or NDFC claims config-deploy succeeded, but it +# actually doesn't succeed, and one switch remains in maintenance mode. +- name: pause 1 minutes + pause: + minutes: 1 + +- name: MERGED - TEST - change switches to normal mode (global config) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: true + mode: normal + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result_normal_mode +- debug: + var: result_normal_mode + + +- name: MERGED - TEST - Verify switches changed to normal mode + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +- assert: + that: + - result_maintenance_mode.failed == false + - result_maintenance_mode.metadata[2].action == "change_sytem_mode" + - result_maintenance_mode.metadata[3].action == "change_sytem_mode" + - result_maintenance_mode.metadata[2].check_mode == False + - result_maintenance_mode.metadata[3].check_mode == False + - result_maintenance_mode.metadata[2].state == "merged" + - result_maintenance_mode.metadata[3].state == "merged" + - result_maintenance_mode.response[2].DATA.status == "Success" + - result_maintenance_mode.response[3].DATA.status == "Success" + - result_maintenance_mode.response[2].METHOD == "POST" + - result_maintenance_mode.response[3].METHOD == "POST" + - result_maintenance_mode.response[2].RETURN_CODE == 200 + - result_maintenance_mode.response[3].RETURN_CODE == 200 + - result_maintenance_mode.response[4].DATA.status is match 'Configuration + deployment completed.' + - result_maintenance_mode.response[5].DATA.status is match 'Configuration + deployment completed.' + - result_normal_mode.failed == false + - result_normal_mode.metadata[2].action == "change_sytem_mode" + - result_normal_mode.metadata[3].action == "change_sytem_mode" + - result_normal_mode.metadata[2].check_mode == False + - result_normal_mode.metadata[3].check_mode == False + - result_normal_mode.metadata[2].state == "merged" + - result_normal_mode.metadata[3].state == "merged" + - result_normal_mode.response[2].DATA.status == "Success" + - result_normal_mode.response[3].DATA.status == "Success" + - result_normal_mode.response[2].METHOD == "DELETE" + - result_normal_mode.response[3].METHOD == "DELETE" + - result_normal_mode.response[2].RETURN_CODE == 200 + - result_normal_mode.response[3].RETURN_CODE == 200 + - result_normal_mode.response[4].DATA.status is match 'Configuration + deployment completed.' + - result_normal_mode.response[5].DATA.status is match 'Configuration + deployment completed.' From 0114e4e7e5dadd908cae2802360d9be487c44c08 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 18 Jun 2024 16:59:39 -1000 Subject: [PATCH 186/230] MaintenanceMode(): Fix deploy endpoint MaintenanceMode(): The deploy endpoint needed to have query-string added to instruct NDFC to wait until deploy finished before continuing: /fabrics/{fabric_name}/switches/{serial_number}/deploy-maintenance-mode?waitForModeChange=true --- .../rest/control/fabrics/fabrics.py | 100 ++++++++++ .../module_utils/common/maintenance_mode.py | 12 +- ...1_merged_normal_to_maintenance_deploy.yaml | 185 ++++++++++++++++-- .../common/test_maintenance_mode.py | 22 +-- 4 files changed, 283 insertions(+), 36 deletions(-) diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py index c80cacd89..686787539 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py @@ -772,6 +772,106 @@ def path(self): return self.fabrics +class EpMaintenanceModeDeploy(Fabrics): + """ + ## V1 API - Fabrics().EpMaintenanceModeDeploy() + + ### Description + Return endpoint to deploy maintenance mode on a switch. + + ### Raises + - ``ValueError``: If ``fabric_name`` is not set. + - ``ValueError``: If ``fabric_name`` is invalid. + - ``ValueError``: If ``serial_number`` is not set. + - ``ValueError``: If ``ticket_id`` is not a string. + + ### Path + - ``/fabrics/{fabric_name}/switches/{serial_number}/deploy-maintenance-mode`` + + ### Verb + - POST + + ### Parameters + - fabric_name: string + - set the ``fabric_name`` to be used in the path + - required + - serial_number: string + - set the switch ``serial_number`` to be used in the path + - required + - wait_for_mode_change: boolean + - instruct the API to wait for the mode change to complete + before continuing. + - optional + - default: False + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint + + ### Usage + ```python + instance = EpMaintenanceModeDeploy() + instance.fabric_name = "MyFabric" + instance.serial_number = "CHM1234567" + instance.wait_for_mode_change = True + path = instance.path + verb = instance.verb + ``` + """ + + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.required_properties.add("fabric_name") + self.required_properties.add("serial_number") + self._wait_for_mode_change = False + msg = "ENTERED api.v1.lan_fabric.rest.control.fabrics." + msg += f"Fabrics.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + """ + - Path for deploy-maintenance-mode + - Raise ``ValueError`` if fabric_name is not set. + - Raise ``ValueError`` if serial_number is not set. + """ + _path = self.path_fabric_name_serial_number + _path += "/deploy-maintenance-mode" + if self.wait_for_mode_change: + _path += "?waitForModeChange=true" + return _path + + @property + def verb(self): + """ + - Return the verb for the endpoint. + - verb: POST + """ + return "POST" + + @property + def wait_for_mode_change(self): + """ + - getter: Return the wait_for_mode_change value. + - setter: Set the wait_for_mode_change value. + - setter: Raise ``ValueError`` if wait_for_mode_change is not a boolean. + - Type: boolean + - Default: False + - Optional + """ + return self._wait_for_mode_change + + @wait_for_mode_change.setter + def wait_for_mode_change(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, bool): + msg = f"{self.class_name}.{method_name}: " + msg += f"Expected boolean for {method_name}. " + msg += f"Got {value} with type {type(value).__name__}." + raise ValueError(msg) + self._wait_for_mode_change = value + + class EpMaintenanceModeEnable(Fabrics): """ ## V1 API - Fabrics().EpMaintenanceModeEnable() diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 48ce18abd..55fa729c2 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -24,7 +24,8 @@ import logging from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.lan_fabric.rest.control.fabrics.fabrics import ( - EpFabricConfigDeploy, EpMaintenanceModeDisable, EpMaintenanceModeEnable) + EpFabricConfigDeploy, EpMaintenanceModeDeploy, EpMaintenanceModeDisable, + EpMaintenanceModeEnable) from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ ConversionUtils from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ @@ -138,8 +139,9 @@ def __init__(self, params): self.valid_modes = ["maintenance", "normal"] self.conversion = ConversionUtils() - self.ep_maintenance_mode_enable = EpMaintenanceModeEnable() + self.ep_maintenance_mode_deploy = EpMaintenanceModeDeploy() self.ep_maintenance_mode_disable = EpMaintenanceModeDisable() + self.ep_maintenance_mode_enable = EpMaintenanceModeEnable() self.ep_fabric_config_deploy = EpFabricConfigDeploy() self._config = None @@ -482,12 +484,14 @@ def deploy_switches(self) -> None: method_name = inspect.stack()[0][3] self.build_deploy_dict() self.build_serial_number_to_ip_address() - endpoint = self.ep_fabric_config_deploy + endpoint = self.ep_maintenance_mode_deploy + for fabric_name, serial_numbers in self.deploy_dict.items(): # Build endpoint try: endpoint.fabric_name = fabric_name - endpoint.switch_id = serial_numbers + endpoint.serial_number = ",".join(serial_numbers) + endpoint.wait_for_mode_change = True except (TypeError, ValueError) as error: msg = f"{self.class_name}.{method_name}: " msg += "Error resolving endpoint: " diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_normal_to_maintenance_deploy.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_normal_to_maintenance_deploy.yaml index cad4a0cbf..9f725d9bf 100644 --- a/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_normal_to_maintenance_deploy.yaml +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_normal_to_maintenance_deploy.yaml @@ -3,9 +3,9 @@ # RUNTIME ################################################################################ # Recent run times (MM:SS.ms): -# 05:51.85 +# 21:55.81 ################################################################################ -# DESCRIPTION - merged_normal_to_maintenance +# DESCRIPTION - Normal mode to maintenance mode with config-deploy # # State: merged # Test: Change normal mode switches to maintenance mode with config-deploy. @@ -17,9 +17,20 @@ # 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. # 1. MERGED - SETUP - ensure switches are in normal mode # TEST -# 3. Place leaf_1 and leaf_2 in maintenance mode using global config and verify. +# GLOBAL CONFIG +# 2. Normal to Maintenance mode (global config). +# 3. Verify switches changed to maintenance mode. +# 4. Pause for 5 minutes. +# 5. Maintenance to Normal mode (global config). +# 6. Verify switches changed to normal mode. +# SWITCH CONFIG +# 7. Normal to Maintenance mode (switch config). +# 8. Verify switches changed to maintenance mode. +# 9. Pause for 5 minutes. +# 10. Maintenance to Normal mode (switch config). +# 11. Verify switches changed to normal mode. # CLEANUP -# 4. Place leaf_1 and leaf_2 in normal mode using global config and verify. +# No cleanup needed. ################################################################################ # REQUIREMENTS ################################################################################ @@ -75,7 +86,7 @@ # "sequence_number": 3 # } # ], -- name: MERGED - SETUP - Verify switches are in normal mode +- name: MERGED - SETUP - ensure switches are in normal mode cisco.dcnm.dcnm_maintenance_mode: state: query config: @@ -90,7 +101,7 @@ - result.diff[2][leaf_2].mode == "normal" ################################################################################ -# MERGED - TEST - change switches to maintenance mode (global config) +# 2. MERGED - TEST - Normal to maintenance mode (global config) ################################################################################ - name: MERGED - TEST - change switches to maintenance mode (global config) cisco.dcnm.dcnm_maintenance_mode: @@ -106,7 +117,7 @@ var: result_maintenance_mode ################################################################################ -# 1. MERGED - TEST - verify switches changed to maintenance mode +# 3. MERGED - TEST - Verify switches changed to maintenance mode (global config) ################################################################################ # Expected result # ok: [172.22.150.244] => { @@ -157,12 +168,9 @@ - result.diff[2][leaf_1].mode == "maintenance" - result.diff[2][leaf_2].mode == "maintenance" -# Pause is needed here or NDFC claims config-deploy succeeded, but it -# actually doesn't succeed, and one switch remains in maintenance mode. -- name: pause 1 minutes - pause: - minutes: 1 - +################################################################################ +# 5. MERGED - TEST - Maintenance to Normal mode (global config). +################################################################################ - name: MERGED - TEST - change switches to normal mode (global config) cisco.dcnm.dcnm_maintenance_mode: state: merged @@ -176,7 +184,146 @@ - debug: var: result_normal_mode +################################################################################ +# 6. MERGED - TEST - Verify switches changed to normal mode. +################################################################################ +- name: MERGED - TEST - Verify switches changed to normal mode + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +- assert: + that: + - result_maintenance_mode.failed == false + - result_maintenance_mode.metadata[2].action == "change_sytem_mode" + - result_maintenance_mode.metadata[3].action == "change_sytem_mode" + - result_maintenance_mode.metadata[2].check_mode == False + - result_maintenance_mode.metadata[3].check_mode == False + - result_maintenance_mode.metadata[2].state == "merged" + - result_maintenance_mode.metadata[3].state == "merged" + - result_maintenance_mode.response[2].DATA.status == "Success" + - result_maintenance_mode.response[3].DATA.status == "Success" + - result_maintenance_mode.response[2].METHOD == "POST" + - result_maintenance_mode.response[3].METHOD == "POST" + - result_maintenance_mode.response[2].RETURN_CODE == 200 + - result_maintenance_mode.response[3].RETURN_CODE == 200 + - result_maintenance_mode.response[4].DATA.status is match 'Success' + - result_maintenance_mode.response[5].DATA.status is match 'Success' + - result_normal_mode.failed == false + - result_normal_mode.metadata[2].action == "change_sytem_mode" + - result_normal_mode.metadata[3].action == "change_sytem_mode" + - result_normal_mode.metadata[2].check_mode == False + - result_normal_mode.metadata[3].check_mode == False + - result_normal_mode.metadata[2].state == "merged" + - result_normal_mode.metadata[3].state == "merged" + - result_normal_mode.response[2].DATA.status == "Success" + - result_normal_mode.response[3].DATA.status == "Success" + - result_normal_mode.response[2].METHOD == "DELETE" + - result_normal_mode.response[3].METHOD == "DELETE" + - result_normal_mode.response[2].RETURN_CODE == 200 + - result_normal_mode.response[3].RETURN_CODE == 200 + - result_normal_mode.response[4].DATA.status is match 'Success' + - result_normal_mode.response[5].DATA.status is match 'Success' + +################################################################################ +# 7. MERGED - TEST - Normal to maintenance mode (switch config) +################################################################################ +- name: MERGED - TEST - change switches to maintenance mode (switch config) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: true + switches: + - ip_address: "{{ leaf_1 }}" + mode: maintenance + - ip_address: "{{ leaf_2 }}" + mode: maintenance + register: result_maintenance_mode +- debug: + var: result_maintenance_mode + +################################################################################ +# 8. MERGED - TEST - Verify switches changed to maintenance mode (switch config) +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "fabric_deployment_disabled": false, +# "fabric_freeze_mode": false, +# "fabric_name": "VXLAN_EVPN_Fabric", +# "fabric_read_only": false, +# "ip_address": "172.22.150.103", +# "mode": "maintenance", +# "role": "leaf", +# "serial_number": "FDO211218GC" +# }, +# "172.22.150.104": { +# "fabric_deployment_disabled": false, +# "fabric_freeze_mode": false, +# "fabric_name": "LAN_CLASSIC_Fabric", +# "fabric_read_only": false, +# "ip_address": "172.22.150.104", +# "mode": "maintenance", +# "role": "leaf", +# "serial_number": "FDO211218HH" +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switches changed to maintenance mode (switch config) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "maintenance" + - result.diff[2][leaf_2].mode == "maintenance" + +################################################################################ +# 10. MERGED - TEST - Maintenance to Normal mode (switch config). +################################################################################ +- name: MERGED - TEST - Maintenance to Normal mode (switch config) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: true + mode: normal + switches: + - ip_address: "{{ leaf_1 }}" + mode: normal + - ip_address: "{{ leaf_2 }}" + mode: normal + register: result_normal_mode +- debug: + var: result_normal_mode +################################################################################ +# 11. MERGED - TEST - Verify switches changed to normal mode. +################################################################################ - name: MERGED - TEST - Verify switches changed to normal mode cisco.dcnm.dcnm_maintenance_mode: state: query @@ -206,10 +353,8 @@ - result_maintenance_mode.response[3].METHOD == "POST" - result_maintenance_mode.response[2].RETURN_CODE == 200 - result_maintenance_mode.response[3].RETURN_CODE == 200 - - result_maintenance_mode.response[4].DATA.status is match 'Configuration - deployment completed.' - - result_maintenance_mode.response[5].DATA.status is match 'Configuration - deployment completed.' + - result_maintenance_mode.response[4].DATA.status is match 'Success' + - result_maintenance_mode.response[5].DATA.status is match 'Success' - result_normal_mode.failed == false - result_normal_mode.metadata[2].action == "change_sytem_mode" - result_normal_mode.metadata[3].action == "change_sytem_mode" @@ -223,7 +368,5 @@ - result_normal_mode.response[3].METHOD == "DELETE" - result_normal_mode.response[2].RETURN_CODE == 200 - result_normal_mode.response[3].RETURN_CODE == 200 - - result_normal_mode.response[4].DATA.status is match 'Configuration - deployment completed.' - - result_normal_mode.response[5].DATA.status is match 'Configuration - deployment completed.' + - result_normal_mode.response[4].DATA.status is match 'Success' + - result_normal_mode.response[5].DATA.status is match 'Success' diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 3cab37fd8..d2843d2b1 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -880,8 +880,8 @@ def serial_number(self, value): @pytest.mark.parametrize( "endpoint_instance, mock_exception, expected_exception, mock_message", [ - ("ep_fabric_config_deploy", TypeError, ValueError, "Bad type"), - ("ep_fabric_config_deploy", ValueError, ValueError, "Bad value"), + ("ep_maintenance_mode_deploy", TypeError, ValueError, "Bad type"), + ("ep_maintenance_mode_deploy", ValueError, ValueError, "Bad value"), ], ) def test_maintenance_mode_00800( @@ -908,7 +908,7 @@ def test_maintenance_mode_00800( Code Flow - Setup - MaintenanceMode() is instantiated - Required attributes are set - - EpFabricConfigDeploy() is mocked to raise each of the above exceptions + - EpMaintenanceModeDeploy() is mocked to raise each of the above exceptions Code Flow - Test - MaintenanceMode().commit() is called for each exception @@ -920,12 +920,12 @@ def test_maintenance_mode_00800( class MockEndpoint: """ - Mock EpFabricConfigDeploy() class + Mock EpMaintenanceModeDeploy() class """ def __init__(self): self._fabric_name = None - self._switch_id = None + self._serial_number = None @property def fabric_name(self): @@ -940,15 +940,15 @@ def fabric_name(self, *args): raise mock_exception(mock_message) @property - def switch_id(self): + def serial_number(self): """ - Mock switch_id getter/setter + Mock serial_number getter/setter """ - return self._switch_id + return self._serial_number - @switch_id.setter - def switch_id(self, value): - self._switch_id = value + @serial_number.setter + def serial_number(self, value): + self._serial_number = value def responses(): yield {"MESSAGE": "OK", "RETURN_CODE": 200, "DATA": {"status": "Success"}} From 09089929923ac0bf9704cde762d3006ff3ee063a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 19 Jun 2024 13:15:24 -1000 Subject: [PATCH 187/230] dcnm_maintenance_mode: inconsistent mode, change handling. The changes in this commit were needed because config-deploy cannot be used for deploying maintenance mode. Rather deploy-maintenance-mode endpoint must be used with the query-string waitForModeChange set to true. Also, because deploy-maintenance-mode is optional, it is expected that switch mode could be "inconsistent". Previously, we raised an error in this situation. Removed this error. 1. Changed the following unit tests: - test_maintenance_mode_00220 - changed to yield response from response_DeployMaintenanceMode.json - check results for deploy case only if deploy == True - expect action == deploy_maintenance_mode rather than config_deploy - test_maintenance_mode_00800 - Mock EpMaintenanceModeDeploy rather than EpFabricConfigDeploy - yield a second response with RETURN_CODE == 200 and MESSAGE == OK - change expected error message. 2. tests/unit/module_utils/common/common_utils.py - Remove responses_config_deploy - Add responses_deploy_maintenance_mode 3. dcnm_maintenance_mode.py - Merged(): Remove raise ValueError if mode == "inconsistent" 4. maintenance_mode.py - build_endpoints(): new method - deploy_switches(): refactor out functionality in build_endpoints() - Use dict self.endpoints rather than endpoint object. This was required to allow mocking of the endpoint object. - deploy_switches(): modify error message if RETURN_CODE != 200 5. api/v1/lan_fabric/rest/control/fabrics/fabrics.py - Add property wait_for_mode_change to allow setting of waitForModeChange query string. 6. Add playbooks/roles/dcnm_maintenance_mode/* 7. Modify playbooks/roles/dcnm_fabric/* to be specific to the dcnm_fabric role. --- playbooks/roles/dcnm_fabric/dcnm_tests.yaml | 8 +- .../dcnm_maintenance_mode/dcnm_hosts.yaml | 20 + .../dcnm_maintenance_mode/dcnm_tests.yaml | 42 ++ .../rest/control/fabrics/fabrics.py | 4 +- .../module_utils/common/maintenance_mode.py | 70 ++- plugins/modules/dcnm_maintenance_mode.py | 14 +- .../tests/00_setup_fabrics_1x_rw.yaml | 94 +++++ ...cs_rw.yaml => 00_setup_fabrics_2x_rw.yaml} | 2 +- ...=> 01_merged_maintenance_mode_deploy.yaml} | 141 ++++--- .../01_merged_maintenance_mode_no_deploy.yaml | 397 ++++++++++++++++++ .../unit/module_utils/common/common_utils.py | 8 +- ...n => responses_DeployMaintenanceMode.json} | 4 +- .../common/test_maintenance_mode.py | 58 ++- 13 files changed, 735 insertions(+), 127 deletions(-) create mode 100644 playbooks/roles/dcnm_maintenance_mode/dcnm_hosts.yaml create mode 100644 playbooks/roles/dcnm_maintenance_mode/dcnm_tests.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_1x_rw.yaml rename tests/integration/targets/dcnm_maintenance_mode/tests/{00_setup_fabrics_rw.yaml => 00_setup_fabrics_2x_rw.yaml} (98%) rename tests/integration/targets/dcnm_maintenance_mode/tests/{01_merged_normal_to_maintenance_deploy.yaml => 01_merged_maintenance_mode_deploy.yaml} (80%) create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_no_deploy.yaml rename tests/unit/module_utils/common/fixtures/{responses_ConfigDeploy.json => responses_DeployMaintenanceMode.json} (56%) diff --git a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml index a3cc72d88..03f19ec5b 100644 --- a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml +++ b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml @@ -1,5 +1,8 @@ --- -# This playbook can be used to execute the dcnm_fabric test role. +# This playbook can be used to execute integration tests for +# the role located in: +# +# tests/integration/targets/dcnm_fabric # # Modify the vars section with details for testing setup. # @@ -16,7 +19,8 @@ connection: ansible.netcommon.httpapi vars: - # This testcase field can run any test in the tests directory for the role + # See the following location for available test cases: + # tests/integration/targets/dcnm_fabric/tests # testcase: dcnm_fabric_deleted_basic # testcase: dcnm_fabric_deleted_basic_ipfm # testcase: dcnm_fabric_merged_basic diff --git a/playbooks/roles/dcnm_maintenance_mode/dcnm_hosts.yaml b/playbooks/roles/dcnm_maintenance_mode/dcnm_hosts.yaml new file mode 100644 index 000000000..f22bf9dd7 --- /dev/null +++ b/playbooks/roles/dcnm_maintenance_mode/dcnm_hosts.yaml @@ -0,0 +1,20 @@ +all: + vars: + ansible_user: "admin" + ansible_password: "password-secret" + ansible_python_interpreter: python + ansible_httpapi_validate_certs: False + ansible_httpapi_use_ssl: True + children: + dcnm: + vars: + ansible_connection: ansible.netcommon.httpapi + ansible_network_os: cisco.dcnm.dcnm + hosts: + dcnm-instance.example.com: + nxos: + hosts: + n9k-hosta.example.com: + ansible_connection: ansible.netcommon.network_cli + ansible_network_os: cisco.nxos.nxos + ansible_ssh_port: 22 diff --git a/playbooks/roles/dcnm_maintenance_mode/dcnm_tests.yaml b/playbooks/roles/dcnm_maintenance_mode/dcnm_tests.yaml new file mode 100644 index 000000000..4c83185f9 --- /dev/null +++ b/playbooks/roles/dcnm_maintenance_mode/dcnm_tests.yaml @@ -0,0 +1,42 @@ +--- +# This playbook can be used to execute integration tests for +# the role located in: +# +# tests/integration/targets/dcnm_maintenance_mode +# +# Modify the vars section with details for your testing setup. +# +# NOTES: +# 1. For the IPFM test cases (dcnm_*_ipfm), ensure that the controller +# is running in IPFM mode. i.e. Ensure that +# Fabric Controller -> Admin -> System Settings -> Feature Management +# "IP Fabric for Media" is checked. +# 2. For all other test cases, ensure that +# Fabric Controller -> Admin -> System Settings -> Feature Management +# "Fabric Builder" is checked. +- hosts: dcnm + gather_facts: no + connection: ansible.netcommon.httpapi + + vars: + # See the following location for available test cases: + # tests/integration/targets/dcnm_maintenance_mode/tests + # testcase: 00_setup_fabrics_1x_rw + # testcase: 00_setup_fabrics_2x_rw + # testcase: 01_merged_maintenance_mode_deploy + # testcase: 01_merged_maintenance_mode_no_deploy + fabric_name_1: VXLAN_EVPN_Fabric + fabric_type_1: VXLAN_EVPN + fabric_name_2: VXLAN_EVPN_MSD_Fabric + fabric_type_2: VXLAN_EVPN_MSD + fabric_name_3: LAN_CLASSIC_Fabric + fabric_type_3: LAN_CLASSIC + fabric_name_4: IPFM_Fabric + fabric_type_4: IPFM + leaf_1: 172.22.150.103 + leaf_2: 172.22.150.104 + nxos_username: admin + nxos_password: myNxosPassword + + roles: + - dcnm_maintenance_mode diff --git a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py index 686787539..b043e5f94 100644 --- a/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py @@ -854,7 +854,7 @@ def wait_for_mode_change(self): """ - getter: Return the wait_for_mode_change value. - setter: Set the wait_for_mode_change value. - - setter: Raise ``ValueError`` if wait_for_mode_change is not a boolean. + - setter: Raise ``TypeError`` if wait_for_mode_change is not a boolean. - Type: boolean - Default: False - Optional @@ -868,7 +868,7 @@ def wait_for_mode_change(self, value): msg = f"{self.class_name}.{method_name}: " msg += f"Expected boolean for {method_name}. " msg += f"Got {value} with type {type(value).__name__}." - raise ValueError(msg) + raise TypeError(msg) self._wait_for_mode_change = value diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 55fa729c2..4da069cc1 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -119,6 +119,7 @@ def __init__(self, params): self.params = params self.action = "maintenance_mode" + self.endpoints = [] self.check_mode = self.params.get("check_mode", None) if self.check_mode is None: @@ -470,6 +471,35 @@ def build_serial_number_to_ip_address(self) -> None: ip_address = item.get("ip_address") self.serial_number_to_ip_address[serial_number] = ip_address + def build_endpoints(self) -> None: + """ + ### Summary + Build ``endpoints`` dict used in ``self.deploy_switches``. + + ### Raises + ``ValueError`` if endpoint configuration fails. + """ + method_name = inspect.stack()[0][3] + endpoints = [] + for fabric_name, serial_numbers in self.deploy_dict.items(): + for serial_number in serial_numbers: + endpoint = {} + try: + self.ep_maintenance_mode_deploy.fabric_name = fabric_name + self.ep_maintenance_mode_deploy.serial_number = serial_number + self.ep_maintenance_mode_deploy.wait_for_mode_change = True + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error resolving endpoint: " + msg += f"Error details: {error}." + raise ValueError(msg) from error + endpoint["path"] = self.ep_maintenance_mode_deploy.path + endpoint["verb"] = self.ep_maintenance_mode_deploy.verb + endpoint["serial_number"] = serial_number + endpoint["fabric_name"] = fabric_name + endpoints.append(copy.copy(endpoint)) + self.endpoints = copy.copy(endpoints) + def deploy_switches(self) -> None: """ ### Summary @@ -484,37 +514,31 @@ def deploy_switches(self) -> None: method_name = inspect.stack()[0][3] self.build_deploy_dict() self.build_serial_number_to_ip_address() - endpoint = self.ep_maintenance_mode_deploy - - for fabric_name, serial_numbers in self.deploy_dict.items(): - # Build endpoint - try: - endpoint.fabric_name = fabric_name - endpoint.serial_number = ",".join(serial_numbers) - endpoint.wait_for_mode_change = True - except (TypeError, ValueError) as error: - msg = f"{self.class_name}.{method_name}: " - msg += "Error resolving endpoint: " - msg += f"Error details: {error}." - raise ValueError(msg) from error + try: + self.build_endpoints() + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error building endpoints. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error + for endpoint in self.endpoints: # Send request - self.rest_send.path = endpoint.path - self.rest_send.verb = endpoint.verb + self.rest_send.path = endpoint["path"] + self.rest_send.verb = endpoint["verb"] self.rest_send.payload = None self.rest_send.commit() # Register the result - action = "config_deploy" + action = "deploy_maintenance_mode" result = self.rest_send.result_current["success"] if result is False: self.results.diff_current = {} else: diff = {} diff.update({f"{action}": result}) - for serial_number in serial_numbers: - ip_address = self.serial_number_to_ip_address[serial_number] - diff.update({ip_address: serial_number}) + ip_address = self.serial_number_to_ip_address[endpoint["serial_number"]] + diff.update({ip_address: ip_address}) self.results.diff_current = diff self.results.action = action @@ -528,10 +552,10 @@ def deploy_switches(self) -> None: if self.results.response_current["RETURN_CODE"] != 200: msg = f"{self.class_name}.{method_name}: " - msg += "Unable to deploy switches: " - msg += f"fabric_name {fabric_name}, " - msg += "serial_numbers " - msg += f"{','.join(serial_numbers)}. " + msg += "Unable to deploy switch: " + msg += f"fabric_name {endpoint['fabric_name']}, " + msg += "serial_number " + msg += f"{endpoint['serial_number']}. " msg += f"Got response {self.results.response_current}." raise ControllerResponseError(msg) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index b6e227476..5b5830188 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -127,8 +127,6 @@ import logging from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import \ - Sender from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import \ Log from ansible_collections.cisco.dcnm.plugins.module_utils.common.maintenance_mode import \ @@ -149,6 +147,8 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import \ + Sender def json_pretty(msg): @@ -901,16 +901,6 @@ def fabric_deployment_disabled(self) -> None: additional_info += "fabric_read_only: " additional_info += f"{fabric_read_only}, " additional_info += f"maintenance_mode: {mode}. " - if mode == "inconsistent": - msg = f"{self.class_name}.{method_name}: " - msg += "Switch maintenance mode state differs from the " - msg += "controller's maintenance mode state for switch " - msg += f"with ip_address {ip_address}, " - msg += f"serial_number {serial_number}. " - msg += "This is typically resolved by initiating a switch " - msg += "Deploy Config on the controller. " - msg += additional_info - raise ValueError(msg) if mode == "migration": msg = f"{self.class_name}.{method_name}: " diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_1x_rw.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_1x_rw.yaml new file mode 100644 index 000000000..6002bacb3 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_1x_rw.yaml @@ -0,0 +1,94 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 00:49.83 +################################################################################ +# DESCRIPTION +# Setup for dcnm_maintenance_mode integration tests using 1x read-write fabrics. +# +# Create one read-write fabric and add 2x switch. +# - VXLAN_EVPN_Fabric with 2x leaf. +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 1. The following fabrics must be empty on the controller +# See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml +# - fabric_name_1 +# - fabric_type_1 # VXLAN_EVPN +# 2. Create fabrics if they do not exist +# - fabric_name_1 +# 3. Add switches to fabric_name_1 if they do not exist. +# - leaf_1 +# - leaf_2 +# CLEANUP +# 5. See 00_cleanup.yaml +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: merged_normal_to_maintenance +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 00_SETUP - Create fabrics if they do not exist. +################################################################################ +- name: 00_SETUP - Create fabrics + cisco.dcnm.dcnm_fabric: + state: merged + config: + - FABRIC_NAME: "{{ fabric_name_1 }}" + FABRIC_TYPE: "{{ fabric_type_1 }}" + BGP_AS: "65535.65534" + DEPLOY: true + register: result +- debug: + var: result +- assert: + that: + - result.failed == false + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' +################################################################################ +# 00_SETUP - Merge leaf_1 and leaf_2 into fabric_1 +################################################################################ +- name: Merge leaf_1 and leaf_2 into fabric_1 + cisco.dcnm.dcnm_inventory: + fabric: "{{ fabric_name_1 }}" + state: merged + config: + - seed_ip: "{{ leaf_1 }}" + auth_proto: MD5 + user_name: "{{ nxos_username}}" + password: "{{ nxos_password }}" + max_hops: 0 + role: leaf + preserve_config: false + - seed_ip: "{{ leaf_2 }}" + auth_proto: MD5 + user_name: "{{ nxos_username}}" + password: "{{ nxos_password }}" + max_hops: 0 + role: leaf + preserve_config: false + register: result +- debug: + var: result + +- assert: + that: + - 'result.failed == false' diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_rw.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_2x_rw.yaml similarity index 98% rename from tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_rw.yaml rename to tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_2x_rw.yaml index 65d4bf8ed..ebde6e6f6 100644 --- a/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_rw.yaml +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_2x_rw.yaml @@ -8,7 +8,7 @@ # DESCRIPTION # Setup for dcnm_maintenance_mode integration tests using read-write fabrics. # -# Create two read-write fabrics and 1x switch to each. +# Create two read-write fabrics and add 1x switch to each. # - VXLAN_EVPN_Fabric with 1x leaf. # - LAN_CLASSIC_Fabric with 1x leaf. ################################################################################ diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_normal_to_maintenance_deploy.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_deploy.yaml similarity index 80% rename from tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_normal_to_maintenance_deploy.yaml rename to tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_deploy.yaml index 9f725d9bf..cda036997 100644 --- a/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_normal_to_maintenance_deploy.yaml +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_deploy.yaml @@ -3,12 +3,23 @@ # RUNTIME ################################################################################ # Recent run times (MM:SS.ms): -# 21:55.81 +# 23:45.94 +# 23:49.52 ################################################################################ -# DESCRIPTION - Normal mode to maintenance mode with config-deploy +# DESCRIPTION - Normal mode to maintenance mode with deploy-maintenance-mode # # State: merged -# Test: Change normal mode switches to maintenance mode with config-deploy. +# Tests: +# - All tests use deploy-maintenance-mode endpoint. +# 1. Change normal mode switches to maintenance mode using playbook global config. +# 2. Change maintenance mode switches to normal mode using playbook global config. +# 3. Change normal mode switches to maintenance mode using playbook switch config. +# 4. Change maintenance mode switches to normal mode using playbook switch config. +# +# NOTES: +# - Execute either of the following testcases to setup the fabric and switches +# - 00_setup_fabrics_1x_rw.yaml (1x fabric with 2x switches) +# - 00_setup_fabrics_2x_rw.yaml (2x fabrics with 1x switch each) ################################################################################ ################################################################################ # STEPS @@ -18,17 +29,15 @@ # 1. MERGED - SETUP - ensure switches are in normal mode # TEST # GLOBAL CONFIG -# 2. Normal to Maintenance mode (global config). -# 3. Verify switches changed to maintenance mode. -# 4. Pause for 5 minutes. -# 5. Maintenance to Normal mode (global config). -# 6. Verify switches changed to normal mode. +# 2. Normal to Maintenance mode (global config) +# 3. Verify switch mode is maintenance (global config) +# 4. Maintenance to Normal mode (global config) +# 5. Verify switch mode is normal (global config) # SWITCH CONFIG -# 7. Normal to Maintenance mode (switch config). -# 8. Verify switches changed to maintenance mode. -# 9. Pause for 5 minutes. -# 10. Maintenance to Normal mode (switch config). -# 11. Verify switches changed to normal mode. +# 6. Normal to Maintenance mode (switch config) +# 7. Verify switch mode is maintenance (switch config) +# 8. Maintenance to Normal mode (switch config) +# 9. Verify switch mode is normal (switch config) # CLEANUP # No cleanup needed. ################################################################################ @@ -64,24 +73,12 @@ # }, # { # "172.22.150.103": { -# "fabric_deployment_disabled": false, -# "fabric_freeze_mode": false, -# "fabric_name": "VXLAN_EVPN_Fabric", -# "fabric_read_only": false, # "ip_address": "172.22.150.103", # "mode": "normal", -# "role": "leaf", -# "serial_number": "FDO211218GC" # }, # "172.22.150.104": { -# "fabric_deployment_disabled": false, -# "fabric_freeze_mode": false, -# "fabric_name": "LAN_CLASSIC_Fabric", -# "fabric_read_only": false, # "ip_address": "172.22.150.104", # "mode": "normal", -# "role": "leaf", -# "serial_number": "FDO211218HH" # }, # "sequence_number": 3 # } @@ -117,7 +114,7 @@ var: result_maintenance_mode ################################################################################ -# 3. MERGED - TEST - Verify switches changed to maintenance mode (global config) +# 3. MERGED - TEST - Verify switch mode is maintenance (global config) ################################################################################ # Expected result # ok: [172.22.150.244] => { @@ -132,29 +129,17 @@ # }, # { # "172.22.150.103": { -# "fabric_deployment_disabled": false, -# "fabric_freeze_mode": false, -# "fabric_name": "VXLAN_EVPN_Fabric", -# "fabric_read_only": false, # "ip_address": "172.22.150.103", # "mode": "maintenance", -# "role": "leaf", -# "serial_number": "FDO211218GC" # }, # "172.22.150.104": { -# "fabric_deployment_disabled": false, -# "fabric_freeze_mode": false, -# "fabric_name": "LAN_CLASSIC_Fabric", -# "fabric_read_only": false, # "ip_address": "172.22.150.104", # "mode": "maintenance", -# "role": "leaf", -# "serial_number": "FDO211218HH" # }, # "sequence_number": 3 # } # ], -- name: MERGED - TEST - Verify switches changed to maintenance mode +- name: MERGED - TEST - Verify switch mode is maintenance (global config) cisco.dcnm.dcnm_maintenance_mode: state: query config: @@ -169,7 +154,7 @@ - result.diff[2][leaf_2].mode == "maintenance" ################################################################################ -# 5. MERGED - TEST - Maintenance to Normal mode (global config). +# 4. MERGED - TEST - Maintenance to Normal mode (global config) ################################################################################ - name: MERGED - TEST - change switches to normal mode (global config) cisco.dcnm.dcnm_maintenance_mode: @@ -185,9 +170,32 @@ var: result_normal_mode ################################################################################ -# 6. MERGED - TEST - Verify switches changed to normal mode. +# 5. MERGED - TEST - Verify switch mode is normal (global config) ################################################################################ -- name: MERGED - TEST - Verify switches changed to normal mode +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is normal (global config) cisco.dcnm.dcnm_maintenance_mode: state: query config: @@ -235,7 +243,7 @@ - result_normal_mode.response[5].DATA.status is match 'Success' ################################################################################ -# 7. MERGED - TEST - Normal to maintenance mode (switch config) +# 6. MERGED - TEST - Normal to maintenance mode (switch config) ################################################################################ - name: MERGED - TEST - change switches to maintenance mode (switch config) cisco.dcnm.dcnm_maintenance_mode: @@ -252,9 +260,9 @@ var: result_maintenance_mode ################################################################################ -# 8. MERGED - TEST - Verify switches changed to maintenance mode (switch config) +# 7. MERGED - TEST - Verify switch mode is maintenance (switch config) ################################################################################ -# Expected result +# Expected result (only relevant fields shown) # ok: [172.22.150.244] => { # "result": { # "changed": false, @@ -267,29 +275,17 @@ # }, # { # "172.22.150.103": { -# "fabric_deployment_disabled": false, -# "fabric_freeze_mode": false, -# "fabric_name": "VXLAN_EVPN_Fabric", -# "fabric_read_only": false, # "ip_address": "172.22.150.103", # "mode": "maintenance", -# "role": "leaf", -# "serial_number": "FDO211218GC" # }, # "172.22.150.104": { -# "fabric_deployment_disabled": false, -# "fabric_freeze_mode": false, -# "fabric_name": "LAN_CLASSIC_Fabric", -# "fabric_read_only": false, # "ip_address": "172.22.150.104", # "mode": "maintenance", -# "role": "leaf", -# "serial_number": "FDO211218HH" # }, # "sequence_number": 3 # } # ], -- name: MERGED - TEST - Verify switches changed to maintenance mode (switch config) +- name: MERGED - TEST - Verify switch mode is maintenance (switch config) cisco.dcnm.dcnm_maintenance_mode: state: query config: @@ -304,7 +300,7 @@ - result.diff[2][leaf_2].mode == "maintenance" ################################################################################ -# 10. MERGED - TEST - Maintenance to Normal mode (switch config). +# 8. MERGED - TEST - Maintenance to Normal mode (switch config) ################################################################################ - name: MERGED - TEST - Maintenance to Normal mode (switch config) cisco.dcnm.dcnm_maintenance_mode: @@ -322,9 +318,32 @@ var: result_normal_mode ################################################################################ -# 11. MERGED - TEST - Verify switches changed to normal mode. +# 9. MERGED - TEST - Verify switch mode is normal (switch config) ################################################################################ -- name: MERGED - TEST - Verify switches changed to normal mode +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is normal (switch config) cisco.dcnm.dcnm_maintenance_mode: state: query config: diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_no_deploy.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_no_deploy.yaml new file mode 100644 index 000000000..9d110feb6 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_no_deploy.yaml @@ -0,0 +1,397 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 00:36.466 +################################################################################ +# DESCRIPTION - Normal mode to maintenance mode without deploy-maintenance-mode +# +# State: merged +# Tests: +# - All tests do NOT use deploy-maintenance-mode endpoint (hence, maintenance +# mode state is changed only on the controller and NOT on the switches.) +# 1. Change normal mode switches to maintenance mode using playbook global config. +# 2. Change maintenance mode switches to normal mode using playbook global config. +# 3. Change normal mode switches to maintenance mode using playbook switch config. +# 4. Change maintenance mode switches to normal mode using playbook switch config. +# +# NOTES: +# a. Execute either of the following testcases to setup the fabric and switches +# - 00_setup_fabrics_1x_rw.yaml (1x fabric with 2x switches) +# - 00_setup_fabrics_2x_rw.yaml (2x fabrics with 1x switch each) +# b. Switch mode will be inconsistent after changing to maintenance mode +# without deploy since the switch state will differ from controller state. +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. +# 1. MERGED - SETUP - ensure switches are in normal mode +# TEST +# GLOBAL CONFIG +# 2. Normal to Maintenance mode (global config) +# 3. Verify switch mode is inconsistent (global config) +# 4. Maintenance to Normal mode (global config) +# 5. Verify switch mode is normal (global config) +# SWITCH CONFIG +# 6. Normal to Maintenance mode (switch config) +# 7. Verify switch mode is inconsistent (switch config) +# 8. Maintenance to Normal mode (switch config) +# 9. Verify switch mode is normal (switch config) +# CLEANUP +# No cleanup needed. +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: merged_normal_to_maintenance +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# fabric_name_3: LAN_CLASSIC_Fabric +# fabric_type_3: LAN_CLASSIC +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 1. MERGED - SETUP - ensure switches are in normal mode +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - SETUP - ensure switches are in normal mode + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +################################################################################ +# 2. MERGED - TEST - Normal to maintenance mode (global config) +################################################################################ +- name: MERGED - TEST - change switches to maintenance mode (global config) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: false + mode: maintenance + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result_maintenance_mode +- debug: + var: result_maintenance_mode + +################################################################################ +# 3. MERGED - TEST - Verify switch mode is inconsistent (global config) +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "inconsistent", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "inconsistent", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is inconsistent (global config) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "inconsistent" + - result.diff[2][leaf_2].mode == "inconsistent" + +################################################################################ +# 5. MERGED - TEST - Maintenance to Normal mode (global config) +################################################################################ +- name: MERGED - TEST - change switches to normal mode (global config) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: false + mode: normal + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result_normal_mode +- debug: + var: result_normal_mode + +################################################################################ +# 6. MERGED - TEST - Verify switch mode is normal (global config) +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is normal (global config) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +- debug: + var: result_maintenance_mode + +- debug: + var: result_normal_mode + +- assert: + that: + - result_maintenance_mode.failed == false + - result_maintenance_mode.metadata[2].action == "change_sytem_mode" + - result_maintenance_mode.metadata[3].action == "change_sytem_mode" + - result_maintenance_mode.metadata[2].check_mode == False + - result_maintenance_mode.metadata[3].check_mode == False + - result_maintenance_mode.metadata[2].state == "merged" + - result_maintenance_mode.metadata[3].state == "merged" + - result_maintenance_mode.response[2].DATA.status == "Success" + - result_maintenance_mode.response[3].DATA.status == "Success" + - result_maintenance_mode.response[2].METHOD == "POST" + - result_maintenance_mode.response[3].METHOD == "POST" + - result_maintenance_mode.response[2].RETURN_CODE == 200 + - result_maintenance_mode.response[3].RETURN_CODE == 200 + - result_normal_mode.failed == false + - result_normal_mode.metadata[2].action == "change_sytem_mode" + - result_normal_mode.metadata[3].action == "change_sytem_mode" + - result_normal_mode.metadata[2].check_mode == False + - result_normal_mode.metadata[3].check_mode == False + - result_normal_mode.metadata[2].state == "merged" + - result_normal_mode.metadata[3].state == "merged" + - result_normal_mode.response[2].DATA.status == "Success" + - result_normal_mode.response[3].DATA.status == "Success" + - result_normal_mode.response[2].METHOD == "DELETE" + - result_normal_mode.response[3].METHOD == "DELETE" + - result_normal_mode.response[2].RETURN_CODE == 200 + - result_normal_mode.response[3].RETURN_CODE == 200 + +################################################################################ +# 7. MERGED - TEST - Normal to maintenance mode (switch config) +################################################################################ +- name: MERGED - TEST - change switches to maintenance mode (switch config) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: false + switches: + - ip_address: "{{ leaf_1 }}" + mode: maintenance + - ip_address: "{{ leaf_2 }}" + mode: maintenance + register: result_maintenance_mode +- debug: + var: result_maintenance_mode + +################################################################################ +# 8. MERGED - TEST - Verify switch mode is inconsistent (switch config) +################################################################################ +# Expected result (only relevant fields shown) +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "maintenance", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "maintenance", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is inconsistent (switch config) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "inconsistent" + - result.diff[2][leaf_2].mode == "inconsistent" + +################################################################################ +# 10. MERGED - TEST - Inconsistent to Normal mode (switch config) +################################################################################ +- name: MERGED - TEST - Inconsistent to Normal mode (switch config) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: false + mode: normal + switches: + - ip_address: "{{ leaf_1 }}" + mode: normal + - ip_address: "{{ leaf_2 }}" + mode: normal + register: result_normal_mode +- debug: + var: result_normal_mode + +################################################################################ +# 11. MERGED - TEST - Verify switch mode is normal (switch config) +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is normal (switch config) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +- debug: + var: result_maintenance_mode + +- debug: + var: result_normal_mode + +- assert: + that: + - result_maintenance_mode.failed == false + - result_maintenance_mode.metadata[2].action == "change_sytem_mode" + - result_maintenance_mode.metadata[3].action == "change_sytem_mode" + - result_maintenance_mode.metadata[2].check_mode == False + - result_maintenance_mode.metadata[3].check_mode == False + - result_maintenance_mode.metadata[2].state == "merged" + - result_maintenance_mode.metadata[3].state == "merged" + - result_maintenance_mode.response[2].DATA.status == "Success" + - result_maintenance_mode.response[3].DATA.status == "Success" + - result_maintenance_mode.response[2].METHOD == "POST" + - result_maintenance_mode.response[3].METHOD == "POST" + - result_maintenance_mode.response[2].RETURN_CODE == 200 + - result_maintenance_mode.response[3].RETURN_CODE == 200 + - result_normal_mode.failed == false + - result_normal_mode.metadata[2].action == "change_sytem_mode" + - result_normal_mode.metadata[3].action == "change_sytem_mode" + - result_normal_mode.metadata[2].check_mode == False + - result_normal_mode.metadata[3].check_mode == False + - result_normal_mode.metadata[2].state == "merged" + - result_normal_mode.metadata[3].state == "merged" + - result_normal_mode.response[2].DATA.status == "Success" + - result_normal_mode.response[3].DATA.status == "Success" + - result_normal_mode.response[2].METHOD == "DELETE" + - result_normal_mode.response[3].METHOD == "DELETE" + - result_normal_mode.response[2].RETURN_CODE == 200 + - result_normal_mode.response[3].RETURN_CODE == 200 diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index 42c856b89..56c28d2fe 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -257,13 +257,13 @@ def merge_dicts_v2_data(key: str) -> Dict[str, str]: return data -def responses_config_deploy(key: str) -> Dict[str, str]: +def responses_deploy_maintenance_mode(key: str) -> Dict[str, str]: """ - Return data in responses_ConfigDeploy.json + Return data in responses_DeployMaintenanceMode.json """ - response_file = "responses_ConfigDeploy" + response_file = "responses_DeployMaintenanceMode" response = load_fixture(response_file).get(key) - print(f"responses_config_deploy: {key} : {response}") + print(f"responses_deploy_maintenance_mode: {key} : {response}") return response diff --git a/tests/unit/module_utils/common/fixtures/responses_ConfigDeploy.json b/tests/unit/module_utils/common/fixtures/responses_DeployMaintenanceMode.json similarity index 56% rename from tests/unit/module_utils/common/fixtures/responses_ConfigDeploy.json rename to tests/unit/module_utils/common/fixtures/responses_DeployMaintenanceMode.json index e147169ca..8fbbd2578 100644 --- a/tests/unit/module_utils/common/fixtures/responses_ConfigDeploy.json +++ b/tests/unit/module_utils/common/fixtures/responses_DeployMaintenanceMode.json @@ -1,11 +1,11 @@ { "test_maintenance_mode_00220a": { "DATA": { - "status": "Configuration deployment completed." + "status": "Success" }, "MESSAGE": "OK", "METHOD": "POST", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_Fabric/config-deploy/FDO22180ASJ?forceShowRun=False", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_EVPN_Fabric/switches/FDO211218HH/deploy-maintenance-mode?waitForModeChange=true", "RETURN_CODE": 200 } } \ No newline at end of file diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index d2843d2b1..48797fd53 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -51,7 +51,7 @@ Sender from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import ( ResponseGenerator, does_not_raise, maintenance_mode_fixture, params, - responses_config_deploy, responses_maintenance_mode) + responses_deploy_maintenance_mode, responses_maintenance_mode) FABRIC_NAME = "VXLAN_Fabric" CONFIG = [ @@ -390,7 +390,7 @@ def test_maintenance_mode_00220(maintenance_mode, mode, deploy) -> None: def responses(): yield responses_maintenance_mode(key) - yield responses_config_deploy(key) + yield responses_deploy_maintenance_mode(key) sender = Sender() sender.gen = ResponseGenerator(responses()) @@ -421,33 +421,37 @@ def responses(): assert instance.results.diff[0].get("sequence_number", None) == 1 assert instance.results.diff[0].get("serial_number", None) == "FDO22180ASJ" - assert instance.results.diff[1].get("config_deploy", None) is True - assert instance.results.diff[1].get("sequence_number", None) == 2 - assert instance.results.metadata[0].get("action", None) == "change_sytem_mode" assert instance.results.metadata[0].get("sequence_number", None) == 1 assert instance.results.metadata[0].get("state", None) == "merged" - assert instance.results.metadata[1].get("action", None) == "config_deploy" - assert instance.results.metadata[1].get("sequence_number", None) == 2 - assert instance.results.metadata[1].get("state", None) == "merged" - assert instance.results.response[0].get("DATA", {}).get("status") == "Success" assert instance.results.response[0].get("MESSAGE", None) == "OK" assert instance.results.response[0].get("RETURN_CODE", None) == 200 assert instance.results.response[0].get("METHOD", None) == "POST" - value = "Configuration deployment completed." - assert instance.results.response[1].get("DATA", {}).get("status") == value - assert instance.results.response[1].get("MESSAGE", None) == "OK" - assert instance.results.response[1].get("RETURN_CODE", None) == 200 - assert instance.results.response[1].get("METHOD", None) == "POST" - assert instance.results.result[0].get("changed", None) is True assert instance.results.result[0].get("success", None) is True - assert instance.results.result[1].get("changed", None) is True - assert instance.results.result[1].get("success", None) is True + if deploy: + assert instance.results.diff[1].get("deploy_maintenance_mode", None) is True + assert instance.results.diff[1].get("sequence_number", None) == 2 + + assert ( + instance.results.metadata[1].get("action", None) + == "deploy_maintenance_mode" + ) + assert instance.results.metadata[1].get("sequence_number", None) == 2 + assert instance.results.metadata[1].get("state", None) == "merged" + + value = "Success" + assert instance.results.response[1].get("DATA", {}).get("status") == value + assert instance.results.response[1].get("MESSAGE", None) == "OK" + assert instance.results.response[1].get("RETURN_CODE", None) == 200 + assert instance.results.response[1].get("METHOD", None) == "POST" + + assert instance.results.result[1].get("changed", None) is True + assert instance.results.result[1].get("success", None) is True @pytest.mark.parametrize( @@ -900,7 +904,7 @@ def test_maintenance_mode_00800( Summary - Verify MaintenanceMode().deploy_switches() raises ``ValueError`` - when ``EpFabricConfigDeploy`` raises any of: + when ``EpMaintenanceModeDeploy`` raises any of: - ``TypeError`` - ``ValueError`` @@ -924,8 +928,10 @@ class MockEndpoint: """ def __init__(self): + self.class_name = "MockEpMaintenanceModeDeploy" self._fabric_name = None self._serial_number = None + self._wait_for_mode_change = False @property def fabric_name(self): @@ -950,8 +956,20 @@ def serial_number(self): def serial_number(self, value): self._serial_number = value + @property + def wait_for_mode_change(self): + """ + Mock wait_for_mode_change getter/setter + """ + return self._wait_for_mode_change + + @wait_for_mode_change.setter + def wait_for_mode_change(self, value): + self._wait_for_mode_change = value + def responses(): yield {"MESSAGE": "OK", "RETURN_CODE": 200, "DATA": {"status": "Success"}} + yield {"MESSAGE": "OK", "RETURN_CODE": 200, "DATA": {"status": "Success"}} sender = Sender() sender.gen = ResponseGenerator(responses()) @@ -1109,9 +1127,9 @@ def responses(): instance.results = Results() match = r"MaintenanceMode\.deploy_switches:\s+" - match += r"Unable to deploy switches:\s+" + match += r"Unable to deploy switch:\s+" match += r"fabric_name VXLAN_Fabric,\s+" - match += r"serial_numbers FDO22180ASJ\.\s+" + match += r"serial_number FDO22180ASJ\.\s+" match += r"Got response.*\." with pytest.raises(ValueError, match=match): instance.commit() From 336102b97c36570cec9ef44c21af84c294c0c31d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 20 Jun 2024 14:28:32 -1000 Subject: [PATCH 188/230] Add wait_for_mode_change playbook parameter Expose playbook parameter wait_for_mode_change. Default is currently false (which aligns with the NDFC GUI), but we can change this easily if needed. 1. test_maintenance_mode.py: Add wait_for_mode_change to CONFIG shared by unit tests. 2. tests/integration/targets/dcnm_maintenance_mode/*.yaml : Restructure tests. 3. dcnm_maintenance_mode.py: Update the following to handle wait_for_mode_change: - DOCUMENTATION - EXAMPLES - ParamsSpec() - Merged().get_need() --- .../module_utils/common/maintenance_mode.py | 67 ++++++- plugins/modules/dcnm_maintenance_mode.py | 27 ++- ...ance_mode_deploy_no_wait_switch_level.yaml | 173 +++++++++++++++++ ...rmal_mode_deploy_no_wait_switch_level.yaml | 174 ++++++++++++++++++ ...tenance_mode_deploy_no_wait_top_level.yaml | 165 +++++++++++++++++ ..._normal_mode_deploy_no_wait_top_level.yaml | 167 +++++++++++++++++ ..._merged_maintenance_mode_deploy_wait.yaml} | 54 +++--- ...07_merged_maintenance_mode_no_deploy.yaml} | 46 ++--- .../common/test_maintenance_mode.py | 1 + 9 files changed, 816 insertions(+), 58 deletions(-) create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_deploy_no_wait_switch_level.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/02_merged_normal_mode_deploy_no_wait_switch_level.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/03_merged_maintenance_mode_deploy_no_wait_top_level.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/04_merged_normal_mode_deploy_no_wait_top_level.yaml rename tests/integration/targets/dcnm_maintenance_mode/tests/{01_merged_maintenance_mode_deploy.yaml => 05_merged_maintenance_mode_deploy_wait.yaml} (91%) rename tests/integration/targets/dcnm_maintenance_mode/tests/{01_merged_maintenance_mode_no_deploy.yaml => 07_merged_maintenance_mode_no_deploy.yaml} (92%) diff --git a/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py index 4da069cc1..180c059d1 100644 --- a/plugins/module_utils/common/maintenance_mode.py +++ b/plugins/module_utils/common/maintenance_mode.py @@ -185,6 +185,7 @@ def verify_config_parameters(self, value) -> None: self.verify_ip_address(item) self.verify_mode(item) self.verify_serial_number(item) + self.verify_wait_for_mode_change(item) except (TypeError, ValueError) as error: raise ValueError(error) from error @@ -282,6 +283,29 @@ def verify_serial_number(self, item) -> None: msg += "config is missing mandatory key: serial_number." raise ValueError(msg) + def verify_wait_for_mode_change(self, item) -> None: + """ + ### Summary + Verify the ``wait_for_mode_change`` parameter. + + ### Raises + - ``ValueError`` if: + - ``wait_for_mode_change`` is not present. + - ``TypeError`` if: + - `wait_for_mode_change`` is not a boolean. + """ + method_name = inspect.stack()[0][3] + if item.get("wait_for_mode_change", None) is None: + msg = f"{self.class_name}.{method_name}: " + msg += "config is missing mandatory key: wait_for_mode_change." + raise ValueError(msg) + if not isinstance(item.get("wait_for_mode_change", None), bool): + msg = f"{self.class_name}.{method_name}: " + msg += "Expected boolean for wait_for_mode_change. " + msg += f"Got type {type(item).__name__}, " + msg += f"value {item.get('deploy', None)}." + raise TypeError(msg) + def verify_commit_parameters(self) -> None: """ ### Summary @@ -425,13 +449,32 @@ def build_deploy_dict(self) -> None: ### Structure - key: fabric_name - - value: list of serial_numbers to deploy for each fabric + - value: list of dict + - each dict contains ``serial_number`` and ``wait_for_mode_change keys`` ### Example ```json { - "MyFabric": ["CDM4593459", "CDM4593460"], - "YourFabric": ["CDM4593461", "CDM4593462"] + "MyFabric": [ + { + "serial_number": "CDM4593459", + "wait_for_mode_change": True + }, + { + "serial_number": "CDM4593460", + "wait_for_mode_change": False + } + ], + "YourFabric": [ + { + "serial_number": "DDM0455882", + "wait_for_mode_change": True + }, + { + "serial_number": "DDM5598759", + "wait_for_mode_change": True + } + ] } """ self.deploy_dict = {} @@ -439,10 +482,14 @@ def build_deploy_dict(self) -> None: fabric_name = item.get("fabric_name") serial_number = item.get("serial_number") deploy = item.get("deploy") + wait_for_mode_change = item.get("wait_for_mode_change") if fabric_name not in self.deploy_dict: self.deploy_dict[fabric_name] = [] + item_dict = {} if deploy is True: - self.deploy_dict[fabric_name].append(serial_number) + item_dict["serial_number"] = serial_number + item_dict["wait_for_mode_change"] = wait_for_mode_change + self.deploy_dict[fabric_name].append(item_dict) def build_serial_number_to_ip_address(self) -> None: """ @@ -481,21 +528,21 @@ def build_endpoints(self) -> None: """ method_name = inspect.stack()[0][3] endpoints = [] - for fabric_name, serial_numbers in self.deploy_dict.items(): - for serial_number in serial_numbers: + for fabric_name, switches in self.deploy_dict.items(): + for item in switches: endpoint = {} try: self.ep_maintenance_mode_deploy.fabric_name = fabric_name - self.ep_maintenance_mode_deploy.serial_number = serial_number - self.ep_maintenance_mode_deploy.wait_for_mode_change = True - except (TypeError, ValueError) as error: + self.ep_maintenance_mode_deploy.serial_number = item["serial_number"] + self.ep_maintenance_mode_deploy.wait_for_mode_change = item["wait_for_mode_change"] + except (KeyError, TypeError, ValueError) as error: msg = f"{self.class_name}.{method_name}: " msg += "Error resolving endpoint: " msg += f"Error details: {error}." raise ValueError(msg) from error endpoint["path"] = self.ep_maintenance_mode_deploy.path endpoint["verb"] = self.ep_maintenance_mode_deploy.verb - endpoint["serial_number"] = serial_number + endpoint["serial_number"] = self.ep_maintenance_mode_deploy.serial_number endpoint["fabric_name"] = fabric_name endpoints.append(copy.copy(endpoint)) self.endpoints = copy.copy(endpoints) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 5b5830188..0778ef612 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -47,6 +47,13 @@ default: False required: false type: bool + wait_for_mode_change: + description: + - If deploy is enabled, whether to wait for NDFC to push the change to the switch. + - Note: This option is ignored if deploy is not enabled. + default: False + required: false + type: bool mode: default: maintenance description: @@ -77,6 +84,13 @@ - Whether to deploy the switch configuration. required: false type: bool + wait_for_mode_change: + description: + - If deploy is enabled, whether to wait for NDFC to push the change to the switch. + - Note: This option is ignored if deploy is not enabled. + default: False + required: false + type: bool """ EXAMPLES = """ @@ -88,7 +102,8 @@ cisco.dcnm.dcnm_maintenance_mode: state: merged config: - deploy: false + deploy: true + wait_for_mode_change: true mode: maintenance switches: - ip_address: 192.168.1.2 @@ -113,6 +128,7 @@ mode: normal - ip_address: 192.160.1.3 deploy: true + wait_for_mode_change: true - ip_address: 192.160.1.4 register: result - debug: @@ -243,6 +259,11 @@ def _build_params_spec_for_merged_state(self) -> None: self._params_spec["deploy"]["type"] = "bool" self._params_spec["deploy"]["default"] = False + self._params_spec["wait_for_mode_change"] = {} + self._params_spec["wait_for_mode_change"]["required"] = False + self._params_spec["wait_for_mode_change"]["type"] = "bool" + self._params_spec["wait_for_mode_change"]["default"] = False + def _build_params_spec_for_query_state(self) -> None: """ Build the parameter specifications for ``query`` state. @@ -324,11 +345,13 @@ class Want: "ip_address": "192.168.1.2", "mode": "maintenance", "deploy": false + "wait_for_mode_change": false }, { "ip_address": "192.168.1.3", "mode": "normal", "deploy": true + "wait_for_mode_change": true } ] ``` @@ -875,7 +898,6 @@ def fabric_deployment_disabled(self) -> None: """ ### Summary Handle the following cases: - - switch migration mode is ``inconsistent`` - switch migration mode is ``migration`` - fabric is in read-only mode (IS_READ_ONLY is True) - fabric is in freeze mode (Deployment Disable) @@ -989,6 +1011,7 @@ def get_need(self): need.update({"ip_address": ip_address}) need.update({"mode": want.get("mode")}) need.update({"serial_number": serial_number}) + need.update({"wait_for_mode_change": want.get("wait_for_mode_change")}) self.need.append(copy.copy(need)) def commit(self): diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_deploy_no_wait_switch_level.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_deploy_no_wait_switch_level.yaml new file mode 100644 index 000000000..8f4510677 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_deploy_no_wait_switch_level.yaml @@ -0,0 +1,173 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 06:02.84 +################################################################################ +# DESCRIPTION +# Normal mode to maintenance mode using deploy-maintenance-mode endpoint. +# deploy is set to true. +# wait_for_mode_change is set to false. +# +# +# State: merged +# Tests: +# - All tests use deploy-maintenance-mode endpoint. +# 1. Change normal mode switches to maintenance mode using playbook switch-level. +# - top-level config is overridden by switch-level config. +# +# NOTES: +# - Execute either of the following testcases to setup the fabric and switches +# - 00_setup_fabrics_1x_rw.yaml (1x fabric with 2x switches) +# - 00_setup_fabrics_2x_rw.yaml (2x fabrics with 1x switch each) +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. +# 1. MERGED - SETUP - Ensure switch mode is normal +# TEST +# 2. Change switch mode to maintenance (switch-level) +# 3. Verify switch mode is maintenance (switch-level) +# CLEANUP +# No cleanup. +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: 01_merged_maintenance_mode_deploy_no_wait_switch_level +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# fabric_name_3: LAN_CLASSIC_Fabric +# fabric_type_3: LAN_CLASSIC +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 1. MERGED - SETUP - Ensure switch mode is normal +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - SETUP - Ensure switch mode is normal + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +################################################################################ +# 2. MERGED - TEST - Change switch mode to maintenance (switch-level) +# +# Override top-level config with switch-level config. +################################################################################ +- name: MERGED - TEST - Change switch mode to maintenance (switch-level) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: false + mode: normal + switches: + - ip_address: "{{ leaf_1 }}" + deploy: true + mode: maintenance + wait_for_mode_change: false + - ip_address: "{{ leaf_2 }}" + deploy: true + mode: maintenance + wait_for_mode_change: false + register: result_maintenance_mode +- debug: + var: result_maintenance_mode + +################################################################################ +# 7. MERGED - TEST - Verify switch mode is maintenance (switch-level) +################################################################################ +# Expected result (only relevant fields shown) +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "maintenance", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "maintenance", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is maintenance (switch-level) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "maintenance" + - result.diff[2][leaf_2].mode == "maintenance" + +- assert: + that: + - result_maintenance_mode.failed == false + - result_maintenance_mode.metadata[2].action == "change_sytem_mode" + - result_maintenance_mode.metadata[3].action == "change_sytem_mode" + - result_maintenance_mode.metadata[2].check_mode == False + - result_maintenance_mode.metadata[3].check_mode == False + - result_maintenance_mode.metadata[2].state == "merged" + - result_maintenance_mode.metadata[3].state == "merged" + - result_maintenance_mode.response[2].DATA.status == "Success" + - result_maintenance_mode.response[3].DATA.status == "Success" + - result_maintenance_mode.response[2].METHOD == "POST" + - result_maintenance_mode.response[3].METHOD == "POST" + - result_maintenance_mode.response[2].RETURN_CODE == 200 + - result_maintenance_mode.response[3].RETURN_CODE == 200 + - result_maintenance_mode.response[4].DATA.status is match 'Success' + - result_maintenance_mode.response[5].DATA.status is match 'Success' diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/02_merged_normal_mode_deploy_no_wait_switch_level.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/02_merged_normal_mode_deploy_no_wait_switch_level.yaml new file mode 100644 index 000000000..59d5fd41a --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/02_merged_normal_mode_deploy_no_wait_switch_level.yaml @@ -0,0 +1,174 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 03:29.21 +################################################################################ +# DESCRIPTION +# Maintenance mode to Normal mode using deploy-maintenance-mode endpoint. +# deploy is set to true. +# wait_for_mode_change is set to false. +# +# +# State: merged +# Tests: +# - All tests use deploy-maintenance-mode endpoint. +# 1. Change normal mode switches to maintenance mode using playbook switch-level. +# - top-level config is overridden by switch-level config. +# +# NOTES: +# - Execute either of the following testcases to setup the fabric and switches +# - 00_setup_fabrics_1x_rw.yaml (1x fabric with 2x switches) +# - 00_setup_fabrics_2x_rw.yaml (2x fabrics with 1x switch each) +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. +# 1. MERGED - SETUP - Ensure switch mode is maintenance +# TEST +# 1. Change switch mode to normal (switch-level) +# 2. Verify switch mode is normal (switch-level) +# CLEANUP +# No cleanup +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: 02_merged_normal_mode_deploy_no_wait_switch_level +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# fabric_name_3: LAN_CLASSIC_Fabric +# fabric_type_3: LAN_CLASSIC +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 1. MERGED - SETUP - Ensure switch mode is maintenance +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "maintenance", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "maintenance", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - SETUP - Ensure switch mode is maintenance + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "maintenance" + - result.diff[2][leaf_2].mode == "maintenance" + +################################################################################ +# 2. MERGED - TEST - Change switch mode to normal (switch-level) +# +# Override top-level config with switch-level config. +################################################################################ +- name: MERGED - TEST - Change switch mode to normal (switch-level) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: false + mode: maintenance + switches: + - ip_address: "{{ leaf_1 }}" + deploy: true + mode: normal + wait_for_mode_change: false + - ip_address: "{{ leaf_2 }}" + deploy: true + mode: normal + wait_for_mode_change: false + register: result_normal_mode +- debug: + var: result_normal_mode + +################################################################################ +# 3. MERGED - TEST - Verify switch mode is normal (switch-level) +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is normal (switch-level) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + + +- assert: + that: + - result_normal_mode.failed == false + - result_normal_mode.metadata[2].action == "change_sytem_mode" + - result_normal_mode.metadata[3].action == "change_sytem_mode" + - result_normal_mode.metadata[2].check_mode == False + - result_normal_mode.metadata[3].check_mode == False + - result_normal_mode.metadata[2].state == "merged" + - result_normal_mode.metadata[3].state == "merged" + - result_normal_mode.response[2].DATA.status == "Success" + - result_normal_mode.response[3].DATA.status == "Success" + - result_normal_mode.response[2].METHOD == "DELETE" + - result_normal_mode.response[3].METHOD == "DELETE" + - result_normal_mode.response[2].RETURN_CODE == 200 + - result_normal_mode.response[3].RETURN_CODE == 200 + - result_normal_mode.response[4].DATA.status is match 'Success' + - result_normal_mode.response[5].DATA.status is match 'Success' diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/03_merged_maintenance_mode_deploy_no_wait_top_level.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/03_merged_maintenance_mode_deploy_no_wait_top_level.yaml new file mode 100644 index 000000000..cf1991486 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/03_merged_maintenance_mode_deploy_no_wait_top_level.yaml @@ -0,0 +1,165 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 05:46.12 +################################################################################ +# DESCRIPTION +# Normal mode to maintenance mode using deploy-maintenance-mode endpoint. +# deploy is set to true. +# wait_for_mode_change is set to false. +# +# +# State: merged +# Tests: +# - All tests use deploy-maintenance-mode endpoint. +# 1. Change normal mode switches to maintenance mode using playbook top-level. +# +# NOTES: +# - Execute either of the following testcases to setup the fabric and switches +# - 00_setup_fabrics_1x_rw.yaml (1x fabric with 2x switches) +# - 00_setup_fabrics_2x_rw.yaml (2x fabrics with 1x switch each) +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. +# 1. MERGED - SETUP - Ensure switch mode is normal +# TEST +# 2. Change switch mode to maintenance (top-level) +# 3. Verify switch mode is maintenance (top-level) +# CLEANUP +# No cleanup. +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: 03_merged_maintenance_mode_deploy_no_wait_top_level +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# fabric_name_3: LAN_CLASSIC_Fabric +# fabric_type_3: LAN_CLASSIC +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 1. MERGED - SETUP - Ensure switch mode is normal +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - SETUP - Ensure switch mode is normal + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +################################################################################ +# 2. MERGED - TEST - Change switch mode to maintenance (top-level) +################################################################################ +- name: MERGED - TEST - Change switch mode to maintenance (top-level) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: true + mode: maintenance + wait_for_mode_change: false + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result_maintenance_mode +- debug: + var: result_maintenance_mode + +################################################################################ +# 3. MERGED - TEST - Verify switch mode is maintenance (top-level) +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "maintenance", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "maintenance", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is maintenance (top-level) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "maintenance" + - result.diff[2][leaf_2].mode == "maintenance" + +- assert: + that: + - result_maintenance_mode.failed == false + - result_maintenance_mode.metadata[2].action == "change_sytem_mode" + - result_maintenance_mode.metadata[3].action == "change_sytem_mode" + - result_maintenance_mode.metadata[2].check_mode == False + - result_maintenance_mode.metadata[3].check_mode == False + - result_maintenance_mode.metadata[2].state == "merged" + - result_maintenance_mode.metadata[3].state == "merged" + - result_maintenance_mode.response[2].DATA.status == "Success" + - result_maintenance_mode.response[3].DATA.status == "Success" + - result_maintenance_mode.response[2].METHOD == "POST" + - result_maintenance_mode.response[3].METHOD == "POST" + - result_maintenance_mode.response[2].RETURN_CODE == 200 + - result_maintenance_mode.response[3].RETURN_CODE == 200 + - result_maintenance_mode.response[4].DATA.status is match 'Success' + - result_maintenance_mode.response[5].DATA.status is match 'Success' diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/04_merged_normal_mode_deploy_no_wait_top_level.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/04_merged_normal_mode_deploy_no_wait_top_level.yaml new file mode 100644 index 000000000..3dcd1e0cd --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/04_merged_normal_mode_deploy_no_wait_top_level.yaml @@ -0,0 +1,167 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 03:55.49 +################################################################################ +# DESCRIPTION +# Maintenance mode to Normal mode using deploy-maintenance-mode endpoint. +# deploy is set to true. +# wait_for_mode_change is set to false. +# +# +# State: merged +# Tests: +# - All tests use deploy-maintenance-mode endpoint. +# 1. Change normal mode switches to maintenance mode using playbook top-level. +# +# NOTES: +# - Execute either of the following testcases to setup the fabric and switches +# - 00_setup_fabrics_1x_rw.yaml (1x fabric with 2x switches) +# - 00_setup_fabrics_2x_rw.yaml (2x fabrics with 1x switch each) +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 0. Run either of the following to create read-write fabrics and add switches: +# - 00_setup_fabrics_1x_rw +# - 00_setup_fabrics_2x_rw +# 1. MERGED - SETUP - Ensure switch mode is maintenance +# TEST +# 2. Change switch mode to normal (top-level) +# 3. Verify switch mode is normal (top-level) +# CLEANUP +# No cleanup. +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: 04_merged_normal_mode_deploy_no_wait_top_level +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# fabric_name_3: LAN_CLASSIC_Fabric +# fabric_type_3: LAN_CLASSIC +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 1. MERGED - SETUP - Ensure switch mode is maintenance +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "maintenance", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "maintenance", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - SETUP - Ensure switch mode is maintenance + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "maintenance" + - result.diff[2][leaf_2].mode == "maintenance" + +################################################################################ +# 2. MERGED - TEST - Change switch mode to normal (top-level) +################################################################################ +- name: MERGED - TEST - Change switch mode to normal (top-level) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: true + mode: normal + wait_for_mode_change: false + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result_normal_mode +- debug: + var: result_normal_mode + +################################################################################ +# 3. MERGED - TEST - Verify switch mode is normal (top-level) +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is normal (top-level) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +- assert: + that: + - result_normal_mode.failed == false + - result_normal_mode.metadata[2].action == "change_sytem_mode" + - result_normal_mode.metadata[3].action == "change_sytem_mode" + - result_normal_mode.metadata[2].check_mode == False + - result_normal_mode.metadata[3].check_mode == False + - result_normal_mode.metadata[2].state == "merged" + - result_normal_mode.metadata[3].state == "merged" + - result_normal_mode.response[2].DATA.status == "Success" + - result_normal_mode.response[3].DATA.status == "Success" + - result_normal_mode.response[2].METHOD == "DELETE" + - result_normal_mode.response[3].METHOD == "DELETE" + - result_normal_mode.response[2].RETURN_CODE == 200 + - result_normal_mode.response[3].RETURN_CODE == 200 + - result_normal_mode.response[4].DATA.status is match 'Success' + - result_normal_mode.response[5].DATA.status is match 'Success' diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_deploy.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/05_merged_maintenance_mode_deploy_wait.yaml similarity index 91% rename from tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_deploy.yaml rename to tests/integration/targets/dcnm_maintenance_mode/tests/05_merged_maintenance_mode_deploy_wait.yaml index cda036997..2d6b555e8 100644 --- a/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_deploy.yaml +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/05_merged_maintenance_mode_deploy_wait.yaml @@ -6,7 +6,10 @@ # 23:45.94 # 23:49.52 ################################################################################ -# DESCRIPTION - Normal mode to maintenance mode with deploy-maintenance-mode +# DESCRIPTION +# Normal mode to maintenance mode using deploy-maintenance-mode endpoint. +# deploy is set to true. +# wait_for_mode_change is set to true. # # State: merged # Tests: @@ -26,17 +29,17 @@ ################################################################################ # SETUP # 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. -# 1. MERGED - SETUP - ensure switches are in normal mode +# 1. MERGED - SETUP - Ensure switch mode is normal # TEST # GLOBAL CONFIG -# 2. Normal to Maintenance mode (global config) +# 2. Change switch mode to maintenance (global config) # 3. Verify switch mode is maintenance (global config) -# 4. Maintenance to Normal mode (global config) +# 4. Change switch mode to normal (global config) # 5. Verify switch mode is normal (global config) # SWITCH CONFIG -# 6. Normal to Maintenance mode (switch config) +# 6. Change switch mode to maintenance (switch config) # 7. Verify switch mode is maintenance (switch config) -# 8. Maintenance to Normal mode (switch config) +# 8. Change switch mode to normal (switch config) # 9. Verify switch mode is normal (switch config) # CLEANUP # No cleanup needed. @@ -48,7 +51,7 @@ # # vars: # # This testcase field can run any test in the tests directory for the role -# testcase: merged_normal_to_maintenance +# testcase: 05_merged_maintenance_mode_deploy_wait # fabric_name_1: VXLAN_EVPN_Fabric # fabric_type_1: VXLAN_EVPN # fabric_name_3: LAN_CLASSIC_Fabric @@ -58,7 +61,7 @@ # nxos_username: admin # nxos_password: mypassword ################################################################################ -# 1. MERGED - SETUP - ensure switches are in normal mode +# 1. MERGED - SETUP - Ensure switch mode is normal ################################################################################ # Expected result # ok: [172.22.150.244] => { @@ -83,7 +86,7 @@ # "sequence_number": 3 # } # ], -- name: MERGED - SETUP - ensure switches are in normal mode +- name: MERGED - SETUP - Ensure switch mode is normal cisco.dcnm.dcnm_maintenance_mode: state: query config: @@ -91,21 +94,22 @@ - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" register: result - retries: 40 + retries: 60 delay: 10 until: - result.diff[2][leaf_1].mode == "normal" - result.diff[2][leaf_2].mode == "normal" ################################################################################ -# 2. MERGED - TEST - Normal to maintenance mode (global config) +# 2. MERGED - TEST - Change switch mode to maintenance (global config) ################################################################################ -- name: MERGED - TEST - change switches to maintenance mode (global config) +- name: MERGED - TEST - Change switch mode to maintenance (global config) cisco.dcnm.dcnm_maintenance_mode: state: merged config: deploy: true mode: maintenance + wait_for_mode_change: true switches: - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" @@ -147,21 +151,22 @@ - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" register: result - retries: 40 + retries: 60 delay: 10 until: - result.diff[2][leaf_1].mode == "maintenance" - result.diff[2][leaf_2].mode == "maintenance" ################################################################################ -# 4. MERGED - TEST - Maintenance to Normal mode (global config) +# 4. MERGED - TEST - Change switch mode to normal (global config) ################################################################################ -- name: MERGED - TEST - change switches to normal mode (global config) +- name: MERGED - TEST - Change switch mode to normal (global config) cisco.dcnm.dcnm_maintenance_mode: state: merged config: deploy: true mode: normal + wait_for_mode_change: true switches: - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" @@ -203,7 +208,7 @@ - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" register: result - retries: 40 + retries: 60 delay: 10 until: - result.diff[2][leaf_1].mode == "normal" @@ -243,9 +248,9 @@ - result_normal_mode.response[5].DATA.status is match 'Success' ################################################################################ -# 6. MERGED - TEST - Normal to maintenance mode (switch config) +# 6. MERGED - TEST - Change switch mode to maintenance (switch config) ################################################################################ -- name: MERGED - TEST - change switches to maintenance mode (switch config) +- name: MERGED - TEST - Change switch mode to maintenance (switch config) cisco.dcnm.dcnm_maintenance_mode: state: merged config: @@ -253,8 +258,10 @@ switches: - ip_address: "{{ leaf_1 }}" mode: maintenance + wait_for_mode_change: true - ip_address: "{{ leaf_2 }}" mode: maintenance + wait_for_mode_change: true register: result_maintenance_mode - debug: var: result_maintenance_mode @@ -293,26 +300,27 @@ - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" register: result - retries: 40 + retries: 60 delay: 10 until: - result.diff[2][leaf_1].mode == "maintenance" - result.diff[2][leaf_2].mode == "maintenance" ################################################################################ -# 8. MERGED - TEST - Maintenance to Normal mode (switch config) +# 8. MERGED - TEST - Change switch mode to normal (switch config) ################################################################################ -- name: MERGED - TEST - Maintenance to Normal mode (switch config) +- name: MERGED - TEST - Change switch mode to normal (switch config) cisco.dcnm.dcnm_maintenance_mode: state: merged config: deploy: true - mode: normal switches: - ip_address: "{{ leaf_1 }}" mode: normal + wait_for_mode_change: true - ip_address: "{{ leaf_2 }}" mode: normal + wait_for_mode_change: true register: result_normal_mode - debug: var: result_normal_mode @@ -351,7 +359,7 @@ - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" register: result - retries: 40 + retries: 60 delay: 10 until: - result.diff[2][leaf_1].mode == "normal" diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_no_deploy.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/07_merged_maintenance_mode_no_deploy.yaml similarity index 92% rename from tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_no_deploy.yaml rename to tests/integration/targets/dcnm_maintenance_mode/tests/07_merged_maintenance_mode_no_deploy.yaml index 9d110feb6..c452e5f13 100644 --- a/tests/integration/targets/dcnm_maintenance_mode/tests/01_merged_maintenance_mode_no_deploy.yaml +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/07_merged_maintenance_mode_no_deploy.yaml @@ -28,17 +28,17 @@ ################################################################################ # SETUP # 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. -# 1. MERGED - SETUP - ensure switches are in normal mode +# 1. MERGED - SETUP - Ensure switch mode is normal # TEST # GLOBAL CONFIG -# 2. Normal to Maintenance mode (global config) +# 2. Change switch mode to maintenance (global config) # 3. Verify switch mode is inconsistent (global config) -# 4. Maintenance to Normal mode (global config) +# 4. Change switch mode to normal (global config) # 5. Verify switch mode is normal (global config) # SWITCH CONFIG -# 6. Normal to Maintenance mode (switch config) +# 6. Change switch mode to maintenance (switch config) # 7. Verify switch mode is inconsistent (switch config) -# 8. Maintenance to Normal mode (switch config) +# 8. Change switch mode to normal (switch config) # 9. Verify switch mode is normal (switch config) # CLEANUP # No cleanup needed. @@ -50,7 +50,7 @@ # # vars: # # This testcase field can run any test in the tests directory for the role -# testcase: merged_normal_to_maintenance +# testcase: 07_merged_maintenance_mode_no_deploy # fabric_name_1: VXLAN_EVPN_Fabric # fabric_type_1: VXLAN_EVPN # fabric_name_3: LAN_CLASSIC_Fabric @@ -60,7 +60,7 @@ # nxos_username: admin # nxos_password: mypassword ################################################################################ -# 1. MERGED - SETUP - ensure switches are in normal mode +# 1. MERGED - SETUP - Ensure switch mode is normal ################################################################################ # Expected result # ok: [172.22.150.244] => { @@ -93,16 +93,16 @@ - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" register: result - retries: 40 + retries: 60 delay: 10 until: - result.diff[2][leaf_1].mode == "normal" - result.diff[2][leaf_2].mode == "normal" ################################################################################ -# 2. MERGED - TEST - Normal to maintenance mode (global config) +# 2. MERGED - TEST - Change switch mode to maintenance (global config) ################################################################################ -- name: MERGED - TEST - change switches to maintenance mode (global config) +- name: MERGED - TEST - Change switch mode to maintenance (global config) cisco.dcnm.dcnm_maintenance_mode: state: merged config: @@ -149,16 +149,16 @@ - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" register: result - retries: 40 + retries: 60 delay: 10 until: - result.diff[2][leaf_1].mode == "inconsistent" - result.diff[2][leaf_2].mode == "inconsistent" ################################################################################ -# 5. MERGED - TEST - Maintenance to Normal mode (global config) +# 4. MERGED - TEST - Change switch mode to normal (global config) ################################################################################ -- name: MERGED - TEST - change switches to normal mode (global config) +- name: MERGED - TEST - Change switch mode to normal (global config) cisco.dcnm.dcnm_maintenance_mode: state: merged config: @@ -172,7 +172,7 @@ var: result_normal_mode ################################################################################ -# 6. MERGED - TEST - Verify switch mode is normal (global config) +# 5. MERGED - TEST - Verify switch mode is normal (global config) ################################################################################ # Expected result # ok: [172.22.150.244] => { @@ -205,7 +205,7 @@ - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" register: result - retries: 40 + retries: 60 delay: 10 until: - result.diff[2][leaf_1].mode == "normal" @@ -247,9 +247,9 @@ - result_normal_mode.response[3].RETURN_CODE == 200 ################################################################################ -# 7. MERGED - TEST - Normal to maintenance mode (switch config) +# 6. MERGED - TEST - Change switch mode to maintenance (switch config) ################################################################################ -- name: MERGED - TEST - change switches to maintenance mode (switch config) +- name: MERGED - TEST - Change switch mode to maintenance (switch config) cisco.dcnm.dcnm_maintenance_mode: state: merged config: @@ -264,7 +264,7 @@ var: result_maintenance_mode ################################################################################ -# 8. MERGED - TEST - Verify switch mode is inconsistent (switch config) +# 7. MERGED - TEST - Verify switch mode is inconsistent (switch config) ################################################################################ # Expected result (only relevant fields shown) # ok: [172.22.150.244] => { @@ -297,16 +297,16 @@ - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" register: result - retries: 40 + retries: 60 delay: 10 until: - result.diff[2][leaf_1].mode == "inconsistent" - result.diff[2][leaf_2].mode == "inconsistent" ################################################################################ -# 10. MERGED - TEST - Inconsistent to Normal mode (switch config) +# 8. MERGED - TEST - Change switch mode to normal (switch config) ################################################################################ -- name: MERGED - TEST - Inconsistent to Normal mode (switch config) +- name: MERGED - TEST - Change switch mode to normal (switch config) cisco.dcnm.dcnm_maintenance_mode: state: merged config: @@ -322,7 +322,7 @@ var: result_normal_mode ################################################################################ -# 11. MERGED - TEST - Verify switch mode is normal (switch config) +# 9. MERGED - TEST - Verify switch mode is normal (switch config) ################################################################################ # Expected result # ok: [172.22.150.244] => { @@ -355,7 +355,7 @@ - ip_address: "{{ leaf_1 }}" - ip_address: "{{ leaf_2 }}" register: result - retries: 40 + retries: 60 delay: 10 until: - result.diff[2][leaf_1].mode == "normal" diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 48797fd53..90b0bc0a0 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -60,6 +60,7 @@ "fabric_name": f"{FABRIC_NAME}", "ip_address": "192.168.1.2", "mode": "maintenance", + "wait_for_mode_change": False, "serial_number": "FDO22180ASJ", } ] From b533db8f8ef0f81fa2cda0be500f2bd39cae4c1a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 20 Jun 2024 14:36:33 -1000 Subject: [PATCH 189/230] Fix validate-modules DOCUMENTATION error. --- plugins/modules/dcnm_maintenance_mode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 0778ef612..861a807a5 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -49,8 +49,7 @@ type: bool wait_for_mode_change: description: - - If deploy is enabled, whether to wait for NDFC to push the change to the switch. - - Note: This option is ignored if deploy is not enabled. + - If deploy is enabled, whether to wait for NDFC to push the change to the switch. Ignored if deploy is not enabled. default: False required: false type: bool From b9cd831ee784e82ca203db3af3e0a2b8e283a328 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 20 Jun 2024 14:40:46 -1000 Subject: [PATCH 190/230] Same fix as last commit, but for switch-level. --- plugins/modules/dcnm_maintenance_mode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 861a807a5..099d3e2eb 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -85,8 +85,7 @@ type: bool wait_for_mode_change: description: - - If deploy is enabled, whether to wait for NDFC to push the change to the switch. - - Note: This option is ignored if deploy is not enabled. + - If deploy is enabled, whether to wait for NDFC to push the change to the switch. Ignored if deploy is not enabled. default: False required: false type: bool From 7a0ed4a9d971271fa6d17bd1c88cc217ca301675 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 20 Jun 2024 14:49:44 -1000 Subject: [PATCH 191/230] Merged().get_need(): Update docstring Merged().get_need(): Update docstring to include wait_for_mode_change in example JSON structure. --- plugins/modules/dcnm_maintenance_mode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 099d3e2eb..19c216908 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -980,6 +980,7 @@ def get_need(self): "ip_address": "172.22.150.2", "mode": "maintenance", "serial_number": "FCI1234567" + "wait_for_mode_change": true }, { "deploy": true, @@ -987,6 +988,7 @@ def get_need(self): "ip_address": "172.22.150.3", "mode": "normal", "serial_number": "HMD2345678" + "wait_for_mode_change": true } ] """ From 699eba19c7ef515ef4da5f3e9dca655c052c7b8d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 20 Jun 2024 15:09:37 -1000 Subject: [PATCH 192/230] MaintenanceMode: Add unit tests, wait_for_mode_change All changes are in test_maintenance_mode.py - renumber unit tests to position wait_for_mode_change test in logical order. - Add test case test_maintenance_mode_00700 for wait_for_mode_change. - Modify test_maintenance_mode_00310 to include wait_for_mode_change. --- .../common/test_maintenance_mode.py | 72 +++++++++++++++++-- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/tests/unit/module_utils/common/test_maintenance_mode.py b/tests/unit/module_utils/common/test_maintenance_mode.py index 90b0bc0a0..c18cd0793 100644 --- a/tests/unit/module_utils/common/test_maintenance_mode.py +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -586,7 +586,14 @@ def test_maintenance_mode_00300(maintenance_mode) -> None: @pytest.mark.parametrize( "remove_param", - [("deploy"), ("fabric_name"), ("ip_address"), ("mode"), ("serial_number")], + [ + ("deploy"), + ("fabric_name"), + ("ip_address"), + ("mode"), + ("serial_number"), + ("wait_for_mode_change"), + ], ) def test_maintenance_mode_00310(maintenance_mode, remove_param) -> None: """ @@ -604,6 +611,7 @@ def test_maintenance_mode_00310(maintenance_mode, remove_param) -> None: - ip_address is missing from config - mode is missing from config - serial_number is missing from config + - wait_for_mode_change is missing from config Code Flow - Setup @@ -791,6 +799,60 @@ def test_maintenance_mode_00600(maintenance_mode, param, raises) -> None: assert instance.config[0]["mode"] == param +@pytest.mark.parametrize( + "param, raises", + [ + (False, None), + (True, None), + (10, ValueError), + ("FOO", ValueError), + (["FOO"], ValueError), + ({"FOO": "BAR"}, ValueError), + ], +) +def test_maintenance_mode_00700(maintenance_mode, param, raises) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - verify_config_parameters() + - config.setter + + Summary + - Verify MaintenanceMode().verify_config_parameters() re-raises + - ``ValueError`` if: + - ``wait_for_mode_change`` raises ``TypeError`` + + Code Flow - Setup + - MaintenanceMode() is instantiated + + Code Flow - Test + - MaintenanceMode().config is set to a dict. + - The dict is updated with wait_for_mode_change set to valid and invalid + values of ``wait_for_mode_change`` + + Expected Result + - ``ValueError`` is raised when wait_for_mode_change is not a boolean + - Exception message matches expected + - Exception is not raised when wait_for_mode_change is a boolean + """ + + with does_not_raise(): + instance = maintenance_mode + + config = copy.deepcopy(CONFIG[0]) + config["wait_for_mode_change"] = param + match = r"MaintenanceMode\.verify_wait_for_mode_change:\s+" + match += r"Expected boolean for wait_for_mode_change\.\s+" + match += r"Got type\s+" + if raises: + with pytest.raises(raises, match=match): + instance.config = [config] + else: + instance.config = [config] + assert instance.config[0]["wait_for_mode_change"] == param + + @pytest.mark.parametrize( "endpoint_instance, mock_exception, expected_exception, mock_message", [ @@ -800,7 +862,7 @@ def test_maintenance_mode_00600(maintenance_mode, param, raises) -> None: ("ep_maintenance_mode_enable", ValueError, ValueError, "Bad value"), ], ) -def test_maintenance_mode_00700( +def test_maintenance_mode_00800( monkeypatch, maintenance_mode, endpoint_instance, @@ -889,7 +951,7 @@ def serial_number(self, value): ("ep_maintenance_mode_deploy", ValueError, ValueError, "Bad value"), ], ) -def test_maintenance_mode_00800( +def test_maintenance_mode_00900( monkeypatch, maintenance_mode, endpoint_instance, @@ -1001,7 +1063,7 @@ def responses(): (ValueError, ValueError, r"Converted ValueError to ValueError"), ], ) -def test_maintenance_mode_00900( +def test_maintenance_mode_01000( maintenance_mode, mock_exception, expected_exception, mock_message ) -> None: """ @@ -1075,7 +1137,7 @@ def responses(): instance.commit() -def test_maintenance_mode_01000(monkeypatch, maintenance_mode) -> None: +def test_maintenance_mode_01100(monkeypatch, maintenance_mode) -> None: """ Classes and Methods - MaintenanceMode() From 032c776ba3b014ce6b9ba5fdaa879d298e9efe0a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 21 Jun 2024 10:25:52 -1000 Subject: [PATCH 193/230] dcnm_maintenance_mode.py: 47% unit test coverage, more... 1. tests/unit/modules/dcnm/dcnm_maintenance_mode/* - Add initial set of tests, fixtures, and utils 2. dcnm_maintenance_mode.py - Update DOCUMENTATION 3. dcnm_maintenance_mode.py - ParamsSpec() - Add choices for mode - Add defaults for deploy, mode, wait_for_mode_change 4. dcnm_maintenance_mode.py - Common() - Update Raises section of docstring for __init__() - __init__(): raise ValueError if config is missing. - __init__(): raise TypeError if config is not a dict. 5. dcnm_maintenance_mode.py - Merged() - Catch TypeError when initializing Common() 6. dcnm_maintenance_mode.py - Query() - Catch TypeError when initializing Common() 7. module_utils/common/params_validate_v2.py - Fix KeyError when optional param is missing. --- .../module_utils/common/params_validate_v2.py | 13 +- plugins/modules/dcnm_maintenance_mode.py | 46 +- .../dcnm/dcnm_maintenance_mode/__init__.py | 0 .../dcnm/dcnm_maintenance_mode/fixture.py | 50 +++ .../fixtures/configs_Common.json | 126 ++++++ .../test_dcnm_maintenance_mode_common.py | 401 ++++++++++++++++++ .../dcnm/dcnm_maintenance_mode/utils.py | 253 +++++++++++ 7 files changed, 870 insertions(+), 19 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/__init__.py create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixture.py create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Common.json create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py diff --git a/plugins/module_utils/common/params_validate_v2.py b/plugins/module_utils/common/params_validate_v2.py index 680cad707..71300cd01 100644 --- a/plugins/module_utils/common/params_validate_v2.py +++ b/plugins/module_utils/common/params_validate_v2.py @@ -267,9 +267,9 @@ def _validate_parameters(self, spec, parameters): spec[param], parameters, param ) else: - parameters[param] = self._verify_type( - spec[param]["type"], parameters, param - ) + value = self._verify_type(spec[param]["type"], parameters, param) + if value is not None: + parameters[param] = value self._verify_choices( spec[param].get("choices", None), parameters[param], param @@ -358,6 +358,7 @@ def _verify_type(self, expected_type: str, params: Any, param: str): ### Raises - ``ValueError`` if expected_type is not in self.valid_expected_types. + - ``ValueError`` if a parameter is missing. - ``TypeError`` if value's type does not match the expected type. """ try: @@ -365,7 +366,11 @@ def _verify_type(self, expected_type: str, params: Any, param: str): except ValueError as error: raise ValueError(error) from error - value = params[param] + value = params.get(param, None) + # param is not a mandatory parameter and user has omitted it. + # We don't need to validate it. + if value is None: + return None if expected_type in self._ipaddress_types: try: self._ipaddress_guard(expected_type, value, param) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 19c216908..5a4c5edc2 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -42,19 +42,22 @@ required: true suboptions: deploy: + default: false description: - Whether to deploy the switch configurations. - default: False required: false type: bool wait_for_mode_change: + default: false description: - If deploy is enabled, whether to wait for NDFC to push the change to the switch. Ignored if deploy is not enabled. - default: False required: false type: bool mode: - default: maintenance + choices: + - maintenance + - normal + default: normal description: - Enable maintenance or normal mode on all switches. required: false @@ -63,7 +66,7 @@ description: - A list of target switches. - Per-switch options override the global options. - required: false + required: true type: list elements: dict suboptions: @@ -73,20 +76,24 @@ required: true type: str mode: + choices: + - maintenance + - normal + default: normal description: - Enable maintenance or normal mode for the switch. - required: true + required: false type: str deploy: - default: False + default: false description: - Whether to deploy the switch configuration. required: false type: bool wait_for_mode_change: + default: false description: - If deploy is enabled, whether to wait for NDFC to push the change to the switch. Ignored if deploy is not enabled. - default: False required: false type: bool """ @@ -249,18 +256,20 @@ def _build_params_spec_for_merged_state(self) -> None: self._params_spec["ip_address"]["type"] = "ipv4" self._params_spec["mode"] = {} + self._params_spec["mode"]["choices"] = ["normal", "maintenance"] + self._params_spec["mode"]["default"] = "normal" self._params_spec["mode"]["required"] = False self._params_spec["mode"]["type"] = "str" self._params_spec["deploy"] = {} + self._params_spec["deploy"]["default"] = False self._params_spec["deploy"]["required"] = False self._params_spec["deploy"]["type"] = "bool" - self._params_spec["deploy"]["default"] = False self._params_spec["wait_for_mode_change"] = {} + self._params_spec["wait_for_mode_change"]["default"] = False self._params_spec["wait_for_mode_change"]["required"] = False self._params_spec["wait_for_mode_change"]["type"] = "bool" - self._params_spec["wait_for_mode_change"]["default"] = False def _build_params_spec_for_query_state(self) -> None: """ @@ -733,6 +742,9 @@ def __init__(self, params): - ``ValueError`` if: - ``params`` does not contain ``check_mode`` - ``params`` does not contain ``state`` + - ``params`` does not contain ``config`` + - ``TypeError`` if: + - ``config`` is not a dict """ self.class_name = self.__class__.__name__ method_name = inspect.stack()[0][3] @@ -752,12 +764,16 @@ def __init__(self, params): msg += "state is required" raise ValueError(msg) - self.config = self.params.get("config") - if not isinstance(self.config, dict): + self.config = self.params.get("config", None) + if self.config is None: msg = f"{self.class_name}.{method_name}: " - msg = "expected dict type for self.config. " - msg += f"got {type(self.config).__name__}" + msg += "config is required" raise ValueError(msg) + if not isinstance(self.config, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "Expected dict type for self.config. " + msg += f"Got {type(self.config).__name__}" + raise TypeError(msg) self.results = Results() self.results.state = self.state @@ -812,7 +828,7 @@ def __init__(self, params): method_name = inspect.stack()[0][3] try: super().__init__(params) - except ValueError as error: + except (TypeError, ValueError) as error: msg = f"{self.class_name}.{method_name}: " msg += f"Error: {error}" raise ValueError(msg) from error @@ -1104,7 +1120,7 @@ def __init__(self, params): method_name = inspect.stack()[0][3] try: super().__init__(params) - except ValueError as error: + except (TypeError, ValueError) as error: msg = f"{self.class_name}.{method_name}: " msg += f"Error: {error}" raise ValueError(msg) from error diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/__init__.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixture.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixture.py new file mode 100644 index 000000000..bb3730787 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixture.py @@ -0,0 +1,50 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json +import os +import sys + +fixture_path = os.path.join(os.path.dirname(__file__), "fixtures") + + +def load_fixture(filename): + """ + load test inputs from json files + """ + path = os.path.join(fixture_path, f"{filename}.json") + + try: + with open(path, encoding="utf-8") as file_handle: + data = file_handle.read() + except IOError as exception: + msg = f"Exception opening test input file {filename}.json : " + msg += f"Exception detail: {exception}" + print(msg) + sys.exit(1) + + try: + fixture = json.loads(data) + except json.JSONDecodeError as exception: + msg = "Exception reading JSON contents in " + msg += f"test input file {filename}.json : " + msg += f"Exception detail: {exception}" + print(msg) + sys.exit(1) + + return fixture diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Common.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Common.json new file mode 100644 index 000000000..c889161ee --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Common.json @@ -0,0 +1,126 @@ +{ + "TEST_NOTES": [ + "Mocked playbook configurations for Common unit tests.", + "tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py", + "00070a - top-level config is inherited by all switches" + ], + "test_dcnm_maintenance_mode_common_00100a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_common_00110a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3", + "deploy": false, + "mode": "maintenance", + "wait_for_mode_change": false + } + ] + }, + "test_dcnm_maintenance_mode_common_00120a": { + "switches": [ + { + "ip_address": "192.168.1.2", + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true + }, + { + "ip_address": "192.168.1.3", + "deploy": false, + "mode": "maintenance", + "wait_for_mode_change": false + } + ] + }, + "test_dcnm_maintenance_mode_common_00130a": { + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3", + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true + } + ] + }, + "test_dcnm_maintenance_mode_common_00140a": { + "switches": [ + { + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true + }, + { + "ip_address": "192.168.1.3", + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true + } + ] + }, + "test_dcnm_maintenance_mode_common_00150a": { + "switches": [ + { + "ip_address": "192.168.1.2", + "mode": "foo" + }, + { + "ip_address": "192.168.1.3", + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true + } + ] + }, + "test_dcnm_maintenance_mode_common_00160a": { + "switches": [ + { + "ip_address": "192.168.1.2", + "deploy": "foo", + "mode": "maintenance", + "wait_for_mode_change": true + }, + { + "ip_address": "192.168.1.3", + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true + } + ] + }, + "test_dcnm_maintenance_mode_common_00170a": { + "switches": [ + { + "ip_address": "192.168.1.2", + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": "foo" + }, + { + "ip_address": "192.168.1.3", + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true + } + ] + } +} diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py new file mode 100644 index 000000000..ca2a9fe41 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py @@ -0,0 +1,401 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-import +# pylint: disable=redefined-outer-name +# pylint: disable=protected-access +# pylint: disable=unused-argument +# pylint: disable=invalid-name + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy +import inspect + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_maintenance_mode import \ + Common +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_maintenance_mode.utils import ( + common_fixture, configs_common, does_not_raise, params, responses_common) + + +def test_dcnm_maintenance_mode_common_00000(common) -> None: + """ + ### Classes and Methods + - Common + - __init__() + + ### Summary + - Verify the class attributes are initialized to expected values. + + ### Test + - Class attributes are initialized to expected values + - ``ValueError`` is not called + """ + with does_not_raise(): + instance = common + assert instance.class_name == "Common" + assert instance.state == "merged" + assert instance.check_mode is False + assert instance.have == {} + assert instance.payloads == {} + assert instance.query == [] + assert instance.want == [] + assert instance.results.class_name == "Results" + assert instance.results.state == "merged" + assert instance.results.check_mode is False + + +def test_dcnm_maintenance_mode_common_00010() -> None: + """ + ### Classes and Methods + - Common + - __init__() + + ### Summary + - Verify ``ValueError`` is raised. + - params is missing ``check_mode`` key/value. + """ + params_test = copy.deepcopy(params) + params_test.pop("check_mode", None) + match = r"Common\.__init__: check_mode is required" + with pytest.raises(ValueError, match=match): + Common(params_test) + + +def test_dcnm_maintenance_mode_common_00020() -> None: + """ + ### Classes and Methods + - Common + - __init__() + + ### Summary + - Verify ``ValueError`` is raised. + - params is missing ``state`` key/value. + """ + params_test = copy.deepcopy(params) + params_test.pop("state", None) + match = r"Common\.__init__: state is required" + with pytest.raises(ValueError, match=match): + Common(params_test) + + +def test_dcnm_maintenance_mode_common_00030() -> None: + """ + ### Classes and Methods + - Common + - __init__() + + ### Summary + - Verify ``ValueError`` is raised. + - params is missing ``config`` key/value. + """ + params_test = copy.deepcopy(params) + params_test.pop("config", None) + match = r"Common\.__init__: config is required" + with pytest.raises(ValueError, match=match): + Common(params_test) + + +def test_dcnm_maintenance_mode_common_00040() -> None: + """ + ### Classes and Methods + - Common + - __init__() + + ### Summary + - Verify ``TypeError`` is raised. + - config is not a dict. + """ + params_test = copy.deepcopy(params) + params_test.update({"config": 10}) + match = r"Common\.__init__: Expected dict type for self\.config\. Got int" + with pytest.raises(TypeError, match=match): + Common(params_test) + + +def test_dcnm_maintenance_mode_common_00100() -> None: + """ + ### Classes and Methods + - Common + - get_want() + + ### Summary + - Verify Common().get_want() builds expected want contents. + - All switches inherit top-level config. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_common(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + with does_not_raise(): + instance = Common(params_test) + instance.get_want() + assert isinstance(instance.config, dict) + assert instance.want[0].get("deploy", None) is True + assert instance.want[1].get("deploy", None) is True + assert instance.want[0].get("ip_address", None) == "192.168.1.2" + assert instance.want[1].get("ip_address", None) == "192.168.1.3" + assert instance.want[0].get("mode", None) == "normal" + assert instance.want[1].get("mode", None) == "normal" + assert instance.want[0].get("wait_for_mode_change", None) is True + assert instance.want[1].get("wait_for_mode_change", None) is True + + +def test_dcnm_maintenance_mode_common_00110() -> None: + """ + ### Classes and Methods + - Common + - get_want() + + ### Summary + - Verify Common().get_want() builds expected want contents. + - 192.168.1.2 inherits top-level config. + - 192.168.1.3 overrides top-level config. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_common(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + with does_not_raise(): + instance = Common(params_test) + instance.get_want() + assert isinstance(instance.config, dict) + assert instance.want[0].get("deploy", None) is True + assert instance.want[1].get("deploy", None) is False + assert instance.want[0].get("ip_address", None) == "192.168.1.2" + assert instance.want[1].get("ip_address", None) == "192.168.1.3" + assert instance.want[0].get("mode", None) == "normal" + assert instance.want[1].get("mode", None) == "maintenance" + assert instance.want[0].get("wait_for_mode_change", None) is True + assert instance.want[1].get("wait_for_mode_change", None) is False + + +def test_dcnm_maintenance_mode_common_00120() -> None: + """ + ### Classes and Methods + - Common + - get_want() + + ### Summary + - Verify Common().get_want() builds expected want contents. + - top-level config is missing. + - 192.168.1.2 uses switch-level config. + - 192.168.1.3 uses switch-level config. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_common(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + with does_not_raise(): + instance = Common(params_test) + instance.get_want() + assert isinstance(instance.config, dict) + assert instance.want[0].get("deploy", None) is True + assert instance.want[1].get("deploy", None) is False + assert instance.want[0].get("ip_address", None) == "192.168.1.2" + assert instance.want[1].get("ip_address", None) == "192.168.1.3" + assert instance.want[0].get("mode", None) == "normal" + assert instance.want[1].get("mode", None) == "maintenance" + assert instance.want[0].get("wait_for_mode_change", None) is True + assert instance.want[1].get("wait_for_mode_change", None) is False + + +def test_dcnm_maintenance_mode_common_00130() -> None: + """ + ### Classes and Methods + - Common + - get_want() + + ### Summary + - Verify Common().get_want() builds expected want contents. + - 192.168.1.2 missing all optional parameters, so default values + are provided. + - deploy default value is False. + - mode default value is "normal". + - wait_for_mode_change default value is False. + - 192.168.1.3 uses switch-level config. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_common(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + with does_not_raise(): + instance = Common(params_test) + instance.get_want() + assert isinstance(instance.config, dict) + assert instance.want[0].get("deploy", None) is False + assert instance.want[1].get("deploy", None) is True + assert instance.want[0].get("ip_address", None) == "192.168.1.2" + assert instance.want[1].get("ip_address", None) == "192.168.1.3" + assert instance.want[0].get("mode", None) == "normal" + assert instance.want[1].get("mode", None) == "maintenance" + assert instance.want[0].get("wait_for_mode_change", None) is False + assert instance.want[1].get("wait_for_mode_change", None) is True + + +def test_dcnm_maintenance_mode_common_00140() -> None: + """ + ### Classes and Methods + - Common + - get_want() + + ### Summary + - Verify ``ValueError`` is raised. + - switch is missing mandatory parameter ip_address + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_common(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + with does_not_raise(): + instance = Common(params_test) + match = r"ParamsValidate\._validate_parameters:\s+" + match += r"Playbook is missing mandatory parameter:\s+" + match += r"ip_address\." + with pytest.raises(ValueError, match=match): + instance.get_want() + + +def test_dcnm_maintenance_mode_common_00150() -> None: + """ + ### Classes and Methods + - Common + - get_want() + + ### Summary + - Verify ``ValueError`` is raised. + - 192.168.1.2 contains invalid choice for mode + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_common(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + with does_not_raise(): + instance = Common(params_test) + match = r"ParamsValidate._verify_choices:\s+" + match += r"Invalid value for parameter 'mode'\.\s+" + match += r"Expected one of \['normal', 'maintenance'\]\.\s+" + match += r"Got foo" + with pytest.raises(ValueError, match=match): + instance.get_want() + + +def test_dcnm_maintenance_mode_common_00160() -> None: + """ + ### Classes and Methods + - Common + - get_want() + + ### Summary + - Verify ``ValueError`` is raised. + - 192.168.1.2 contains non-boolean value for deploy + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_common(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + with does_not_raise(): + instance = Common(params_test) + match = r"ParamsValidate._invalid_type:\s+" + match += r"Invalid type for parameter 'deploy'\.\s+" + match += r"Expected bool\. Got 'foo'\.\s+" + match += r"Error detail: The value 'foo' is not a valid boolean\." + with pytest.raises(ValueError, match=match): + instance.get_want() + + +def test_dcnm_maintenance_mode_common_00170() -> None: + """ + ### Classes and Methods + - Common + - get_want() + + ### Summary + - Verify ``ValueError`` is raised. + - 192.168.1.2 contains non-boolean value for wait_for_mode_change + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_common(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + with does_not_raise(): + instance = Common(params_test) + match = r"ParamsValidate._invalid_type:\s+" + match += r"Invalid type for parameter 'wait_for_mode_change'\.\s+" + match += r"Expected bool\. Got 'foo'\.\s+" + match += r"Error detail: The value 'foo' is not a valid boolean\." + with pytest.raises(ValueError, match=match): + instance.get_want() diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py new file mode 100644 index 000000000..3f01ebd2a --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py @@ -0,0 +1,253 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +from contextlib import contextmanager + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ + AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_maintenance_mode import \ + Common +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_maintenance_mode.fixture import \ + load_fixture + +params = { + "state": "merged", + "config": {"switches": [{"ip_address": "172.22.150.105"}]}, + "check_mode": False, +} + + +class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + + check_mode = False + + params = { + "state": "merged", + "config": {"switches": [{"ip_address": "172.22.150.105"}]}, + "check_mode": False, + } + argument_spec = { + "config": {"required": True, "type": "dict"}, + "state": { + "default": "merged", + "choices": ["deleted", "overridden", "merged", "query", "replaced"], + }, + } + supports_check_mode = True + + @property + def state(self): + """ + return the state + """ + return self.params["state"] + + @state.setter + def state(self, value): + """ + set the state + """ + self.params["state"] = value + + @staticmethod + def fail_json(msg, **kwargs) -> AnsibleFailJson: + """ + mock the fail_json method + """ + raise AnsibleFailJson(msg, kwargs) + + def public_method_for_pylint(self): + """ + Add one public method to appease pylint + """ + + +# See the following for explanation of why fixtures are explicitely named +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html + + +@pytest.fixture(name="common") +def common_fixture(): + """ + return instance of Common() + """ + return Common(params) + + +@pytest.fixture(name="fabric_details_by_name_v2") +def fabric_details_by_name_v2_fixture(): + """ + mock FabricDetailsByName version 2 + """ + instance = MockAnsibleModule() + instance.state = "query" + instance.check_mode = False + return FabricDetailsByNameV2(instance.params) + + +@pytest.fixture(name="response_handler") +def response_handler_fixture(): + """ + mock ResponseHandler() + """ + return ResponseHandler() + + +@contextmanager +def does_not_raise(): + """ + A context manager that does not raise an exception. + """ + yield + + +def configs_common(key: str) -> dict: + """ + Return playbook configs for Common + """ + data_file = "configs_Common" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def payloads_merge(key: str) -> dict: + """ + Return payloads for Merge + """ + data_file = "payloads_Merge" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def payloads_query(key: str) -> dict: + """ + Return payloads for Query + """ + data_file = "payloads_Query" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def responses_common(key: str) -> dict: + """ + Return responses for Common + """ + data_file = "responses_Common" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def responses_merge(key: str) -> dict: + """ + Return responses for Merge + """ + data_file = "responses_Merge" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def responses_query(key: str) -> dict: + """ + Return responses for Query + """ + data_file = "responses_Query" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def responses_fabric_details_by_name_v2(key: str) -> dict: + """ + Return responses for FabricDetailsByName version 2 + """ + data_file = "responses_FabricDetailsByName_V2" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def responses_response_handler(key: str) -> dict: + """ + Return responses for ResponseHandler + """ + data_file = "responses_ResponseHandler" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def results_common(key: str) -> dict: + """ + Return results for Common + """ + data_file = "results_Common" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def results_merge(key: str) -> dict: + """ + Return results for Merge + """ + data_file = "results_Merge" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def results_query(key: str) -> dict: + """ + Return results for Query + """ + data_file = "results_Query" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def rest_send_response_current(key: str) -> dict: + """ + Mocked return values for RestSend().response_current property + """ + data_file = "response_current_RestSend" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def rest_send_result_current(key: str) -> dict: + """ + Mocked return values for RestSend().result_current property + """ + data_file = "result_current_RestSend" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data From e636a9c9c74daa5ab5c5e53b0561f155c8397871 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 21 Jun 2024 10:36:20 -1000 Subject: [PATCH 194/230] Fix missing import dcnm_maintenance_mode/utils.py: needed import for FabricDetailsByNameV2 --- tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py index 3f01ebd2a..3914a1281 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py @@ -24,6 +24,8 @@ AnsibleFailJson from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.fabric_details_v2 import \ + FabricDetailsByName as FabricDetailsByNameV2 from ansible_collections.cisco.dcnm.plugins.modules.dcnm_maintenance_mode import \ Common from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_maintenance_mode.fixture import \ From cda2a56dd846df4e3c694dc4b7e31cefae388bac Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 21 Jun 2024 13:57:27 -1000 Subject: [PATCH 195/230] dcnm_maintenance_mode.py: 49% unit test coverage 1. Add testcase: - test_dcnm_maintenance_mode_common_00180 - Verify ``ValueError`` is raised. - params contains invalid value for ``state`` 2. Remove self._properties in favor of underescore _vars for properties. 3. ParamsSpec().results: remove unused property and update class docstring. --- plugins/modules/dcnm_maintenance_mode.py | 44 +++++++++---------- .../fixtures/configs_Common.json | 16 +++++++ .../test_dcnm_maintenance_mode_common.py | 29 ++++++++++++ 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 5a4c5edc2..4326866ac 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -219,8 +219,7 @@ def __init__(self): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED ParamsSpec()") - self._properties = {} - self._properties["params"] = None + self._params = None self._params_spec: dict = {} self.valid_states = ["merged", "query"] @@ -297,7 +296,7 @@ def params(self) -> dict: - setter: set the params - setter: raise ``ValueError`` if value is not a dict """ - return self._properties["params"] + return self._params @params.setter def params(self, value: dict) -> None: @@ -309,7 +308,7 @@ def params(self, value: dict) -> None: msg += "expected dict type for value. " msg += f"got {type(value).__name__}." raise ValueError(msg) - self._properties["params"] = value + self._params = value class Want: @@ -336,7 +335,6 @@ class Want: instance = Want() instance.params = ansible_module.params instance.params_spec = ParamsSpec() - instance.results = Results() instance.items_key = "switches" instance.validator = ParamsValidate() instance.commit() @@ -370,14 +368,12 @@ def __init__(self): self.log = logging.getLogger(f"dcnm.{self.class_name}") self.log.debug("ENTERED Want()") - self._properties = {} - self._properties["config"] = None - self._properties["items_key"] = None - self._properties["params"] = None - self._properties["params_spec"] = None - self._properties["results"] = None - self._properties["validator"] = None - self._properties["want"] = [] + self._config = None + self._items_key = None + self._params = None + self._params_spec = None + self._validator = None + self._want = [] self.merged_configs = [] self.item_configs = [] @@ -581,7 +577,7 @@ def config(self): - setter: set config - setter: raise ``ValueError`` if value is not a dict """ - return self._properties["config"] + return self._config @config.setter def config(self, value) -> None: @@ -590,7 +586,7 @@ def config(self, value) -> None: msg += "expected dict for value. " msg += f"got {type(value).__name__}." raise TypeError(msg) - self._properties["config"] = value + self._config = value @property def items_key(self) -> str: @@ -602,7 +598,7 @@ def items_key(self) -> str: - setter: set the items_key - setter: raise ``ValueError`` if value is not a string """ - return self._properties["items_key"] + return self._items_key @items_key.setter def items_key(self, value: str) -> None: @@ -614,7 +610,7 @@ def items_key(self, value: str) -> None: msg += "expected string type for value. " msg += f"got {type(value).__name__}." raise TypeError(msg) - self._properties["items_key"] = value + self._items_key = value @property def want(self) -> list: @@ -622,7 +618,7 @@ def want(self) -> list: ### Summary Return the want list. See class docstring for structure details. """ - return self._properties["want"] + return self._want @property def params(self) -> dict: @@ -641,7 +637,7 @@ def params(self) -> dict: ### setter Set params """ - return self._properties["params"] + return self._params @params.setter def params(self, value: dict) -> None: @@ -653,7 +649,7 @@ def params(self, value: dict) -> None: msg += "expected dict type for value. " msg += f"got {type(value).__name__}." raise TypeError(msg) - self._properties["params"] = value + self._params = value @property def params_spec(self): @@ -675,7 +671,7 @@ def params_spec(self): ### setter Set params_spec """ - return self._properties["params_spec"] + return self._params_spec @params_spec.setter def params_spec(self, value) -> None: @@ -692,7 +688,7 @@ def params_spec(self, value) -> None: raise TypeError(msg) from error if _class_have != _class_need: raise TypeError(msg) - self._properties["params_spec"] = value + self._params_spec = value @property def validator(self): @@ -710,7 +706,7 @@ def validator(self): ### setter Set validator """ - return self._properties["validator"] + return self._validator @validator.setter def validator(self, value) -> None: @@ -727,7 +723,7 @@ def validator(self, value) -> None: raise TypeError(msg) from error if _class_have != _class_need: raise TypeError(msg) - self._properties["validator"] = value + self._validator = value @Properties.add_rest_send diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Common.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Common.json index c889161ee..e6cd1b333 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Common.json +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Common.json @@ -122,5 +122,21 @@ "wait_for_mode_change": true } ] + }, + "test_dcnm_maintenance_mode_common_00180a": { + "switches": [ + { + "ip_address": "192.168.1.2", + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true + }, + { + "ip_address": "192.168.1.3", + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true + } + ] } } diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py index ca2a9fe41..2b0adf5a3 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py @@ -399,3 +399,32 @@ def configs(): match += r"Error detail: The value 'foo' is not a valid boolean\." with pytest.raises(ValueError, match=match): instance.get_want() + + +def test_dcnm_maintenance_mode_common_00180() -> None: + """ + ### Classes and Methods + - Common + - get_want() + + ### Summary + - Verify ``ValueError`` is raised. + - params contains invalid value for ``state`` + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_common(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + params_test.update({"state": "foo"}) + with does_not_raise(): + instance = Common(params_test) + match = r"ParamsSpec.commit:\s+" + match += r"Invalid state foo\. Expected one of merged, query\." + with pytest.raises(ValueError, match=match): + instance.get_want() From 8ecc190ae771af6f705ff6207aa9980f8e2c5253 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 21 Jun 2024 14:02:52 -1000 Subject: [PATCH 196/230] Fix PEP8 trailing-whitespace --- .../dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py index 2b0adf5a3..6ffb49a4d 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py @@ -409,7 +409,7 @@ def test_dcnm_maintenance_mode_common_00180() -> None: ### Summary - Verify ``ValueError`` is raised. - - params contains invalid value for ``state`` + - params contains invalid value for ``state`` """ method_name = inspect.stack()[0][3] key = f"{method_name}a" From c50f7076c1f01d4db8afa0ed520c5732d62d2b59 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 21 Jun 2024 16:27:16 -1000 Subject: [PATCH 197/230] dcnm_maintenance_mode.py: 53% unit test coverage. 1. Added initial unit tests for Want() 2. Want().__init__(): instantiate MergeDicts() as self.merge_dicts to enable monkeypatching. 3. Want(): improve error messages. 4. test_dcnm_maintenance_mode_common.py: remove unused imports. --- plugins/modules/dcnm_maintenance_mode.py | 43 +- .../fixtures/configs_Want.json | 124 ++++++ .../test_dcnm_maintenance_mode_common.py | 8 +- .../test_dcnm_maintenance_mode_want.py | 407 ++++++++++++++++++ .../dcnm/dcnm_maintenance_mode/utils.py | 10 + 5 files changed, 570 insertions(+), 22 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Want.json create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 4326866ac..fb5728315 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -333,6 +333,7 @@ class Want: ```python try: instance = Want() + instance.config = playbook_config instance.params = ansible_module.params instance.params_spec = ParamsSpec() instance.items_key = "switches" @@ -375,6 +376,7 @@ def __init__(self): self._validator = None self._want = [] + self.merge_dicts = MergeDicts() self.merged_configs = [] self.item_configs = [] @@ -390,11 +392,11 @@ def generate_params_spec(self) -> None: # Generate the params_spec used to validate the configs if self.params is None: msg = f"{self.class_name}.generate_params_spec(): " - msg += "self.params is required" + msg += "params is not set, and is required." raise ValueError(msg) if self.params_spec is None: msg = f"{self.class_name}.generate_params_spec(): " - msg += "self.params_spec is required" + msg += "params_spec is not set, and is required." raise ValueError(msg) try: @@ -474,25 +476,34 @@ def commit(self) -> None: if self.validator is None: msg = f"{self.class_name}.{method_name}: " - msg += f"self.validator must be set before calling {method_name}" + msg += f"self.validator must be set before calling {method_name}." raise ValueError(msg) try: self.generate_params_spec() except ValueError as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Error generating params_spec. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error try: self._merge_global_and_item_configs() except ValueError as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Error merging global and item configs. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error self.build_merged_configs() try: self.validate_configs() except ValueError as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Error validating playbook configs against params spec. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error def _merge_global_and_item_configs(self) -> None: """ @@ -518,15 +529,15 @@ def _merge_global_and_item_configs(self) -> None: if self.config is None: msg = f"{self.class_name}.{method_name}: " - msg += "self.config is required" + msg += "config is not set, and is required." raise ValueError(msg) if self.items_key is None: msg = f"{self.class_name}.{method_name}: " - msg += "self.items_key is required" + msg += "items_key is not set, and is required." raise ValueError(msg) if not self.config.get(self.items_key): msg = f"{self.class_name}.{method_name}: " - msg += f"playbook is missing list of {self.items_key}" + msg += f"playbook is missing list of {self.items_key}." raise ValueError(msg) self.item_configs = [] @@ -547,14 +558,16 @@ def _merge_global_and_item_configs(self) -> None: msg += f"{json.dumps(item, indent=4, sort_keys=True)}" self.log.debug(msg) - merge_dicts = MergeDicts() try: - merge_dicts.dict1 = global_config - merge_dicts.dict2 = item - merge_dicts.commit() - item_config = merge_dicts.dict_merged + self.merge_dicts.dict1 = global_config + self.merge_dicts.dict2 = item + self.merge_dicts.commit() + item_config = self.merge_dicts.dict_merged except (TypeError, ValueError) as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Error in MergeDicts(). " + msg += f"Error detail: {error}" + raise ValueError(msg) from error msg = f"{self.class_name}.{method_name}: " msg += "switch POST_MERGE: " diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Want.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Want.json new file mode 100644 index 000000000..b4e233696 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Want.json @@ -0,0 +1,124 @@ +{ + "TEST_NOTES": [ + "Mocked playbook configurations for Common unit tests.", + "tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py", + "00070a - top-level config is inherited by all switches" + ], + "test_dcnm_maintenance_mode_want_00100a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_want_00110a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_want_00120a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_want_00121a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_want_00130a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_want_00131a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_want_00132a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_want_00133a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_want_00140a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + } +} diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py index 6ffb49a4d..cb00bf718 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py @@ -17,10 +17,6 @@ # Due to the above, we also need to disable unused-import # Also, fixtures need to use *args to match the signature of the function they are mocking # pylint: disable=unused-import -# pylint: disable=redefined-outer-name -# pylint: disable=protected-access -# pylint: disable=unused-argument -# pylint: disable=invalid-name from __future__ import absolute_import, division, print_function @@ -33,14 +29,12 @@ import inspect import pytest -from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ - Results from ansible_collections.cisco.dcnm.plugins.modules.dcnm_maintenance_mode import \ Common from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_maintenance_mode.utils import ( - common_fixture, configs_common, does_not_raise, params, responses_common) + common_fixture, configs_common, does_not_raise, params) def test_dcnm_maintenance_mode_common_00000(common) -> None: diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py new file mode 100644 index 000000000..c313c8c1e --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py @@ -0,0 +1,407 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=protected-access +# pylint: disable=use-implicit-booleaness-not-comparison + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy +import inspect + +import pytest +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_maintenance_mode import ( + ParamsSpec, Want) +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.test_params_validate_v2 import \ + ParamsValidate +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_maintenance_mode.utils import ( + configs_want, does_not_raise, params) + + +def test_dcnm_maintenance_mode_want_00000() -> None: + """ + ### Classes and Methods + - Common + - __init__() + + ### Summary + - Verify the class attributes are initialized to expected values. + + ### Test + - Class attributes are initialized to expected values + - ``ValueError`` is not called + """ + with does_not_raise(): + instance = Want() + assert instance.class_name == "Want" + assert instance._config is None + assert instance._items_key is None + assert instance._params is None + assert instance._params_spec is None + assert instance._validator is None + assert instance._want == [] + assert instance.merged_configs == [] + assert instance.item_configs == [] + + +def test_dcnm_maintenance_mode_want_00100() -> None: + """ + ### Classes and Methods + - Want() + - commit() + + ### Summary + - Verify ``commit()`` happy path. + - No exceptions are raised. + - want contains expected structure and values. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_want(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + + with does_not_raise(): + instance = Want() + instance.items_key = "switches" + instance.config = params_test.get("config") + instance.params = params_test + instance.params_spec = ParamsSpec() + instance.validator = ParamsValidate() + instance.commit() + assert instance.want[0].get("deploy", None) is True + assert instance.want[0].get("ip_address", None) == "192.168.1.2" + assert instance.want[0].get("mode", None) == "normal" + assert instance.want[0].get("wait_for_mode_change", None) is True + assert instance.want[1].get("deploy", None) is True + assert instance.want[1].get("ip_address", None) == "192.168.1.3" + assert instance.want[1].get("mode", None) == "normal" + assert instance.want[1].get("wait_for_mode_change", None) is True + + +def test_dcnm_maintenance_mode_want_00110() -> None: + """ + ### Classes and Methods + - Want() + - commit() + + ### Summary + - Verify ``ValueError`` is raised. + - Want().validator is not set prior to calling commit(). + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_want(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + + with does_not_raise(): + instance = Want() + instance.items_key = "switches" + instance.config = params_test.get("config") + instance.params = params_test + instance.params_spec = ParamsSpec() + match = r"Want.commit:\s+" + match += r"self\.validator must be set before calling commit\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_dcnm_maintenance_mode_want_00120() -> None: + """ + ### Classes and Methods + - Want() + - commit() + + ### Summary + - Verify Want().commit() catches and re-raises ``ValueError``. + - Want().generate_params_spec() raises ``ValueError`` because + ``params`` is not set. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_want(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + + with does_not_raise(): + instance = Want() + instance.items_key = "switches" + instance.config = params_test.get("config") + instance.params_spec = ParamsSpec() + instance.validator = ParamsValidate() + match = r"Want.commit:\s+" + match += r"Error generating params_spec\.\s+" + match += r"Error detail:\s+" + match += r"Want\.generate_params_spec\(\):\s+" + match += r"params is not set, and is required\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_dcnm_maintenance_mode_want_00121() -> None: + """ + ### Classes and Methods + - Want() + - commit() + + ### Summary + - Verify Want().commit() catches and re-raises ``ValueError``. + - Want().generate_params_spec() raises ``ValueError`` because + ``params_spec`` is not set. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_want(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + + with does_not_raise(): + instance = Want() + instance.items_key = "switches" + instance.config = params_test.get("config") + instance.params = params_test + instance.validator = ParamsValidate() + match = r"Want.commit:\s+" + match += r"Error generating params_spec\.\s+" + match += r"Error detail:\s+" + match += r"Want\.generate_params_spec\(\):\s+" + match += r"params_spec is not set, and is required\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_dcnm_maintenance_mode_want_00130() -> None: + """ + ### Classes and Methods + - Want() + - commit() + + ### Summary + - Verify Want().commit() catches and re-raises ``ValueError``. + - Want()._merge_global_and_item_configs() raises ``ValueError`` + because ``config`` is not set, and is required. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_want(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + + with does_not_raise(): + instance = Want() + instance.items_key = "switches" + instance.params = params_test + instance.params_spec = ParamsSpec() + instance.validator = ParamsValidate() + match = r"Want.commit:\s+" + match += r"Error merging global and item configs\.\s+" + match += r"Error detail:\s+" + match += r"Want\._merge_global_and_item_configs:\s+" + match += r"config is not set, and is required\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_dcnm_maintenance_mode_want_00131() -> None: + """ + ### Classes and Methods + - Want() + - commit() + + ### Summary + - Verify Want().commit() catches and re-raises ``ValueError``. + - Want()._merge_global_and_item_configs() raises ``ValueError`` + because ``config`` is not set, and is required. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_want(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + + with does_not_raise(): + instance = Want() + instance.config = params_test.get("config") + instance.params = params_test + instance.params_spec = ParamsSpec() + instance.validator = ParamsValidate() + match = r"Want.commit:\s+" + match += r"Error merging global and item configs\.\s+" + match += r"Error detail:\s+" + match += r"Want\._merge_global_and_item_configs:\s+" + match += r"items_key is not set, and is required\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_dcnm_maintenance_mode_want_00132() -> None: + """ + ### Classes and Methods + - Want() + - commit() + + ### Summary + - Verify Want().commit() catches and re-raises ``ValueError``. + - Want()._merge_global_and_item_configs() raises ``ValueError`` + because ``config`` is missing the key specified by items_key. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_want(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + + with does_not_raise(): + instance = Want() + instance.config = params_test.get("config") + instance.items_key = "NOT_PRESENT_IN_CONFIG" + instance.params = params_test + instance.params_spec = ParamsSpec() + instance.validator = ParamsValidate() + match = r"Want.commit:\s+" + match += r"Error merging global and item configs\.\s+" + match += r"Error detail:\s+" + match += r"Want\._merge_global_and_item_configs:\s+" + match += r"playbook is missing list of NOT_PRESENT_IN_CONFIG\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_dcnm_maintenance_mode_want_00133(monkeypatch) -> None: + """ + ### Classes and Methods + - Want() + - commit() + + ### Summary + - Verify Want().commit() catches and re-raises ``ValueError``. + - Want()._merge_global_and_item_configs() raises ``ValueError`` + because MergeDict().commit() raises ``ValueError``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_want(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + + class MockMergeDicts: + def commit(): + raise ValueError("MergeDicts().commit(). ValueError.") + + with does_not_raise(): + instance = Want() + monkeypatch.setattr(instance, "merge_dicts", MockMergeDicts) + instance.config = params_test.get("config") + instance.items_key = "switches" + instance.params = params_test + instance.params_spec = ParamsSpec() + instance.validator = ParamsValidate() + match = r"Want.commit: Error merging global and item configs\.\s+" + match += r"Error detail:\s+" + match += r"Want\._merge_global_and_item_configs:\s+" + match += r"Error in MergeDicts\(\)\.\s+" + match += r"Error detail: MergeDicts\(\)\.commit\(\)\. ValueError\." + with pytest.raises(ValueError, match=match): + instance.commit() + + +def test_dcnm_maintenance_mode_want_00140(monkeypatch) -> None: + """ + ### Classes and Methods + - Want() + - commit() + + ### Summary + - Verify Want().commit() catches and re-raises ``ValueError``. + - Want().validate_configs() raises ``ValueError``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def configs(): + yield configs_want(key) + + gen = ResponseGenerator(configs()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen.next}) + + def mock_def(): + raise ValueError("validate_configs ValueError.") + + with does_not_raise(): + instance = Want() + monkeypatch.setattr(instance, "validate_configs", mock_def) + instance.config = params_test.get("config") + instance.params = params_test + instance.params_spec = ParamsSpec() + instance.items_key = "switches" + instance.validator = ParamsValidate() + match = r"Want.commit:\s+" + match += r"Error validating playbook configs against params spec\.\s+" + match += r"Error detail: validate_configs ValueError\." + with pytest.raises(ValueError, match=match): + instance.commit() diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py index 3914a1281..c2ecb373e 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py @@ -135,6 +135,16 @@ def configs_common(key: str) -> dict: return data +def configs_want(key: str) -> dict: + """ + Return playbook configs for Want + """ + data_file = "configs_Want" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + def payloads_merge(key: str) -> dict: """ Return payloads for Merge From 4d8b3730edb2a7b0598f37c21828353df0f9768e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 21 Jun 2024 16:49:09 -1000 Subject: [PATCH 198/230] Fix pylint no-method-argument in MockMergeDicts() --- .../dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py index c313c8c1e..2e7d66d8a 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py @@ -348,6 +348,7 @@ def configs(): params_test.update({"config": gen.next}) class MockMergeDicts: + @staticmethod def commit(): raise ValueError("MergeDicts().commit(). ValueError.") From 24766e9cef6e5f8f3c40e9b0f0814ff663e387f5 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 23 Jun 2024 15:56:25 -1000 Subject: [PATCH 199/230] dcnm_maintenance_mode.py: 77% unit test coverage. Added testcases for Want(), Merged(). --- plugins/modules/dcnm_maintenance_mode.py | 24 +- .../fixtures/configs_Merged.json | 109 +++ .../fixtures/responses_EpAllSwitches.json | 234 +++++ .../fixtures/responses_EpFabrics.json | 136 +++ .../responses_EpMaintenanceModeDeploy.json | 42 + .../responses_EpMaintenanceModeDisable.json | 24 + .../responses_EpMaintenanceModeEnable.json | 33 + .../test_dcnm_maintenance_mode_merged.py | 841 ++++++++++++++++++ .../test_dcnm_maintenance_mode_want.py | 2 +- .../dcnm/dcnm_maintenance_mode/utils.py | 64 +- 10 files changed, 1491 insertions(+), 18 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Merged.json create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeDeploy.json create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeDisable.json create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeEnable.json create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index fb5728315..395c3444b 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -764,19 +764,19 @@ def __init__(self, params): self.check_mode = self.params.get("check_mode", None) if self.check_mode is None: msg = f"{self.class_name}.{method_name}: " - msg += "check_mode is required" + msg += "check_mode is required." raise ValueError(msg) self.state = self.params.get("state", None) if self.state is None: msg = f"{self.class_name}.{method_name}: " - msg += "state is required" + msg += "state is required." raise ValueError(msg) self.config = self.params.get("config", None) if self.config is None: msg = f"{self.class_name}.{method_name}: " - msg += "config is required" + msg += "config is required." raise ValueError(msg) if not isinstance(self.config, dict): msg = f"{self.class_name}.{method_name}: " @@ -784,6 +784,8 @@ def __init__(self, params): msg += f"Got {type(self.config).__name__}" raise TypeError(msg) + self._rest_send = None + self.results = Results() self.results.state = self.state self.results.check_mode = self.check_mode @@ -839,7 +841,8 @@ def __init__(self, params): super().__init__(params) except (TypeError, ValueError) as error: msg = f"{self.class_name}.{method_name}: " - msg += f"Error: {error}" + msg += "Error during super().__init__(). " + msg += f"Error detail: {error}" raise ValueError(msg) from error self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -1023,8 +1026,7 @@ def get_need(self): ip_address = want.get("ip_address", None) if ip_address not in self.have: msg = f"{self.class_name}.{method_name}: " - msg += f"Switch {ip_address} in fabric {fabric_name} " - msg += "not found on the controller." + msg += f"Switch {ip_address} not found on the controller." raise ValueError(msg) serial_number = self.have[ip_address]["serial_number"] @@ -1063,7 +1065,10 @@ def commit(self): try: self.get_want() except ValueError as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Error while retrieving playbook config. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error if len(self.want) == 0: return @@ -1080,7 +1085,10 @@ def commit(self): try: self.send_need() except ValueError as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Error while sending maintenance mode request. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error def send_need(self) -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Merged.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Merged.json new file mode 100644 index 000000000..c27c0ccb8 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Merged.json @@ -0,0 +1,109 @@ +{ + "TEST_NOTES": [ + "Mocked playbook configurations for Common unit tests.", + "tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py", + "00070a - top-level config is inherited by all switches" + ], + "test_dcnm_maintenance_mode_merged_00100a": { + "deploy": true, + "mode": "normal", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_merged_00110a": { + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_merged_00115a": { + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_merged_00120a": { + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.4" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_merged_00130a": { + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + } + ] + }, + "test_dcnm_maintenance_mode_merged_00140a": { + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + } + ] + }, + "test_dcnm_maintenance_mode_merged_00150a": { + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + } + ] + }, + "test_dcnm_maintenance_mode_merged_00300a": { + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + } + ] + }, + "test_dcnm_maintenance_mode_merged_00600a": { + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + } + ] + } +} diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json new file mode 100644 index 000000000..f7e44e803 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json @@ -0,0 +1,234 @@ +{ + "TEST_NOTES": [ + "Mocked SwitchDetails() responses for Merged unit tests.", + "tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py" + ], + "test_dcnm_maintenance_mode_merged_00100a": { + "DATA": [ + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1312-leaf", + "mode": "Maintenance", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD2222222GA", + "switchRole": "leaf", + "systemMode": "Maintenance" + }, + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.3", + "logicalName": "cvd-1313-leaf", + "mode": "Maintenance", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD3333333GA", + "switchRole": "leaf", + "systemMode": "Maintenance" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00110a": { + "DATA": [ + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1312-leaf", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD2222222GA", + "switchRole": "leaf", + "systemMode": "Normal" + }, + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.3", + "logicalName": "cvd-1313-leaf", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD3333333GA", + "switchRole": "leaf", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00115a": { + "DATA": [ + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1312-leaf", + "mode": "Maintenance", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD2222222GA", + "switchRole": "leaf", + "systemMode": "Maintenance" + }, + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.3", + "logicalName": "cvd-1313-leaf", + "mode": "Maintenance", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD3333333GA", + "switchRole": "leaf", + "systemMode": "Maintenance" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00120a": { + "DATA": [ + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1312-leaf", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD2222222GA", + "switchRole": "leaf", + "systemMode": "Normal" + }, + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.3", + "logicalName": "cvd-1313-leaf", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD3333333GA", + "switchRole": "leaf", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00130a": { + "DATA": [ + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1312-leaf", + "mode": "Migration", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD2222222GA", + "switchRole": "leaf", + "systemMode": "Migration" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00140a": { + "DATA": [ + { + "fabricName": "LAN_Classic_Fabric", + "freezeMode": false, + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1312-leaf", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "monitorMode": true, + "serialNumber": "FD2222222GA", + "switchRole": "leaf", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00150a": { + "DATA": [ + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": true, + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1312-leaf", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD2222222GA", + "switchRole": "leaf", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00300a": { + "DATA": [ + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1312-leaf", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD2222222GA", + "switchRole": "leaf", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00600a": { + "DATA": [ + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1312-leaf", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD2222222GA", + "switchRole": "leaf", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + } +} diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json new file mode 100644 index 000000000..66f6ec40e --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json @@ -0,0 +1,136 @@ +{ + "TEST_NOTES": [ + "Mocked EpFabrics responses.", + "tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py" + ], + "test_dcnm_maintenance_mode_merged_00100a": { + "DATA": [ + { + "nvPairs": { + "DEPLOYMENT_FREEZE": "false", + "FABRIC_NAME": "VXLAN_EVPN_Fabric" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00110a": { + "DATA": [ + { + "nvPairs": { + "DEPLOYMENT_FREEZE": "false", + "FABRIC_NAME": "VXLAN_EVPN_Fabric" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00115a": { + "DATA": [ + { + "nvPairs": { + "DEPLOYMENT_FREEZE": "false", + "FABRIC_NAME": "VXLAN_EVPN_Fabric" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00120a": { + "DATA": [ + { + "nvPairs": { + "DEPLOYMENT_FREEZE": "false", + "FABRIC_NAME": "VXLAN_EVPN_Fabric" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00130a": { + "DATA": [ + { + "nvPairs": { + "DEPLOYMENT_FREEZE": "false", + "FABRIC_NAME": "VXLAN_EVPN_Fabric" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00140a": { + "DATA": [ + { + "nvPairs": { + "DEPLOYMENT_FREEZE": "false", + "FABRIC_NAME": "LAN_Classic_Fabric", + "IS_READ_ONLY": "true" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00150a": { + "DATA": [ + { + "nvPairs": { + "DEPLOYMENT_FREEZE": "true", + "FABRIC_NAME": "VXLAN_EVPN_Fabric", + "IS_READ_ONLY": "false" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00300a": { + "DATA": [ + { + "nvPairs": { + "DEPLOYMENT_FREEZE": "false", + "FABRIC_NAME": "VXLAN_EVPN_Fabric", + "IS_READ_ONLY": "false" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00600a": { + "DATA": [ + { + "nvPairs": { + "DEPLOYMENT_FREEZE": "false", + "FABRIC_NAME": "VXLAN_EVPN_Fabric", + "IS_READ_ONLY": "false" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + } +} diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeDeploy.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeDeploy.json new file mode 100644 index 000000000..052855c78 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeDeploy.json @@ -0,0 +1,42 @@ +{ + "TEST_NOTES": [ + "Mocked responses for endpoint EpMaintenanceModeDeploy (deploy-maintenance-mode)", + "tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py" + ], + "test_dcnm_maintenance_mode_merged_00100a": { + "DATA": { + "status": "Success" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_EVPN_Fabric/switches/FD2222222GA/deploy-maintenance-mode", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00100b": { + "DATA": { + "status": "Success" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_EVPN_Fabric/switches/FD3333333GA/deploy-maintenance-mode", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00110a": { + "DATA": { + "status": "Success" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_EVPN_Fabric/switches/FD2222222GA/deploy-maintenance-mode", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00110b": { + "DATA": { + "status": "Success" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_EVPN_Fabric/switches/FD3333333GA/deploy-maintenance-mode", + "RETURN_CODE": 200 + } +} diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeDisable.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeDisable.json new file mode 100644 index 000000000..41cb36ee9 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeDisable.json @@ -0,0 +1,24 @@ +{ + "TEST_NOTES": [ + "Mocked EpMaintenanceModeDisable() responses.", + "tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py" + ], + "test_dcnm_maintenance_mode_merged_00110a": { + "DATA": { + "status": "Success" + }, + "MESSAGE": "OK", + "METHOD": "DELETE", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_EVPN_Fabric/switches/FD2222222GA/maintenance-mode", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00110b": { + "DATA": { + "status": "Success" + }, + "MESSAGE": "OK", + "METHOD": "DELETE", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_EVPN_Fabric/switches/FD3333333GA/maintenance-mode", + "RETURN_CODE": 200 + } +} diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeEnable.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeEnable.json new file mode 100644 index 000000000..abcf0c65a --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpMaintenanceModeEnable.json @@ -0,0 +1,33 @@ +{ + "TEST_NOTES": [ + "Mocked EpMaintenanceModeEnable() responses.", + "tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py" + ], + "test_dcnm_maintenance_mode_merged_00100a": { + "DATA": { + "status": "Success" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_EVPN_Fabric/switches/FD2222222GA/maintenance-mode", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00100b": { + "DATA": { + "status": "Success" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_EVPN_Fabric/switches/FD3333333GA/maintenance-mode", + "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_merged_00300a": { + "DATA": { + "status": "Success" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/VXLAN_EVPN_Fabric/switches/FD3333333GA/maintenance-mode", + "RETURN_CODE": 200 + } +} diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py new file mode 100644 index 000000000..7e3550538 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py @@ -0,0 +1,841 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=protected-access +# pylint: disable=use-implicit-booleaness-not-comparison + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy +import inspect + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_maintenance_mode import ( + Merged) +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_maintenance_mode.utils import ( + MockAnsibleModule, configs_merged, does_not_raise, params, + responses_ep_all_switches, responses_ep_fabrics, + responses_ep_maintenance_mode_deploy, + responses_ep_maintenance_mode_disable, + responses_ep_maintenance_mode_enable) + + +def test_dcnm_maintenance_mode_merged_00000() -> None: + """ + ### Classes and Methods + - Common + - __init__() + + ### Summary + - Verify the class attributes are initialized to expected values. + + ### Test + - Class attributes are initialized to expected values. + - Exception is not raised. + """ + with does_not_raise(): + instance = Merged(params) + switches = instance.config.get("switches", None) + + assert instance.class_name == "Merged" + assert instance.log.name == "dcnm.Merged" + + assert instance.check_mode is False + assert instance.state == "merged" + + assert isinstance(instance.config, dict) + assert isinstance(switches, list) + assert switches[0].get("ip_address", None) == "192.168.1.2" + + assert instance.have == {} + assert instance.need == [] + assert instance.payloads == {} + assert instance.query == [] + assert instance.want == [] + + assert instance.results.class_name == "Results" + assert instance.results.state == "merged" + assert instance.results.check_mode is False + + +def test_dcnm_maintenance_mode_merged_00100() -> None: + """ + ### Classes and Methods + - Merged() + - commit() + + ### Summary + - Verify ``commit()`` happy path. + - Change switch mode from maintenance to normal. + - No exceptions are raised. + - want contains expected structure and values. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_merged(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield responses_ep_all_switches(f"{key}a") + yield responses_ep_fabrics(f"{key}a") + yield responses_ep_maintenance_mode_enable(f"{key}a") + yield responses_ep_maintenance_mode_enable(f"{key}b") + yield responses_ep_maintenance_mode_deploy(f"{key}a") + yield responses_ep_maintenance_mode_deploy(f"{key}b") + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Merged(params) + instance.rest_send = rest_send + instance.config = params_test.get("config") + instance.commit() + assert instance.want[0].get("deploy", None) is True + assert instance.want[0].get("ip_address", None) == "192.168.1.2" + assert instance.want[0].get("mode", None) == "normal" + assert instance.want[0].get("wait_for_mode_change", None) is True + assert instance.want[1].get("deploy", None) is True + assert instance.want[1].get("ip_address", None) == "192.168.1.3" + assert instance.want[1].get("mode", None) == "normal" + assert instance.want[1].get("wait_for_mode_change", None) is True + + assert instance.results.diff[2]["maintenance_mode"] == "normal" + assert instance.results.diff[3]["maintenance_mode"] == "normal" + assert instance.results.diff[4]["deploy_maintenance_mode"] is True + assert instance.results.diff[5]["deploy_maintenance_mode"] is True + + assert instance.results.metadata[0]["action"] == "switch_details" + assert instance.results.metadata[1]["action"] == "fabric_details" + assert instance.results.metadata[2]["action"] == "change_sytem_mode" + assert instance.results.metadata[3]["action"] == "change_sytem_mode" + assert instance.results.metadata[4]["action"] == "deploy_maintenance_mode" + assert instance.results.metadata[5]["action"] == "deploy_maintenance_mode" + + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[1]["state"] == "merged" + assert instance.results.metadata[2]["state"] == "merged" + assert instance.results.metadata[3]["state"] == "merged" + assert instance.results.metadata[4]["state"] == "merged" + assert instance.results.metadata[5]["state"] == "merged" + + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[1]["check_mode"] is False + assert instance.results.metadata[2]["check_mode"] is False + assert instance.results.metadata[3]["check_mode"] is False + assert instance.results.metadata[4]["check_mode"] is False + assert instance.results.metadata[5]["check_mode"] is False + + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True + + assert instance.results.result[2]["changed"] is True + assert instance.results.result[3]["changed"] is True + assert instance.results.result[4]["changed"] is True + assert instance.results.result[5]["changed"] is True + + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True + assert instance.results.result[2]["success"] is True + assert instance.results.result[3]["success"] is True + assert instance.results.result[4]["success"] is True + assert instance.results.result[5]["success"] is True + + +def test_dcnm_maintenance_mode_merged_00110() -> None: + """ + ### Classes and Methods + - Merged() + - commit() + + ### Summary + - Verify ``commit()`` happy path. + - Change switch mode from normal to maintenance. + - No exceptions are raised. + - want contains expected structure and values. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_merged(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield responses_ep_all_switches(f"{key}a") + yield responses_ep_fabrics(f"{key}a") + yield responses_ep_maintenance_mode_disable(f"{key}a") + yield responses_ep_maintenance_mode_disable(f"{key}b") + yield responses_ep_maintenance_mode_deploy(f"{key}a") + yield responses_ep_maintenance_mode_deploy(f"{key}b") + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Merged(params) + instance.rest_send = rest_send + instance.config = params_test.get("config") + instance.commit() + assert instance.want[0].get("deploy", None) is True + assert instance.want[0].get("ip_address", None) == "192.168.1.2" + assert instance.want[0].get("mode", None) == "maintenance" + assert instance.want[0].get("wait_for_mode_change", None) is True + assert instance.want[1].get("deploy", None) is True + assert instance.want[1].get("ip_address", None) == "192.168.1.3" + assert instance.want[1].get("mode", None) == "maintenance" + assert instance.want[1].get("wait_for_mode_change", None) is True + + assert instance.results.diff[2]["maintenance_mode"] == "maintenance" + assert instance.results.diff[3]["maintenance_mode"] == "maintenance" + assert instance.results.diff[4]["deploy_maintenance_mode"] is True + assert instance.results.diff[5]["deploy_maintenance_mode"] is True + + assert instance.results.metadata[0]["action"] == "switch_details" + assert instance.results.metadata[1]["action"] == "fabric_details" + assert instance.results.metadata[2]["action"] == "change_sytem_mode" + assert instance.results.metadata[3]["action"] == "change_sytem_mode" + assert instance.results.metadata[4]["action"] == "deploy_maintenance_mode" + assert instance.results.metadata[5]["action"] == "deploy_maintenance_mode" + + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[1]["state"] == "merged" + assert instance.results.metadata[2]["state"] == "merged" + assert instance.results.metadata[3]["state"] == "merged" + assert instance.results.metadata[4]["state"] == "merged" + assert instance.results.metadata[5]["state"] == "merged" + + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[1]["check_mode"] is False + assert instance.results.metadata[2]["check_mode"] is False + assert instance.results.metadata[3]["check_mode"] is False + assert instance.results.metadata[4]["check_mode"] is False + assert instance.results.metadata[5]["check_mode"] is False + + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True + + assert instance.results.result[2]["changed"] is True + assert instance.results.result[3]["changed"] is True + assert instance.results.result[4]["changed"] is True + assert instance.results.result[5]["changed"] is True + + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True + assert instance.results.result[2]["success"] is True + assert instance.results.result[3]["success"] is True + assert instance.results.result[4]["success"] is True + assert instance.results.result[5]["success"] is True + + +def test_dcnm_maintenance_mode_merged_00115() -> None: + """ + ### Classes and Methods + - Merged() + - commit() + + ### Summary + - Verify ``commit()`` happy path. + - User wants to change switches to maintenance mode, but all + switches are already in maintenance mode. + - send_need() returns without sending any requests since + instance.need is empty. + - No exceptions are raised. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_merged(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield responses_ep_all_switches(f"{key}a") + yield responses_ep_fabrics(f"{key}a") + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Merged(params) + instance.rest_send = rest_send + instance.config = params_test.get("config") + instance.commit() + + assert len(instance.need) == 0 + + assert instance.results.metadata[0]["action"] == "switch_details" + assert instance.results.metadata[1]["action"] == "fabric_details" + + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[1]["state"] == "merged" + + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[1]["check_mode"] is False + + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True + + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True + + +def test_dcnm_maintenance_mode_merged_00120() -> None: + """ + ### Classes and Methods + - Merged() + - get_need() + - commit() + + ### Summary + - Verify ``get_have()`` raises ``ValueError`` when ip_address + does not exist on the controller. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_merged(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield responses_ep_all_switches(f"{key}a") + yield responses_ep_fabrics(f"{key}a") + yield responses_ep_maintenance_mode_disable(f"{key}a") + yield responses_ep_maintenance_mode_disable(f"{key}b") + yield responses_ep_maintenance_mode_deploy(f"{key}a") + yield responses_ep_maintenance_mode_deploy(f"{key}b") + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Merged(params) + instance.rest_send = rest_send + instance.config = params_test.get("config") + match = r"Merged\.get_have:\s+" + match += r"Error while retrieving switch info\.\s+" + match += r"Error detail: SwitchDetails\._get:\s+" + match += r"Switch with ip_address 192\.168\.1\.4 does not exist on the controller\." + with pytest.raises(ValueError, match=match): + instance.commit() + + assert len(instance.results.diff) == 2 + + assert instance.results.metadata[0]["action"] == "switch_details" + assert instance.results.metadata[1]["action"] == "fabric_details" + + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[1]["state"] == "merged" + + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[1]["check_mode"] is False + + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True + + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True + + +def test_dcnm_maintenance_mode_merged_00130() -> None: + """ + ### Classes and Methods + - Merged() + - fabric_deployment_disabled() + - commit() + + ### Summary + - Verify ``fabric_deployment_disabled()`` raises ``ValueError`` when + have ip_address is in migration mode. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_merged(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield responses_ep_all_switches(f"{key}a") + yield responses_ep_fabrics(f"{key}a") + yield responses_ep_maintenance_mode_disable(f"{key}a") + yield responses_ep_maintenance_mode_disable(f"{key}b") + yield responses_ep_maintenance_mode_deploy(f"{key}a") + yield responses_ep_maintenance_mode_deploy(f"{key}b") + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Merged(params) + instance.rest_send = rest_send + instance.config = params_test.get("config") + match = r"Merged\.fabric_deployment_disabled:\s+" + match += r"Switch maintenance mode is in migration state\s+" + match += r"for the switch with ip_address 192\.168\.1\.2,\s+" + match += r"serial_number FD2222222GA\.\s+" + match += r"This indicates that the switch configuration is not compatible\s+" + match += r"with the switch role in the hosting fabric\.\s+" + match += r"The issue might be resolved by initiating a fabric\s+" + match += r"Recalculate \& Deploy on the controller\.\s+" + match += r"Failing that, the switch configuration might need to be\s+" + match += r"manually modified to match the switch role in the hosting\s+" + match += r"fabric\.\s+" + match += r"Additional info:\s+" + match += r"hosting_fabric: VXLAN_EVPN_Fabric,\s+" + match += r"fabric_deployment_disabled: False,\s+" + match += r"fabric_freeze_mode: False,\s+" + match += r"fabric_read_only: False,\s+" + match += r"maintenance_mode: migration\." + with pytest.raises(ValueError, match=match): + instance.commit() + + assert len(instance.results.diff) == 2 + + assert instance.results.metadata[0]["action"] == "switch_details" + assert instance.results.metadata[1]["action"] == "fabric_details" + + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[1]["state"] == "merged" + + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[1]["check_mode"] is False + + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True + + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True + + +def test_dcnm_maintenance_mode_merged_00140() -> None: + """ + ### Classes and Methods + - Merged() + - fabric_deployment_disabled() + - commit() + + ### Summary + - Verify ``fabric_deployment_disabled()`` raises ``ValueError`` when + the fabric is in read-only mode. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_merged(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield responses_ep_all_switches(f"{key}a") + yield responses_ep_fabrics(f"{key}a") + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Merged(params) + instance.rest_send = rest_send + instance.config = params_test.get("config") + match = r"Merged\.fabric_deployment_disabled:\s+" + match += r"The hosting fabric is in read-only mode for the switch with\s+" + match += r"ip_address 192\.168\.1\.2,\s+" + match += r"serial_number FD2222222GA\.\s+" + match += r"The issue can be resolved for LAN_Classic fabrics by\s+" + match += r"unchecking 'Fabric Monitor Mode' in the fabric settings\s+" + match += r"on the controller\.\s+" + match += r"Additional info:\s+" + match += r"hosting_fabric: LAN_Classic_Fabric,\s+" + match += r"fabric_deployment_disabled: True,\s+" + match += r"fabric_freeze_mode: False,\s+" + match += r"fabric_read_only: True,\s+" + match += r"maintenance_mode: normal\.\s+" + with pytest.raises(ValueError, match=match): + instance.commit() + + assert len(instance.results.diff) == 2 + + assert instance.results.metadata[0]["action"] == "switch_details" + assert instance.results.metadata[1]["action"] == "fabric_details" + + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[1]["state"] == "merged" + + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[1]["check_mode"] is False + + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True + + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True + + +def test_dcnm_maintenance_mode_merged_00150() -> None: + """ + ### Classes and Methods + - Merged() + - fabric_deployment_disabled() + - commit() + + ### Summary + - Verify ``fabric_deployment_disabled()`` raises ``ValueError`` when + fabric freeze-mode is True. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_merged(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield responses_ep_all_switches(f"{key}a") + yield responses_ep_fabrics(f"{key}a") + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Merged(params) + instance.rest_send = rest_send + instance.config = params_test.get("config") + match = r"Merged\.fabric_deployment_disabled:\s+" + match += ( + r"The hosting fabric is in 'Deployment Disable' state for the switch with\s+" + ) + match += r"ip_address 192\.168\.1\.2,\s+" + match += r"serial_number FD2222222GA\.\s+" + match += r"Review the 'Deployment Enable / Deployment Disable' setting on the controller at:\s+" + match += r"Fabric Controller > Overview > Topology > \s+" + match += r"> Actions > More, and change the setting to 'Deployment Enable'\.\s+" + match += r"Additional info:\s+" + match += r"hosting_fabric: VXLAN_EVPN_Fabric,\s+" + match += r"fabric_deployment_disabled: True,\s+" + match += r"fabric_freeze_mode: True,\s+" + match += r"fabric_read_only: False,\s+" + match += r"maintenance_mode: normal\.\s+" + with pytest.raises(ValueError, match=match): + instance.commit() + + assert len(instance.results.diff) == 2 + + assert instance.results.metadata[0]["action"] == "switch_details" + assert instance.results.metadata[1]["action"] == "fabric_details" + + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[1]["state"] == "merged" + + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[1]["check_mode"] is False + + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True + + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True + + +def test_dcnm_maintenance_mode_merged_00200() -> None: + """ + ### Classes and Methods + - Merged() + - fabric_deployment_disabled() + - commit() + + ### Summary + - Verify ``commit()`` raises ``ValueError`` when rest_send has not + been set. + """ + with does_not_raise(): + instance = Merged(params) + match = r"Merged\.commit:\s+" + match += r"rest_send must be set before calling commit\." + with pytest.raises(ValueError, match=match): + instance.commit() + + assert len(instance.results.diff) == 0 + assert len(instance.results.metadata) == 0 + assert len(instance.results.response) == 0 + assert len(instance.results.result) == 0 + + +def test_dcnm_maintenance_mode_merged_00300(monkeypatch) -> None: + """ + ### Classes and Methods + - Merged() + - get_need() + - commit() + + ### Summary + - Verify ``get_need()`` raises ``ValueError`` when ip_address + does not exist in self.have. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_merged(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Merged(params) + instance.rest_send = rest_send + instance.config = params_test.get("config") + + def mock_get_have(): + return {} + + match = r"Merged\.get_need: Switch 192\.168\.1\.2 not found\s+" + match += r"on the controller\." + with pytest.raises(ValueError, match=match): + monkeypatch.setattr(instance, "get_have", mock_get_have) + instance.commit() + + assert len(instance.results.diff) == 0 + assert len(instance.results.metadata) == 0 + assert len(instance.results.response) == 0 + assert len(instance.results.result) == 0 + + +def test_dcnm_maintenance_mode_merged_00400(monkeypatch) -> None: + """ + ### Classes and Methods + - Merged() + - get_want() + - commit() + + ### Summary + - Verify ``commit`` re-raises ``ValueError`` when ``get_want()`` + raises ``ValueError``. + """ + params_test = copy.deepcopy(params) + params_test.update({"config": {}}) + + with does_not_raise(): + instance = Merged(params) + instance.rest_send = RestSend(params) + instance.config = params_test.get("config") + + def mock_get_want(): + raise ValueError("get_want(): Mocked ValueError.") + + match = r"Merged\.commit:\s+" + match += r"Error while retrieving playbook config\.\s+" + match += r"Error detail: get_want\(\): Mocked ValueError\." + with pytest.raises(ValueError, match=match): + monkeypatch.setattr(instance, "get_want", mock_get_want) + instance.commit() + + assert len(instance.results.diff) == 0 + assert len(instance.results.metadata) == 0 + assert len(instance.results.response) == 0 + assert len(instance.results.result) == 0 + + +def test_dcnm_maintenance_mode_merged_00500() -> None: + """ + ### Classes and Methods + - Merged() + - __init__() + + ### Summary + - Verify ``__init__`` re-raises ``ValueError`` when ``Common().__init__`` + raises ``ValueError``. + """ + params_test = copy.deepcopy(params) + params_test.update({"config": {}}) + params_test.pop("check_mode", None) + # params_test.pop("state", None) + + print(f"params_test: {params_test}") + match = r"Merged\.__init__:\s+" + match += r"Error during super\(\)\.__init__\(\)\.\s+" + match += r"Error detail: Merged\.__init__: check_mode is required\." + with pytest.raises(ValueError, match=match): + instance = Merged(params_test) # pylint: disable=unused-variable + + +def test_dcnm_maintenance_mode_merged_00600(monkeypatch) -> None: + """ + ### Classes and Methods + - Merged() + - send_need() + - commit() + + ### Summary + - Verify ``commit()`` re-raises ``ValueError`` when + send_need() raises ``ValueError``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_merged(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield responses_ep_all_switches(f"{key}a") + yield responses_ep_fabrics(f"{key}a") + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Merged(params) + instance.rest_send = rest_send + instance.config = params_test.get("config") + + def mock_send_need(): + raise ValueError("send_need(): Mocked ValueError.") + + match = r"Merged\.commit:\s+" + match += r"Error while sending maintenance mode request\.\s+" + match += r"Error detail: send_need\(\): Mocked ValueError\." + with pytest.raises(ValueError, match=match): + monkeypatch.setattr(instance, "send_need", mock_send_need) + instance.commit() + + assert len(instance.results.diff) == 2 + + assert instance.results.metadata[0]["action"] == "switch_details" + assert instance.results.metadata[1]["action"] == "fabric_details" + + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[1]["state"] == "merged" + + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[1]["check_mode"] is False + + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True + + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py index 2e7d66d8a..5142cf673 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py @@ -347,7 +347,7 @@ def configs(): params_test = copy.deepcopy(params) params_test.update({"config": gen.next}) - class MockMergeDicts: + class MockMergeDicts: # pylint: disable=too-few-public-methods @staticmethod def commit(): raise ValueError("MergeDicts().commit(). ValueError.") diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py index c2ecb373e..f3410bd6d 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py @@ -33,7 +33,7 @@ params = { "state": "merged", - "config": {"switches": [{"ip_address": "172.22.150.105"}]}, + "config": {"switches": [{"ip_address": "192.168.1.2"}]}, "check_mode": False, } @@ -45,11 +45,7 @@ class MockAnsibleModule: check_mode = False - params = { - "state": "merged", - "config": {"switches": [{"ip_address": "172.22.150.105"}]}, - "check_mode": False, - } + params = params argument_spec = { "config": {"required": True, "type": "dict"}, "state": { @@ -135,6 +131,16 @@ def configs_common(key: str) -> dict: return data +def configs_merged(key: str) -> dict: + """ + Return playbook configs for Merged + """ + data_file = "configs_Merged" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + def configs_want(key: str) -> dict: """ Return playbook configs for Want @@ -175,11 +181,51 @@ def responses_common(key: str) -> dict: return data -def responses_merge(key: str) -> dict: +def responses_ep_all_switches(key: str) -> dict: + """ + Return EpAllSwitches() responses. + """ + data_file = "responses_EpAllSwitches" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def responses_ep_maintenance_mode_deploy(key: str) -> dict: + """ + Return responses for endpoint EpMaintenanceModeDeploy. + """ + data_file = "responses_EpMaintenanceModeDeploy" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def responses_ep_maintenance_mode_disable(key: str) -> dict: + """ + Return responses for EpMaintenanceModeDisable(). + """ + data_file = "responses_EpMaintenanceModeDisable" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def responses_ep_maintenance_mode_enable(key: str) -> dict: + """ + Return responses for EpMaintenanceModeEnable(). + """ + data_file = "responses_EpMaintenanceModeEnable" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def responses_ep_fabrics(key: str) -> dict: """ - Return responses for Merge + Return responses for EpFabrics(). """ - data_file = "responses_Merge" + data_file = "responses_EpFabrics" data = load_fixture(data_file).get(key) print(f"{data_file}: {key} : {data}") return data From e82752e2bc344ea47efc9551bf1a15b81380c7f3 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 23 Jun 2024 16:58:34 -1000 Subject: [PATCH 200/230] dcnm_maintenance_mode.py: 87% unit test coverage. 1. Query(): Add initial unit tests. 2. Query(): Update error messages for consistency with Merged() --- plugins/modules/dcnm_maintenance_mode.py | 14 +- .../fixtures/configs_Query.json | 27 ++ .../fixtures/responses_EpAllSwitches.json | 32 ++ .../fixtures/responses_EpFabrics.json | 14 + .../test_dcnm_maintenance_mode_merged.py | 4 +- .../test_dcnm_maintenance_mode_query.py | 308 ++++++++++++++++++ .../dcnm/dcnm_maintenance_mode/utils.py | 17 + 7 files changed, 410 insertions(+), 6 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Query.json create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_query.py diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 395c3444b..c3eef71bb 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -1139,7 +1139,8 @@ def __init__(self, params): super().__init__(params) except (TypeError, ValueError) as error: msg = f"{self.class_name}.{method_name}: " - msg += f"Error: {error}" + msg += "Error during super().__init__(). " + msg += f"Error detail: {error}" raise ValueError(msg) from error self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -1238,7 +1239,10 @@ def commit(self) -> None: try: self.get_want() except ValueError as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Error while retrieving playbook config. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error if len(self.want) == 0: return @@ -1246,7 +1250,11 @@ def commit(self) -> None: try: self.get_have() except ValueError as error: - raise ValueError(error) from error + msg = f"{self.class_name}.{method_name}: " + msg += "Error while retrieving switch information " + msg += "from the controller. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error # If we got this far, the requests were successful. self.results.action = "maintenance_mode_info" diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Query.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Query.json new file mode 100644 index 000000000..3ea7a519e --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Query.json @@ -0,0 +1,27 @@ +{ + "TEST_NOTES": [ + "Mocked playbook configurations for Common unit tests.", + "tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py", + "00070a - top-level config is inherited by all switches" + ], + "test_dcnm_maintenance_mode_query_00100a": { + "switches": [ + { + "ip_address": "192.168.1.2" + }, + { + "ip_address": "192.168.1.3" + } + ] + }, + "test_dcnm_maintenance_mode_query_00300a": { + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + } + ] + } +} diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json index f7e44e803..63db8a49b 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json @@ -230,5 +230,37 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_query_00100a": { + "DATA": [ + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1312-leaf", + "mode": "Maintenance", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD2222222GA", + "switchRole": "leaf", + "systemMode": "Maintenance" + }, + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.3", + "logicalName": "cvd-1313-leaf", + "mode": "Maintenance", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD3333333GA", + "switchRole": "leaf", + "systemMode": "Maintenance" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 } } diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json index 66f6ec40e..4088b5285 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json @@ -132,5 +132,19 @@ "METHOD": "GET", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", "RETURN_CODE": 200 + }, + "test_dcnm_maintenance_mode_query_00100a": { + "DATA": [ + { + "nvPairs": { + "DEPLOYMENT_FREEZE": "false", + "FABRIC_NAME": "VXLAN_EVPN_Fabric" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 } } diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py index 7e3550538..f0ee244cc 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py @@ -641,7 +641,6 @@ def test_dcnm_maintenance_mode_merged_00200() -> None: """ ### Classes and Methods - Merged() - - fabric_deployment_disabled() - commit() ### Summary @@ -696,7 +695,7 @@ def responses(): rest_send.sender = sender with does_not_raise(): - instance = Merged(params) + instance = Merged(params_test) instance.rest_send = rest_send instance.config = params_test.get("config") @@ -763,7 +762,6 @@ def test_dcnm_maintenance_mode_merged_00500() -> None: params_test = copy.deepcopy(params) params_test.update({"config": {}}) params_test.pop("check_mode", None) - # params_test.pop("state", None) print(f"params_test: {params_test}") match = r"Merged\.__init__:\s+" diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_query.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_query.py new file mode 100644 index 000000000..a10de920f --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_query.py @@ -0,0 +1,308 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=protected-access +# pylint: disable=use-implicit-booleaness-not-comparison + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy +import inspect + +import pytest +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_maintenance_mode import \ + Query +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_maintenance_mode.utils import ( + MockAnsibleModule, configs_query, does_not_raise, params_query, + responses_ep_all_switches, responses_ep_fabrics) + + +def test_dcnm_maintenance_mode_query_00000() -> None: + """ + ### Classes and Methods + - Common + - __init__() + + ### Summary + - Verify the class attributes are initialized to expected values. + + ### Test + - Class attributes are initialized to expected values. + - Exception is not raised. + """ + with does_not_raise(): + instance = Query(params_query) + switches = instance.config.get("switches", None) + + assert instance.class_name == "Query" + assert instance.log.name == "dcnm.Query" + + assert instance.check_mode is False + assert instance.state == "query" + + assert isinstance(instance.config, dict) + assert isinstance(switches, list) + assert switches[0].get("ip_address", None) == "192.168.1.2" + + assert instance.have == {} + assert instance.query == [] + assert instance.want == [] + + assert instance.results.class_name == "Results" + assert instance.results.state == "query" + assert instance.results.check_mode is False + + +def test_dcnm_maintenance_mode_query_00100() -> None: + """ + ### Classes and Methods + - Query() + - commit() + + ### Summary + - Verify ``commit()`` happy path. + - No exceptions are raised. + - want contains expected structure and values. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_query(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield responses_ep_all_switches(f"{key}a") + yield responses_ep_fabrics(f"{key}a") + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params_query) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params_test) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Query(params_query) + instance.rest_send = rest_send + instance.config = params_test.get("config") + instance.commit() + assert instance.want[0].get("ip_address", None) == "192.168.1.2" + assert instance.want[1].get("ip_address", None) == "192.168.1.3" + + switch_2 = instance.results.diff[2]["192.168.1.2"] + switch_3 = instance.results.diff[2]["192.168.1.3"] + + assert switch_2.get("fabric_deployment_disabled", None) is False + assert switch_3.get("fabric_deployment_disabled", None) is False + + assert switch_2.get("fabric_freeze_mode", None) is False + assert switch_3.get("fabric_freeze_mode", None) is False + + assert switch_2.get("fabric_name", None) == "VXLAN_EVPN_Fabric" + assert switch_3.get("fabric_name", None) == "VXLAN_EVPN_Fabric" + + assert switch_2.get("fabric_read_only", None) is False + assert switch_3.get("fabric_read_only", None) is False + + assert switch_2.get("ip_address", None) == "192.168.1.2" + assert switch_3.get("ip_address", None) == "192.168.1.3" + + assert switch_2.get("mode", None) == "maintenance" + assert switch_3.get("mode", None) == "maintenance" + + assert switch_2.get("role", None) == "leaf" + assert switch_3.get("role", None) == "leaf" + + assert switch_2.get("serial_number", None) == "FD2222222GA" + assert switch_3.get("serial_number", None) == "FD3333333GA" + + assert instance.results.metadata[0]["action"] == "switch_details" + assert instance.results.metadata[1]["action"] == "fabric_details" + assert instance.results.metadata[2]["action"] == "maintenance_mode_info" + + assert instance.results.metadata[0]["state"] == "query" + assert instance.results.metadata[1]["state"] == "query" + assert instance.results.metadata[2]["state"] == "query" + + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[1]["check_mode"] is False + assert instance.results.metadata[2]["check_mode"] is False + + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True + + assert instance.results.result[2]["changed"] is False + + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True + assert instance.results.result[2]["success"] is True + + +def test_dcnm_maintenance_mode_query_00200() -> None: + """ + ### Classes and Methods + - Query() + - commit() + + ### Summary + - Verify ``commit()`` raises ``ValueError`` when rest_send has not + been set. + """ + with does_not_raise(): + instance = Query(params_query) + match = r"Query\.commit:\s+" + match += r"rest_send must be set before calling commit\." + with pytest.raises(ValueError, match=match): + instance.commit() + + assert len(instance.results.diff) == 0 + assert len(instance.results.metadata) == 0 + assert len(instance.results.response) == 0 + assert len(instance.results.result) == 0 + + +def test_dcnm_maintenance_mode_query_00300(monkeypatch) -> None: + """ + ### Classes and Methods + - Query() + - get_need() + - commit() + + ### Summary + - Verify ``get_need()`` raises ``ValueError`` when ip_address + does not exist in self.have. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_query(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params_query) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params_query) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Query(params_test) + instance.rest_send = rest_send + instance.config = params_test.get("config") + + def mock_get_have(): + raise ValueError("Query.get_need: Mocked ValueError.") + + match = r"Query\.commit:\s+" + match += r"Error while retrieving switch information from the controller\.\s+" + match += r"Error detail: Query\.get_need: Mocked ValueError\." + with pytest.raises(ValueError, match=match): + monkeypatch.setattr(instance, "get_have", mock_get_have) + instance.commit() + + assert len(instance.results.diff) == 0 + assert len(instance.results.metadata) == 0 + assert len(instance.results.response) == 0 + assert len(instance.results.result) == 0 + + +def test_dcnm_maintenance_mode_query_00400(monkeypatch) -> None: + """ + ### Classes and Methods + - Merged() + - get_want() + - commit() + + ### Summary + - Verify ``commit`` re-raises ``ValueError`` when ``get_want()`` + raises ``ValueError``. + """ + params_test = copy.deepcopy(params_query) + params_test.update({"config": {}}) + + with does_not_raise(): + instance = Query(params_test) + instance.rest_send = RestSend(params_test) + instance.config = params_test.get("config") + + def mock_get_want(): + raise ValueError("get_want(): Mocked ValueError.") + + match = r"Query\.commit:\s+" + match += r"Error while retrieving playbook config\.\s+" + match += r"Error detail: get_want\(\): Mocked ValueError\." + with pytest.raises(ValueError, match=match): + monkeypatch.setattr(instance, "get_want", mock_get_want) + instance.commit() + + assert len(instance.results.diff) == 0 + assert len(instance.results.metadata) == 0 + assert len(instance.results.response) == 0 + assert len(instance.results.result) == 0 + + +def test_dcnm_maintenance_mode_query_00500() -> None: + """ + ### Classes and Methods + - Query() + - __init__() + + ### Summary + - Verify ``__init__`` re-raises ``ValueError`` when ``Common().__init__`` + raises ``ValueError``. + """ + params_test = copy.deepcopy(params_query) + params_test.update({"config": {}}) + params_test.pop("check_mode", None) + + print(f"params_test: {params_test}") + match = r"Query\.__init__:\s+" + match += r"Error during super\(\)\.__init__\(\)\.\s+" + match += r"Error detail: Query\.__init__: check_mode is required\." + with pytest.raises(ValueError, match=match): + instance = Query(params_test) # pylint: disable=unused-variable diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py index f3410bd6d..7ce1e6082 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py @@ -31,6 +31,13 @@ from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_maintenance_mode.fixture import \ load_fixture +params_query = { + "state": "query", + "config": {"switches": [{"ip_address": "192.168.1.2"}]}, + "check_mode": False, +} + + params = { "state": "merged", "config": {"switches": [{"ip_address": "192.168.1.2"}]}, @@ -151,6 +158,16 @@ def configs_want(key: str) -> dict: return data +def configs_query(key: str) -> dict: + """ + Return playbook configs for Query + """ + data_file = "configs_Query" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + def payloads_merge(key: str) -> dict: """ Return payloads for Merge From 20e267ef93303a3143481498a2284129dbf237ec Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 23 Jun 2024 17:29:59 -1000 Subject: [PATCH 201/230] dcnm_maintenance_mode.py: 88% unit test coverage. Query: add unit test. - test_dcnm_maintenance_mode_query_00600 - Verify ``commit`` re-raises ``ValueError`` when ``get_have()`` raises ``ValueError``. --- plugins/modules/dcnm_maintenance_mode.py | 13 ++-- .../fixtures/configs_Query.json | 10 ++- .../test_dcnm_maintenance_mode_query.py | 65 +++++++++++++++++++ 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index c3eef71bb..a090f58c4 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -1145,6 +1145,8 @@ def __init__(self, params): self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.maintenance_mode_info = MaintenanceModeInfo(self.params) + msg = "ENTERED Query(): " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" @@ -1201,19 +1203,18 @@ def get_have(self): method_name = inspect.stack()[0][3] # pylint: disable=unused-variable try: - instance = MaintenanceModeInfo(self.params) - instance.rest_send = self.rest_send - instance.results = self.results - instance.config = [ + self.maintenance_mode_info.rest_send = self.rest_send + self.maintenance_mode_info.results = self.results + self.maintenance_mode_info.config = [ item["ip_address"] for item in self.config.get("switches", {}) ] - instance.refresh() + self.maintenance_mode_info.refresh() except (TypeError, ValueError) as error: msg = f"{self.class_name}.{method_name}: " msg += "Error while retrieving switch info. " msg += f"Error detail: {error}" raise ValueError(msg) from error - self.have = instance.info + self.have = self.maintenance_mode_info.info def commit(self) -> None: """ diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Query.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Query.json index 3ea7a519e..f1b80929c 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Query.json +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Query.json @@ -15,9 +15,13 @@ ] }, "test_dcnm_maintenance_mode_query_00300a": { - "deploy": true, - "mode": "maintenance", - "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + } + ] + }, + "test_dcnm_maintenance_mode_query_00600a": { "switches": [ { "ip_address": "192.168.1.2" diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_query.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_query.py index a10de920f..934912b82 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_query.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_query.py @@ -306,3 +306,68 @@ def test_dcnm_maintenance_mode_query_00500() -> None: match += r"Error detail: Query\.__init__: check_mode is required\." with pytest.raises(ValueError, match=match): instance = Query(params_test) # pylint: disable=unused-variable + + +def test_dcnm_maintenance_mode_query_00600(monkeypatch) -> None: + """ + ### Classes and Methods + - Query() + - commit() + + ### Summary + - Verify ``commit`` re-raises ``ValueError`` when ``get_have()`` + raises ``ValueError``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_query(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params_query) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params_query) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Query(params_test) + instance.rest_send = RestSend(params_test) + instance.config = params_test.get("config") + + class MockMaintenanceModeInfo: # pylint: disable=too-few-public-methods + """ + Mocked MaintenanceModeInfo class. + """ + def __init__(self, *args): + pass + + def refresh(self): + """ + Mocked refresh method. + """ + raise ValueError("MockMaintenanceModeInfo.refresh: Mocked ValueError.") + + match = r"Query\.commit:\s+" + match += r"Error while retrieving switch information from the\s+" + match += r"controller\.\s+" + match += r"Error detail:\s+" + match += r"Query\.get_have: Error while retrieving switch info\.\s+" + match += r"Error detail: MockMaintenanceModeInfo\.refresh:\s+" + match += r"Mocked ValueError\." + with pytest.raises(ValueError, match=match): + monkeypatch.setattr( + instance, "maintenance_mode_info", MockMaintenanceModeInfo() + ) + instance.commit() From 00df43a60187ccef8a16104aedaa27a02641782c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 23 Jun 2024 17:58:08 -1000 Subject: [PATCH 202/230] dcnm_maintenance_mode.py: 88% unit test coverage. Merged(): Added the following unit test. - test_dcnm_maintenance_mode_merged_00700 - Verify ``send_need()`` re-raises ``ValueError`` when MaintenanceMode.commit() raises ``ValueError``. Merged()__init__(): instantiate MaintenanceMode() in __init__() to enable mocking. --- plugins/modules/dcnm_maintenance_mode.py | 11 +-- .../fixtures/configs_Merged.json | 10 +++ .../fixtures/responses_EpAllSwitches.json | 20 +++++ .../fixtures/responses_EpFabrics.json | 15 ++++ .../test_dcnm_maintenance_mode_merged.py | 90 ++++++++++++++++++- 5 files changed, 139 insertions(+), 7 deletions(-) diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index a090f58c4..95a92ddda 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -847,6 +847,8 @@ def __init__(self, params): self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.maintenance_mode = MaintenanceMode(params) + msg = f"ENTERED Merged.{method_name}: " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" @@ -1109,11 +1111,10 @@ def send_need(self) -> None: return try: - instance = MaintenanceMode(self.params) - instance.rest_send = self.rest_send - instance.results = self.results - instance.config = self.need - instance.commit() + self.maintenance_mode.rest_send = self.rest_send + self.maintenance_mode.results = self.results + self.maintenance_mode.config = self.need + self.maintenance_mode.commit() except (TypeError, ValueError) as error: raise ValueError(error) from error diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Merged.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Merged.json index c27c0ccb8..8b65e469c 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Merged.json +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Merged.json @@ -105,5 +105,15 @@ "ip_address": "192.168.1.2" } ] + }, + "test_dcnm_maintenance_mode_merged_00700a": { + "deploy": true, + "mode": "maintenance", + "wait_for_mode_change": true, + "switches": [ + { + "ip_address": "192.168.1.2" + } + ] } } diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json index 63db8a49b..10f1debd5 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json @@ -231,6 +231,26 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", "RETURN_CODE": 200 }, + "test_dcnm_maintenance_mode_merged_00700a": { + "DATA": [ + { + "fabricName": "VXLAN_EVPN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.2", + "logicalName": "cvd-1312-leaf", + "mode": "Normal", + "model": "N9K-C93180YC-EX", + "monitorMode": null, + "serialNumber": "FD2222222GA", + "switchRole": "leaf", + "systemMode": "Normal" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/inventory/allswitches", + "RETURN_CODE": 200 + }, "test_dcnm_maintenance_mode_query_00100a": { "DATA": [ { diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json index 4088b5285..80706b13e 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json @@ -133,6 +133,21 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", "RETURN_CODE": 200 }, + "test_dcnm_maintenance_mode_merged_00700a": { + "DATA": [ + { + "nvPairs": { + "DEPLOYMENT_FREEZE": "false", + "FABRIC_NAME": "VXLAN_EVPN_Fabric", + "IS_READ_ONLY": "false" + } + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics", + "RETURN_CODE": 200 + }, "test_dcnm_maintenance_mode_query_00100a": { "DATA": [ { diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py index f0ee244cc..c4b43a40c 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py @@ -36,8 +36,8 @@ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ Sender -from ansible_collections.cisco.dcnm.plugins.modules.dcnm_maintenance_mode import ( - Merged) +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_maintenance_mode import \ + Merged from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_maintenance_mode.utils import ( @@ -81,6 +81,10 @@ def test_dcnm_maintenance_mode_merged_00000() -> None: assert instance.query == [] assert instance.want == [] + assert instance.maintenance_mode.class_name == "MaintenanceMode" + assert instance.maintenance_mode.state == "merged" + assert instance.maintenance_mode.check_mode is False + assert instance.results.class_name == "Results" assert instance.results.state == "merged" assert instance.results.check_mode is False @@ -837,3 +841,85 @@ def mock_send_need(): assert instance.results.result[0]["success"] is True assert instance.results.result[1]["success"] is True + + +def test_dcnm_maintenance_mode_merged_00700(monkeypatch) -> None: + """ + ### Classes and Methods + - Merged() + - send_need() + - commit() + + ### Summary + - Verify ``send_need()`` re-raises ``ValueError`` when + MaintenanceMode.commit() raises ``ValueError``. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}" + + def configs(): + yield configs_merged(f"{key}a") + + gen_configs = ResponseGenerator(configs()) + + def responses(): + yield responses_ep_all_switches(f"{key}a") + yield responses_ep_fabrics(f"{key}a") + + gen_responses = ResponseGenerator(responses()) + + params_test = copy.deepcopy(params) + params_test.update({"config": gen_configs.next}) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = Merged(params) + instance.rest_send = rest_send + instance.config = params_test.get("config") + + class MockMaintenanceMode: # pylint: disable=too-few-public-methods + """ + Mocked MaintenanceMode class. + """ + + def __init__(self, *args): + pass + + def commit(self): + """ + Mocked commit method. + """ + raise ValueError("MockMaintenanceModeInfo.refresh: Mocked ValueError.") + + match = r"Merged\.commit:\s+" + match += r"Error while sending maintenance mode request\.\s+" + match += r"Error detail:\s+" + match += r"MockMaintenanceModeInfo\.refresh: Mocked ValueError\." + with pytest.raises(ValueError, match=match): + monkeypatch.setattr( + instance, "maintenance_mode", MockMaintenanceMode(params_test) + ) + instance.commit() + + assert len(instance.results.diff) == 2 + + assert instance.results.metadata[0]["action"] == "switch_details" + assert instance.results.metadata[1]["action"] == "fabric_details" + + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[1]["state"] == "merged" + + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[1]["check_mode"] is False + + assert instance.results.result[0]["found"] is True + assert instance.results.result[1]["found"] is True + + assert instance.results.result[0]["success"] is True + assert instance.results.result[1]["success"] is True From 69b65c773ea6a4367176ef47cc65804f63c8f931 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 24 Jun 2024 09:04:01 -1000 Subject: [PATCH 203/230] dcnm_maintenance_mode.py: 93% unit test coverage. Want(): Improve error messages. Want(): Add multiple test cases to validate property setters. Want(): Update docstrings. Want().validate_configs(): remove check for validator since this is already verified in commit(). ParamsSpec(): Add unit tests. ParamsSpec(): Move params validation to params.setter. --- plugins/modules/dcnm_maintenance_mode.py | 80 ++++--- .../test_dcnm_maintenance_mode_common.py | 8 +- .../test_dcnm_maintenance_mode_params_spec.py | 208 +++++++++++++++++ .../test_dcnm_maintenance_mode_want.py | 217 +++++++++++++++++- 4 files changed, 467 insertions(+), 46 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_params_spec.py diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py index 95a92ddda..45176ce2e 100644 --- a/plugins/modules/dcnm_maintenance_mode.py +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -229,15 +229,11 @@ def commit(self): Build the parameter specification based on the state ## Raises - - ValueError if params.state is not a valid state for - the dcnm_maintenance_mode module + - ``ValueError`` if params is not set """ - method_name = inspect.stack()[0][3] - - if self.params["state"] not in self.valid_states: - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid state {self.params['state']}. " - msg += f"Expected one of {', '.join(self.valid_states)}." + if self._params is None: + msg = f"{self.class_name}.commit: " + msg += "params must be set before calling commit()." raise ValueError(msg) if self.params["state"] == "merged": @@ -289,12 +285,19 @@ def params_spec(self) -> dict: @property def params(self) -> dict: """ - Expects value to be the return value of - ``AnsibleModule.params`` property. + ### Summary + Expects value to be a dictionary containing, at mimimum, + the key "state" with value of either "merged" or "query". + ### Raises + - setter: raise ``ValueError`` if value is not a dict + - setter: raise ``ValueError`` if value["state"] is missing + - setter: raise ``ValueError`` if value["state"] is not a valid state + + ### Details + - Valid params: {"state": "merged"} or {"state": "query"} - getter: return the params - setter: set the params - - setter: raise ``ValueError`` if value is not a dict """ return self._params @@ -303,11 +306,25 @@ def params(self, value: dict) -> None: """ - setter: set the params """ + method_name = inspect.stack()[0][3] if not isinstance(value, dict): - msg = f"{self.class_name}.params.setter: " - msg += "expected dict type for value. " - msg += f"got {type(value).__name__}." + msg = f"{self.class_name}.{method_name}.setter: " + msg += "Invalid type. Expected dict but " + msg += f"got type {type(value).__name__}, " + msg += f"value {value}." + raise TypeError(msg) + + if value.get("state", None) is None: + msg = f"{self.class_name}.{method_name}.setter: " + msg += "params.state is required but missing." + raise ValueError(msg) + + if value["state"] not in self.valid_states: + msg = f"{self.class_name}.{method_name}.setter: " + msg += f"params.state is invalid: {value['state']}. " + msg += f"Expected one of {', '.join(self.valid_states)}." raise ValueError(msg) + self._params = value @@ -404,10 +421,7 @@ def generate_params_spec(self) -> None: except ValueError as error: raise ValueError(error) from error - try: - self.params_spec.commit() - except ValueError as error: - raise ValueError(error) from error + self.params_spec.commit() def validate_configs(self) -> None: """ @@ -416,14 +430,11 @@ def validate_configs(self) -> None: and populate self.want with the validated configs. ### Raises - - ``ValueError`` if self.validator is not set + None + ### Notes + - validator is already verified in commit()s """ - if self.validator is None: - msg = f"{self.class_name}.validate_configs(): " - msg += "self.validator is required" - raise ValueError(msg) - self.validator.params_spec = self.params_spec.params_spec for config in self.merged_configs: self.validator.parameters = config @@ -436,6 +447,9 @@ def build_merged_configs(self) -> None: If a parameter is missing from the config, and the parameter has a default value, merge the default value for the parameter into the config. + + ### Raises + None """ self.merged_configs = [] merge_defaults = ParamsMergeDefaults() @@ -596,8 +610,8 @@ def config(self): def config(self, value) -> None: if not isinstance(value, dict): msg = f"{self.class_name}.config.setter: " - msg += "expected dict for value. " - msg += f"got {type(value).__name__}." + msg += "expected dict but got " + msg += f"{type(value).__name__}, value {value}." raise TypeError(msg) self._config = value @@ -620,8 +634,8 @@ def items_key(self, value: str) -> None: """ if not isinstance(value, str): msg = f"{self.class_name}.items_key.setter: " - msg += "expected string type for value. " - msg += f"got {type(value).__name__}." + msg += "expected string but got " + msg += f"{type(value).__name__}, value {value}." raise TypeError(msg) self._items_key = value @@ -659,8 +673,8 @@ def params(self, value: dict) -> None: """ if not isinstance(value, dict): msg = f"{self.class_name}.params.setter: " - msg += "expected dict type for value. " - msg += f"got {type(value).__name__}." + msg += "expected dict but got " + msg += f"{type(value).__name__}, value {value}." raise TypeError(msg) self._params = value @@ -693,7 +707,8 @@ def params_spec(self, value) -> None: _class_need = "ParamsSpec" msg = f"{self.class_name}.{method_name}: " msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." + msg += f"Got type {type(value).__name__}, " + msg += f"value {value}. " try: _class_have = value.class_name except AttributeError as error: @@ -728,7 +743,8 @@ def validator(self, value) -> None: _class_need = "ParamsValidate" msg = f"{self.class_name}.{method_name}: " msg += f"value must be an instance of {_class_need}. " - msg += f"Got value {value} of type {type(value).__name__}." + msg += f"Got type {type(value).__name__}, " + msg += f"value {value}. " try: _class_have = value.class_name except AttributeError as error: diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py index cb00bf718..734cc5826 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py @@ -418,7 +418,11 @@ def configs(): params_test.update({"state": "foo"}) with does_not_raise(): instance = Common(params_test) - match = r"ParamsSpec.commit:\s+" - match += r"Invalid state foo\. Expected one of merged, query\." + match = r"Want.commit:\s+" + match += r"Error generating params_spec\.\s+" + match += r"Error detail:\s+" + match += r"ParamsSpec\.params\.setter:\s+" + match += r"params\.state is invalid: foo\.\s+" + match += r"Expected one of merged, query\." with pytest.raises(ValueError, match=match): instance.get_want() diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_params_spec.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_params_spec.py new file mode 100644 index 000000000..9b80c1456 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_params_spec.py @@ -0,0 +1,208 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# Also, fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-import +# Prefer to use more explicit "== {}" rather than "is None" for comparison of lists and dicts. +# pylint: disable=use-implicit-booleaness-not-comparison +# Unit tests commonly test protected members. +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Allen Robel" + +import copy + +import pytest +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_maintenance_mode import \ + ParamsSpec +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_maintenance_mode.utils import ( + does_not_raise, params) + + +def test_dcnm_maintenance_mode_params_spec_00000() -> None: + """ + ### Classes and Methods + - ParamsSpec + - __init__() + + ### Summary + - Verify the class attributes are initialized to expected values. + + ### Test + - Class attributes are initialized to expected values + - ``ValueError`` is not called + """ + with does_not_raise(): + instance = ParamsSpec() + assert instance.class_name == "ParamsSpec" + assert instance._params is None + assert instance._params_spec == {} + assert instance.valid_states == ["merged", "query"] + + +def test_dcnm_maintenance_mode_params_spec_00100() -> None: + """ + ### Classes and Methods + - ParamsSpec + - params.setter + + ### Summary + - Verify ``TypeError`` is raised. + - params is not a dict. + """ + params_test = "foo" + + with does_not_raise(): + instance = ParamsSpec() + + match = r"ParamsSpec\.params.setter:\s+" + match += r"Invalid type\. Expected dict but got type str, value foo\." + with pytest.raises(TypeError, match=match): + instance.params = params_test + + +def test_dcnm_maintenance_mode_params_spec_00110() -> None: + """ + ### Classes and Methods + - ParamsSpec + - params.setter + + ### Summary + - Verify ``ValueError`` is raised. + - params is missing ``state`` key/value. + """ + params_test = copy.deepcopy(params) + params_test.pop("state", None) + + with does_not_raise(): + instance = ParamsSpec() + + match = r"ParamsSpec\.params\.setter:\s+" + match += r"params.state is required but missing\." + with pytest.raises(ValueError, match=match): + instance.params = params_test + + +def test_dcnm_maintenance_mode_params_spec_00120() -> None: + """ + ### Classes and Methods + - ParamsSpec + - params.setter + + ### Summary + - Verify ``ValueError`` is raised. + - params ``state`` has invalid value. + """ + params_test = copy.deepcopy(params) + params_test.update({"state": "foo"}) + + with does_not_raise(): + instance = ParamsSpec() + + match = r"ParamsSpec\.params\.setter:\s+" + match += r"params\.state is invalid: foo\. Expected one of merged, query\." + with pytest.raises(ValueError, match=match): + instance.params = params_test + + +def test_dcnm_maintenance_mode_params_spec_00200() -> None: + """ + ### Classes and Methods + - ParamsSpec + - params.setter + - commit() + + ### Summary + - Verify commit() happy path for merged state. + """ + params_test = copy.deepcopy(params) + + with does_not_raise(): + instance = ParamsSpec() + instance.params = params_test + instance.commit() + + assert instance.params == params_test + assert instance.params_spec["ip_address"]["required"] is True + assert instance.params_spec["ip_address"]["type"] == "ipv4" + assert instance.params_spec["ip_address"].get("default", None) is None + + assert instance.params_spec["mode"]["choices"] == ["normal", "maintenance"] + assert instance.params_spec["mode"]["default"] == "normal" + assert instance.params_spec["mode"]["required"] is False + assert instance.params_spec["mode"]["type"] == "str" + + assert instance.params_spec["deploy"]["default"] is False + assert instance.params_spec["deploy"]["required"] is False + assert instance.params_spec["deploy"]["type"] == "bool" + + assert instance.params_spec["wait_for_mode_change"]["default"] is False + assert instance.params_spec["wait_for_mode_change"]["required"] is False + assert instance.params_spec["wait_for_mode_change"]["type"] == "bool" + + +def test_dcnm_maintenance_mode_params_spec_00210() -> None: + """ + ### Classes and Methods + - ParamsSpec + - params.setter + - commit() + + ### Summary + - Verify commit() happy path for query state. + """ + params_test = copy.deepcopy(params) + params_test.update({"state": "query"}) + + with does_not_raise(): + instance = ParamsSpec() + instance.params = params_test + instance.commit() + + assert instance.params == params_test + assert instance.params_spec["ip_address"]["required"] is True + assert instance.params_spec["ip_address"]["type"] == "ipv4" + assert instance.params_spec["ip_address"].get("default", None) is None + + +def test_dcnm_maintenance_mode_params_spec_00220() -> None: + """ + ### Classes and Methods + - ParamsSpec + - params.setter + - commit() + + ### Summary + - Verify commit() sad path. + - params is not set before calling commit. + - commit() raises ``ValueError`` when params is not set. + """ + params_test = copy.deepcopy(params) + params_test.update({"state": "query"}) + + with does_not_raise(): + instance = ParamsSpec() + + match = r"ParamsSpec\.commit:\s+" + match += r"params must be set before calling commit\(\)\." + with pytest.raises(ValueError, match=match): + instance.commit() diff --git a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py index 5142cf673..31d79f753 100644 --- a/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py @@ -133,7 +133,7 @@ def configs(): instance.config = params_test.get("config") instance.params = params_test instance.params_spec = ParamsSpec() - match = r"Want.commit:\s+" + match = r"Want\.commit:\s+" match += r"self\.validator must be set before calling commit\." with pytest.raises(ValueError, match=match): instance.commit() @@ -167,7 +167,7 @@ def configs(): instance.config = params_test.get("config") instance.params_spec = ParamsSpec() instance.validator = ParamsValidate() - match = r"Want.commit:\s+" + match = r"Want\.commit:\s+" match += r"Error generating params_spec\.\s+" match += r"Error detail:\s+" match += r"Want\.generate_params_spec\(\):\s+" @@ -204,7 +204,7 @@ def configs(): instance.config = params_test.get("config") instance.params = params_test instance.validator = ParamsValidate() - match = r"Want.commit:\s+" + match = r"Want\.commit:\s+" match += r"Error generating params_spec\.\s+" match += r"Error detail:\s+" match += r"Want\.generate_params_spec\(\):\s+" @@ -222,7 +222,7 @@ def test_dcnm_maintenance_mode_want_00130() -> None: ### Summary - Verify Want().commit() catches and re-raises ``ValueError``. - Want()._merge_global_and_item_configs() raises ``ValueError`` - because ``config`` is not set, and is required. + because ``config`` is not set. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -241,7 +241,7 @@ def configs(): instance.params = params_test instance.params_spec = ParamsSpec() instance.validator = ParamsValidate() - match = r"Want.commit:\s+" + match = r"Want\.commit:\s+" match += r"Error merging global and item configs\.\s+" match += r"Error detail:\s+" match += r"Want\._merge_global_and_item_configs:\s+" @@ -259,7 +259,7 @@ def test_dcnm_maintenance_mode_want_00131() -> None: ### Summary - Verify Want().commit() catches and re-raises ``ValueError``. - Want()._merge_global_and_item_configs() raises ``ValueError`` - because ``config`` is not set, and is required. + because ``items_key`` is not set. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -278,7 +278,7 @@ def configs(): instance.params = params_test instance.params_spec = ParamsSpec() instance.validator = ParamsValidate() - match = r"Want.commit:\s+" + match = r"Want\.commit:\s+" match += r"Error merging global and item configs\.\s+" match += r"Error detail:\s+" match += r"Want\._merge_global_and_item_configs:\s+" @@ -316,7 +316,7 @@ def configs(): instance.params = params_test instance.params_spec = ParamsSpec() instance.validator = ParamsValidate() - match = r"Want.commit:\s+" + match = r"Want\.commit:\s+" match += r"Error merging global and item configs\.\s+" match += r"Error detail:\s+" match += r"Want\._merge_global_and_item_configs:\s+" @@ -348,8 +348,19 @@ def configs(): params_test.update({"config": gen.next}) class MockMergeDicts: # pylint: disable=too-few-public-methods + """ + Mock class for MergeDicts(). + """ + @staticmethod def commit(): + """ + ### Summary + Mock method for MergeDicts().commit(). + + ### Raises + ValueError: Always + """ raise ValueError("MergeDicts().commit(). ValueError.") with does_not_raise(): @@ -360,7 +371,7 @@ def commit(): instance.params = params_test instance.params_spec = ParamsSpec() instance.validator = ParamsValidate() - match = r"Want.commit: Error merging global and item configs\.\s+" + match = r"Want\.commit: Error merging global and item configs\.\s+" match += r"Error detail:\s+" match += r"Want\._merge_global_and_item_configs:\s+" match += r"Error in MergeDicts\(\)\.\s+" @@ -376,8 +387,9 @@ def test_dcnm_maintenance_mode_want_00140(monkeypatch) -> None: - commit() ### Summary - - Verify Want().commit() catches and re-raises ``ValueError``. - - Want().validate_configs() raises ``ValueError``. + - Verify Want().commit() catches and re-raises ``ValueError`` + when Want().validate_configs() raises ``ValueError``. + - Want().validate_configs() is mocked to raise ``ValueError``. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -401,8 +413,189 @@ def mock_def(): instance.params_spec = ParamsSpec() instance.items_key = "switches" instance.validator = ParamsValidate() - match = r"Want.commit:\s+" + match = r"Want\.commit:\s+" match += r"Error validating playbook configs against params spec\.\s+" match += r"Error detail: validate_configs ValueError\." with pytest.raises(ValueError, match=match): instance.commit() + + +def test_dcnm_maintenance_mode_want_00200() -> None: + """ + ### Classes and Methods + - Want() + - config.setter + + ### Summary + - Verify Want().config raises ``TypeError`` when config is not a dict. + """ + with does_not_raise(): + instance = Want() + + match = r"Want\.config\.setter:\s+" + match += r"expected dict but got str, value NOT_A_DICT\." + with pytest.raises(TypeError, match=match): + instance.config = "NOT_A_DICT" + + +def test_dcnm_maintenance_mode_want_00300() -> None: + """ + ### Classes and Methods + - Want() + - items_key.setter + + ### Summary + - Verify Want().items_key raises ``TypeError`` when items_key is not + a string. + """ + with does_not_raise(): + instance = Want() + + match = r"Want\.items_key\.setter:\s+" + match += r"expected string but got set, value {'NOT_A_STRING'}\." + with pytest.raises(TypeError, match=match): + instance.items_key = {"NOT_A_STRING"} + + +def test_dcnm_maintenance_mode_want_00400() -> None: + """ + ### Classes and Methods + - Want() + - params.setter + + ### Summary + Verify Want().params happy path. + """ + with does_not_raise(): + instance = Want() + instance.params = {"state": "merged"} + + +def test_dcnm_maintenance_mode_want_00410() -> None: + """ + ### Classes and Methods + - Want() + - params.setter + + ### Summary + - Verify Want().params raises ``TypeError`` when params is not a dict. + """ + with does_not_raise(): + instance = Want() + + match = r"Want\.params\.setter:\s+" + match += r"expected dict but got str, value NOT_A_DICT\." + with pytest.raises(TypeError, match=match): + instance.params = "NOT_A_DICT" + + +def test_dcnm_maintenance_mode_want_00500() -> None: + """ + ### Classes and Methods + - Want() + - params_spec.setter + + ### Summary + Verify Want().params_spec happy path. + """ + with does_not_raise(): + instance = Want() + instance.params_spec = ParamsSpec() + + +def test_dcnm_maintenance_mode_want_00510() -> None: + """ + ### Classes and Methods + - Want() + - params_spec.setter + + ### Summary + - Verify Want().params_spec raises ``TypeError`` when params_spec + is not an instance of ParamsSpec(). + """ + with does_not_raise(): + instance = Want() + + match = r"Want\.params_spec:\s+" + match += r"value must be an instance of ParamsSpec\.\s+" + match += r"Got type str, value NOT_AN_INSTANCE_OF_PARAMS_SPEC\.\s+" + match += r"Error detail: 'str' object has no attribute 'class_name'\." + with pytest.raises(TypeError, match=match): + instance.params_spec = "NOT_AN_INSTANCE_OF_PARAMS_SPEC" + + +def test_dcnm_maintenance_mode_want_00520() -> None: + """ + ### Classes and Methods + - Want() + - params_spec.setter + + ### Summary + Verify Want().params_spec raises ``TypeError`` when params_spec + is not an instance of ParamsSpec(), but IS an instance of another + class. + """ + with does_not_raise(): + instance = Want() + + match = r"Want\.params_spec:\s+" + match += r"value must be an instance of ParamsSpec\.\s+" + match += r"Got type ParamsValidate, value .* object at 0x.*\." + with pytest.raises(TypeError, match=match): + instance.params_spec = ParamsValidate() + + +def test_dcnm_maintenance_mode_want_00600() -> None: + """ + ### Classes and Methods + - Want() + - validator.setter + + ### Summary + Verify Want().validator happy path. + """ + with does_not_raise(): + instance = Want() + instance.validator = ParamsValidate() + + +def test_dcnm_maintenance_mode_want_00610() -> None: + """ + ### Classes and Methods + - Want() + - validator.setter + + ### Summary + - Verify Want().validator raises ``TypeError`` when validator + is not an instance of ParamsValidate(). + """ + with does_not_raise(): + instance = Want() + + match = r"Want\.validator:\s+" + match += r"value must be an instance of ParamsValidate\.\s+" + match += r"Got type str, value NOT_AN_INSTANCE_OF_PARAMS_VALIDATE\.\s+" + match += r"Error detail: 'str' object has no attribute 'class_name'\." + with pytest.raises(TypeError, match=match): + instance.validator = "NOT_AN_INSTANCE_OF_PARAMS_VALIDATE" + + +def test_dcnm_maintenance_mode_want_00620() -> None: + """ + ### Classes and Methods + - Want() + - validator.setter + + ### Summary + Verify Want().validator raises ``TypeError`` when validator + is not an instance of ParamsValidate(), but IS an instance of + another class. + """ + with does_not_raise(): + instance = Want() + + match = r"Want\.validator:\s+" + match += r"value must be an instance of ParamsValidate\.\s+" + match += r"Got type ParamsSpec, value .* object at 0x.*\." + with pytest.raises(TypeError, match=match): + instance.validator = ParamsSpec() From b8c0b419e75155aadc5bc4a56010c1e1c65fc329 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 24 Jun 2024 11:24:09 -1000 Subject: [PATCH 204/230] Complete integration tests. --- ...rmal_mode_deploy_no_wait_switch_level.yaml | 1 - ...5_merged_maintenance_mode_deploy_wait.yaml | 399 ------------------ ...aintenance_mode_deploy_wait_top_level.yaml | 167 ++++++++ ...ged_normal_mode_deploy_wait_top_level.yaml | 168 ++++++++ ...tenance_mode_deploy_wait_switch_level.yaml | 173 ++++++++ ..._normal_mode_deploy_wait_switch_level.yaml | 174 ++++++++ ...09_merged_maintenance_mode_no_deploy.yaml} | 0 7 files changed, 682 insertions(+), 400 deletions(-) delete mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/05_merged_maintenance_mode_deploy_wait.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/05_merged_maintenance_mode_deploy_wait_top_level.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/06_merged_normal_mode_deploy_wait_top_level.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/07_merged_maintenance_mode_deploy_wait_switch_level.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/08_merged_normal_mode_deploy_wait_switch_level.yaml rename tests/integration/targets/dcnm_maintenance_mode/tests/{07_merged_maintenance_mode_no_deploy.yaml => 09_merged_maintenance_mode_no_deploy.yaml} (100%) diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/02_merged_normal_mode_deploy_no_wait_switch_level.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/02_merged_normal_mode_deploy_no_wait_switch_level.yaml index 59d5fd41a..a1899e21f 100644 --- a/tests/integration/targets/dcnm_maintenance_mode/tests/02_merged_normal_mode_deploy_no_wait_switch_level.yaml +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/02_merged_normal_mode_deploy_no_wait_switch_level.yaml @@ -154,7 +154,6 @@ - result.diff[2][leaf_1].mode == "normal" - result.diff[2][leaf_2].mode == "normal" - - assert: that: - result_normal_mode.failed == false diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/05_merged_maintenance_mode_deploy_wait.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/05_merged_maintenance_mode_deploy_wait.yaml deleted file mode 100644 index 2d6b555e8..000000000 --- a/tests/integration/targets/dcnm_maintenance_mode/tests/05_merged_maintenance_mode_deploy_wait.yaml +++ /dev/null @@ -1,399 +0,0 @@ ---- -################################################################################ -# RUNTIME -################################################################################ -# Recent run times (MM:SS.ms): -# 23:45.94 -# 23:49.52 -################################################################################ -# DESCRIPTION -# Normal mode to maintenance mode using deploy-maintenance-mode endpoint. -# deploy is set to true. -# wait_for_mode_change is set to true. -# -# State: merged -# Tests: -# - All tests use deploy-maintenance-mode endpoint. -# 1. Change normal mode switches to maintenance mode using playbook global config. -# 2. Change maintenance mode switches to normal mode using playbook global config. -# 3. Change normal mode switches to maintenance mode using playbook switch config. -# 4. Change maintenance mode switches to normal mode using playbook switch config. -# -# NOTES: -# - Execute either of the following testcases to setup the fabric and switches -# - 00_setup_fabrics_1x_rw.yaml (1x fabric with 2x switches) -# - 00_setup_fabrics_2x_rw.yaml (2x fabrics with 1x switch each) -################################################################################ -################################################################################ -# STEPS -################################################################################ -# SETUP -# 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. -# 1. MERGED - SETUP - Ensure switch mode is normal -# TEST -# GLOBAL CONFIG -# 2. Change switch mode to maintenance (global config) -# 3. Verify switch mode is maintenance (global config) -# 4. Change switch mode to normal (global config) -# 5. Verify switch mode is normal (global config) -# SWITCH CONFIG -# 6. Change switch mode to maintenance (switch config) -# 7. Verify switch mode is maintenance (switch config) -# 8. Change switch mode to normal (switch config) -# 9. Verify switch mode is normal (switch config) -# CLEANUP -# No cleanup needed. -################################################################################ -# REQUIREMENTS -################################################################################ -# Example vars for dcnm_maintenance_mode integration tests -# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) -# -# vars: -# # This testcase field can run any test in the tests directory for the role -# testcase: 05_merged_maintenance_mode_deploy_wait -# fabric_name_1: VXLAN_EVPN_Fabric -# fabric_type_1: VXLAN_EVPN -# fabric_name_3: LAN_CLASSIC_Fabric -# fabric_type_3: LAN_CLASSIC -# leaf_1: 172.22.150.103 -# leaf_2: 172.22.150.104 -# nxos_username: admin -# nxos_password: mypassword -################################################################################ -# 1. MERGED - SETUP - Ensure switch mode is normal -################################################################################ -# Expected result -# ok: [172.22.150.244] => { -# "result": { -# "changed": false, -# "diff": [ -# { -# "sequence_number": 1 -# }, -# { -# "sequence_number": 2 -# }, -# { -# "172.22.150.103": { -# "ip_address": "172.22.150.103", -# "mode": "normal", -# }, -# "172.22.150.104": { -# "ip_address": "172.22.150.104", -# "mode": "normal", -# }, -# "sequence_number": 3 -# } -# ], -- name: MERGED - SETUP - Ensure switch mode is normal - cisco.dcnm.dcnm_maintenance_mode: - state: query - config: - switches: - - ip_address: "{{ leaf_1 }}" - - ip_address: "{{ leaf_2 }}" - register: result - retries: 60 - delay: 10 - until: - - result.diff[2][leaf_1].mode == "normal" - - result.diff[2][leaf_2].mode == "normal" - -################################################################################ -# 2. MERGED - TEST - Change switch mode to maintenance (global config) -################################################################################ -- name: MERGED - TEST - Change switch mode to maintenance (global config) - cisco.dcnm.dcnm_maintenance_mode: - state: merged - config: - deploy: true - mode: maintenance - wait_for_mode_change: true - switches: - - ip_address: "{{ leaf_1 }}" - - ip_address: "{{ leaf_2 }}" - register: result_maintenance_mode -- debug: - var: result_maintenance_mode - -################################################################################ -# 3. MERGED - TEST - Verify switch mode is maintenance (global config) -################################################################################ -# Expected result -# ok: [172.22.150.244] => { -# "result": { -# "changed": false, -# "diff": [ -# { -# "sequence_number": 1 -# }, -# { -# "sequence_number": 2 -# }, -# { -# "172.22.150.103": { -# "ip_address": "172.22.150.103", -# "mode": "maintenance", -# }, -# "172.22.150.104": { -# "ip_address": "172.22.150.104", -# "mode": "maintenance", -# }, -# "sequence_number": 3 -# } -# ], -- name: MERGED - TEST - Verify switch mode is maintenance (global config) - cisco.dcnm.dcnm_maintenance_mode: - state: query - config: - switches: - - ip_address: "{{ leaf_1 }}" - - ip_address: "{{ leaf_2 }}" - register: result - retries: 60 - delay: 10 - until: - - result.diff[2][leaf_1].mode == "maintenance" - - result.diff[2][leaf_2].mode == "maintenance" - -################################################################################ -# 4. MERGED - TEST - Change switch mode to normal (global config) -################################################################################ -- name: MERGED - TEST - Change switch mode to normal (global config) - cisco.dcnm.dcnm_maintenance_mode: - state: merged - config: - deploy: true - mode: normal - wait_for_mode_change: true - switches: - - ip_address: "{{ leaf_1 }}" - - ip_address: "{{ leaf_2 }}" - register: result_normal_mode -- debug: - var: result_normal_mode - -################################################################################ -# 5. MERGED - TEST - Verify switch mode is normal (global config) -################################################################################ -# Expected result -# ok: [172.22.150.244] => { -# "result": { -# "changed": false, -# "diff": [ -# { -# "sequence_number": 1 -# }, -# { -# "sequence_number": 2 -# }, -# { -# "172.22.150.103": { -# "ip_address": "172.22.150.103", -# "mode": "normal", -# }, -# "172.22.150.104": { -# "ip_address": "172.22.150.104", -# "mode": "normal", -# }, -# "sequence_number": 3 -# } -# ], -- name: MERGED - TEST - Verify switch mode is normal (global config) - cisco.dcnm.dcnm_maintenance_mode: - state: query - config: - switches: - - ip_address: "{{ leaf_1 }}" - - ip_address: "{{ leaf_2 }}" - register: result - retries: 60 - delay: 10 - until: - - result.diff[2][leaf_1].mode == "normal" - - result.diff[2][leaf_2].mode == "normal" - -- assert: - that: - - result_maintenance_mode.failed == false - - result_maintenance_mode.metadata[2].action == "change_sytem_mode" - - result_maintenance_mode.metadata[3].action == "change_sytem_mode" - - result_maintenance_mode.metadata[2].check_mode == False - - result_maintenance_mode.metadata[3].check_mode == False - - result_maintenance_mode.metadata[2].state == "merged" - - result_maintenance_mode.metadata[3].state == "merged" - - result_maintenance_mode.response[2].DATA.status == "Success" - - result_maintenance_mode.response[3].DATA.status == "Success" - - result_maintenance_mode.response[2].METHOD == "POST" - - result_maintenance_mode.response[3].METHOD == "POST" - - result_maintenance_mode.response[2].RETURN_CODE == 200 - - result_maintenance_mode.response[3].RETURN_CODE == 200 - - result_maintenance_mode.response[4].DATA.status is match 'Success' - - result_maintenance_mode.response[5].DATA.status is match 'Success' - - result_normal_mode.failed == false - - result_normal_mode.metadata[2].action == "change_sytem_mode" - - result_normal_mode.metadata[3].action == "change_sytem_mode" - - result_normal_mode.metadata[2].check_mode == False - - result_normal_mode.metadata[3].check_mode == False - - result_normal_mode.metadata[2].state == "merged" - - result_normal_mode.metadata[3].state == "merged" - - result_normal_mode.response[2].DATA.status == "Success" - - result_normal_mode.response[3].DATA.status == "Success" - - result_normal_mode.response[2].METHOD == "DELETE" - - result_normal_mode.response[3].METHOD == "DELETE" - - result_normal_mode.response[2].RETURN_CODE == 200 - - result_normal_mode.response[3].RETURN_CODE == 200 - - result_normal_mode.response[4].DATA.status is match 'Success' - - result_normal_mode.response[5].DATA.status is match 'Success' - -################################################################################ -# 6. MERGED - TEST - Change switch mode to maintenance (switch config) -################################################################################ -- name: MERGED - TEST - Change switch mode to maintenance (switch config) - cisco.dcnm.dcnm_maintenance_mode: - state: merged - config: - deploy: true - switches: - - ip_address: "{{ leaf_1 }}" - mode: maintenance - wait_for_mode_change: true - - ip_address: "{{ leaf_2 }}" - mode: maintenance - wait_for_mode_change: true - register: result_maintenance_mode -- debug: - var: result_maintenance_mode - -################################################################################ -# 7. MERGED - TEST - Verify switch mode is maintenance (switch config) -################################################################################ -# Expected result (only relevant fields shown) -# ok: [172.22.150.244] => { -# "result": { -# "changed": false, -# "diff": [ -# { -# "sequence_number": 1 -# }, -# { -# "sequence_number": 2 -# }, -# { -# "172.22.150.103": { -# "ip_address": "172.22.150.103", -# "mode": "maintenance", -# }, -# "172.22.150.104": { -# "ip_address": "172.22.150.104", -# "mode": "maintenance", -# }, -# "sequence_number": 3 -# } -# ], -- name: MERGED - TEST - Verify switch mode is maintenance (switch config) - cisco.dcnm.dcnm_maintenance_mode: - state: query - config: - switches: - - ip_address: "{{ leaf_1 }}" - - ip_address: "{{ leaf_2 }}" - register: result - retries: 60 - delay: 10 - until: - - result.diff[2][leaf_1].mode == "maintenance" - - result.diff[2][leaf_2].mode == "maintenance" - -################################################################################ -# 8. MERGED - TEST - Change switch mode to normal (switch config) -################################################################################ -- name: MERGED - TEST - Change switch mode to normal (switch config) - cisco.dcnm.dcnm_maintenance_mode: - state: merged - config: - deploy: true - switches: - - ip_address: "{{ leaf_1 }}" - mode: normal - wait_for_mode_change: true - - ip_address: "{{ leaf_2 }}" - mode: normal - wait_for_mode_change: true - register: result_normal_mode -- debug: - var: result_normal_mode - -################################################################################ -# 9. MERGED - TEST - Verify switch mode is normal (switch config) -################################################################################ -# Expected result -# ok: [172.22.150.244] => { -# "result": { -# "changed": false, -# "diff": [ -# { -# "sequence_number": 1 -# }, -# { -# "sequence_number": 2 -# }, -# { -# "172.22.150.103": { -# "ip_address": "172.22.150.103", -# "mode": "normal", -# }, -# "172.22.150.104": { -# "ip_address": "172.22.150.104", -# "mode": "normal", -# }, -# "sequence_number": 3 -# } -# ], -- name: MERGED - TEST - Verify switch mode is normal (switch config) - cisco.dcnm.dcnm_maintenance_mode: - state: query - config: - switches: - - ip_address: "{{ leaf_1 }}" - - ip_address: "{{ leaf_2 }}" - register: result - retries: 60 - delay: 10 - until: - - result.diff[2][leaf_1].mode == "normal" - - result.diff[2][leaf_2].mode == "normal" - -- assert: - that: - - result_maintenance_mode.failed == false - - result_maintenance_mode.metadata[2].action == "change_sytem_mode" - - result_maintenance_mode.metadata[3].action == "change_sytem_mode" - - result_maintenance_mode.metadata[2].check_mode == False - - result_maintenance_mode.metadata[3].check_mode == False - - result_maintenance_mode.metadata[2].state == "merged" - - result_maintenance_mode.metadata[3].state == "merged" - - result_maintenance_mode.response[2].DATA.status == "Success" - - result_maintenance_mode.response[3].DATA.status == "Success" - - result_maintenance_mode.response[2].METHOD == "POST" - - result_maintenance_mode.response[3].METHOD == "POST" - - result_maintenance_mode.response[2].RETURN_CODE == 200 - - result_maintenance_mode.response[3].RETURN_CODE == 200 - - result_maintenance_mode.response[4].DATA.status is match 'Success' - - result_maintenance_mode.response[5].DATA.status is match 'Success' - - result_normal_mode.failed == false - - result_normal_mode.metadata[2].action == "change_sytem_mode" - - result_normal_mode.metadata[3].action == "change_sytem_mode" - - result_normal_mode.metadata[2].check_mode == False - - result_normal_mode.metadata[3].check_mode == False - - result_normal_mode.metadata[2].state == "merged" - - result_normal_mode.metadata[3].state == "merged" - - result_normal_mode.response[2].DATA.status == "Success" - - result_normal_mode.response[3].DATA.status == "Success" - - result_normal_mode.response[2].METHOD == "DELETE" - - result_normal_mode.response[3].METHOD == "DELETE" - - result_normal_mode.response[2].RETURN_CODE == 200 - - result_normal_mode.response[3].RETURN_CODE == 200 - - result_normal_mode.response[4].DATA.status is match 'Success' - - result_normal_mode.response[5].DATA.status is match 'Success' diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/05_merged_maintenance_mode_deploy_wait_top_level.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/05_merged_maintenance_mode_deploy_wait_top_level.yaml new file mode 100644 index 000000000..17c541e4c --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/05_merged_maintenance_mode_deploy_wait_top_level.yaml @@ -0,0 +1,167 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 06:01.03 +################################################################################ +# DESCRIPTION +# Normal mode to maintenance mode using deploy-maintenance-mode endpoint. +# deploy is set to true. +# wait_for_mode_change is set to true. +# +# State: merged +# Tests: +# - All tests use deploy-maintenance-mode endpoint. +# 1. Change normal mode switches to maintenance mode using playbook global config. +# 2. Change maintenance mode switches to normal mode using playbook global config. +# 3. Change normal mode switches to maintenance mode using playbook switch config. +# 4. Change maintenance mode switches to normal mode using playbook switch config. +# +# NOTES: +# - Execute either of the following testcases to setup the fabric and switches +# - 00_setup_fabrics_1x_rw.yaml (1x fabric with 2x switches) +# - 00_setup_fabrics_2x_rw.yaml (2x fabrics with 1x switch each) +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. +# 1. MERGED - SETUP - Ensure switch mode is normal +# TEST +# 2. Change switch mode to maintenance (top-level) +# 3. Verify switch mode is maintenance (top-level) +# CLEANUP +# No cleanup. +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: 05_merged_maintenance_mode_deploy_wait +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# fabric_name_3: LAN_CLASSIC_Fabric +# fabric_type_3: LAN_CLASSIC +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 1. MERGED - SETUP - Ensure switch mode is normal +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - SETUP - Ensure switch mode is normal + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +################################################################################ +# 2. MERGED - TEST - Change switch mode to maintenance (top-level) +################################################################################ +- name: MERGED - TEST - Change switch mode to maintenance (top-level) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: true + mode: maintenance + wait_for_mode_change: true + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result_maintenance_mode +- debug: + var: result_maintenance_mode + +################################################################################ +# 3. MERGED - TEST - Verify switch mode is maintenance (top-level) +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "maintenance", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "maintenance", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is maintenance (top-level) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "maintenance" + - result.diff[2][leaf_2].mode == "maintenance" + +- assert: + that: + - result_maintenance_mode.failed == false + - result_maintenance_mode.metadata[2].action == "change_sytem_mode" + - result_maintenance_mode.metadata[3].action == "change_sytem_mode" + - result_maintenance_mode.metadata[2].check_mode == False + - result_maintenance_mode.metadata[3].check_mode == False + - result_maintenance_mode.metadata[2].state == "merged" + - result_maintenance_mode.metadata[3].state == "merged" + - result_maintenance_mode.response[2].DATA.status == "Success" + - result_maintenance_mode.response[3].DATA.status == "Success" + - result_maintenance_mode.response[2].METHOD == "POST" + - result_maintenance_mode.response[3].METHOD == "POST" + - result_maintenance_mode.response[2].RETURN_CODE == 200 + - result_maintenance_mode.response[3].RETURN_CODE == 200 + - result_maintenance_mode.response[4].DATA.status is match 'Success' + - result_maintenance_mode.response[5].DATA.status is match 'Success' diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/06_merged_normal_mode_deploy_wait_top_level.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/06_merged_normal_mode_deploy_wait_top_level.yaml new file mode 100644 index 000000000..d458bfa11 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/06_merged_normal_mode_deploy_wait_top_level.yaml @@ -0,0 +1,168 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 06:01.63 +################################################################################ +# DESCRIPTION +# Normal mode to maintenance mode using deploy-maintenance-mode endpoint. +# deploy is set to true. +# wait_for_mode_change is set to true. +# +# +# State: merged +# Tests: +# - All tests use deploy-maintenance-mode endpoint. +# 1. Change normal mode switches to maintenance mode using playbook top-level. +# +# NOTES: +# - Execute either of the following testcases to setup the fabric and switches +# - 00_setup_fabrics_1x_rw.yaml (1x fabric with 2x switches) +# - 00_setup_fabrics_2x_rw.yaml (2x fabrics with 1x switch each) +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 0. Run either of the following to create read-write fabrics and add switches: +# - 00_setup_fabrics_1x_rw +# - 00_setup_fabrics_2x_rw +# 1. MERGED - SETUP - Ensure switch mode is maintenance +# TEST +# 2. Change switch mode to normal (top-level) +# 3. Verify switch mode is normal (top-level) +# CLEANUP +# No cleanup. +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: 06_merged_normal_mode_deploy_wait_top_level +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# fabric_name_3: LAN_CLASSIC_Fabric +# fabric_type_3: LAN_CLASSIC +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 1. MERGED - SETUP - Ensure switch mode is maintenance +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "maintenance", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "maintenance", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - SETUP - Ensure switch mode is maintenance + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 40 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "maintenance" + - result.diff[2][leaf_2].mode == "maintenance" + +################################################################################ +# 2. MERGED - TEST - Change switch mode to normal (top-level) +################################################################################ +- name: MERGED - TEST - Change switch mode to normal (top-level) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: true + mode: normal + wait_for_mode_change: true + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result_normal_mode +- debug: + var: result_normal_mode + + +################################################################################ +# 3. MERGED - TEST - Verify switch mode is normal (top-level) +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is normal (top-level) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +- assert: + that: + - result_normal_mode.failed == false + - result_normal_mode.metadata[2].action == "change_sytem_mode" + - result_normal_mode.metadata[3].action == "change_sytem_mode" + - result_normal_mode.metadata[2].check_mode == False + - result_normal_mode.metadata[3].check_mode == False + - result_normal_mode.metadata[2].state == "merged" + - result_normal_mode.metadata[3].state == "merged" + - result_normal_mode.response[2].DATA.status == "Success" + - result_normal_mode.response[3].DATA.status == "Success" + - result_normal_mode.response[2].METHOD == "DELETE" + - result_normal_mode.response[3].METHOD == "DELETE" + - result_normal_mode.response[2].RETURN_CODE == 200 + - result_normal_mode.response[3].RETURN_CODE == 200 + - result_normal_mode.response[4].DATA.status is match 'Success' + - result_normal_mode.response[5].DATA.status is match 'Success' diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/07_merged_maintenance_mode_deploy_wait_switch_level.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/07_merged_maintenance_mode_deploy_wait_switch_level.yaml new file mode 100644 index 000000000..34fafe960 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/07_merged_maintenance_mode_deploy_wait_switch_level.yaml @@ -0,0 +1,173 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 06:00.45 +################################################################################ +# DESCRIPTION +# Normal mode to maintenance mode using deploy-maintenance-mode endpoint. +# deploy is set to true. +# wait_for_mode_change is set to true. +# +# +# State: merged +# Tests: +# - All tests use deploy-maintenance-mode endpoint. +# 1. Change normal mode switches to maintenance mode using playbook switch-level. +# - top-level config is overridden by switch-level config. +# +# NOTES: +# - Execute either of the following testcases to setup the fabric and switches +# - 00_setup_fabrics_1x_rw.yaml (1x fabric with 2x switches) +# - 00_setup_fabrics_2x_rw.yaml (2x fabrics with 1x switch each) +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. +# 1. MERGED - SETUP - Ensure switch mode is normal +# TEST +# 2. Change switch mode to maintenance (switch-level) +# 3. Verify switch mode is maintenance (switch-level) +# CLEANUP +# No cleanup. +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: 07_merged_maintenance_mode_deploy_wait_switch_level +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# fabric_name_3: LAN_CLASSIC_Fabric +# fabric_type_3: LAN_CLASSIC +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 1. MERGED - SETUP - Ensure switch mode is normal +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - SETUP - Ensure switch mode is normal + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +################################################################################ +# 2. MERGED - TEST - Change switch mode to maintenance (switch-level) +# +# Override top-level config with switch-level config. +################################################################################ +- name: MERGED - TEST - Change switch mode to maintenance (switch-level) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: false + mode: normal + switches: + - ip_address: "{{ leaf_1 }}" + deploy: true + mode: maintenance + wait_for_mode_change: true + - ip_address: "{{ leaf_2 }}" + deploy: true + mode: maintenance + wait_for_mode_change: true + register: result_maintenance_mode +- debug: + var: result_maintenance_mode + +################################################################################ +# 7. MERGED - TEST - Verify switch mode is maintenance (switch-level) +################################################################################ +# Expected result (only relevant fields shown) +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "maintenance", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "maintenance", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is maintenance (switch-level) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "maintenance" + - result.diff[2][leaf_2].mode == "maintenance" + +- assert: + that: + - result_maintenance_mode.failed == false + - result_maintenance_mode.metadata[2].action == "change_sytem_mode" + - result_maintenance_mode.metadata[3].action == "change_sytem_mode" + - result_maintenance_mode.metadata[2].check_mode == False + - result_maintenance_mode.metadata[3].check_mode == False + - result_maintenance_mode.metadata[2].state == "merged" + - result_maintenance_mode.metadata[3].state == "merged" + - result_maintenance_mode.response[2].DATA.status == "Success" + - result_maintenance_mode.response[3].DATA.status == "Success" + - result_maintenance_mode.response[2].METHOD == "POST" + - result_maintenance_mode.response[3].METHOD == "POST" + - result_maintenance_mode.response[2].RETURN_CODE == 200 + - result_maintenance_mode.response[3].RETURN_CODE == 200 + - result_maintenance_mode.response[4].DATA.status is match 'Success' + - result_maintenance_mode.response[5].DATA.status is match 'Success' diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/08_merged_normal_mode_deploy_wait_switch_level.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/08_merged_normal_mode_deploy_wait_switch_level.yaml new file mode 100644 index 000000000..b72b3a388 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/08_merged_normal_mode_deploy_wait_switch_level.yaml @@ -0,0 +1,174 @@ +--- +################################################################################ +# RUNTIME +################################################################################ +# Recent run times (MM:SS.ms): +# 06:00.68 +################################################################################ +# DESCRIPTION +# Maintenance mode to Normal mode using deploy-maintenance-mode endpoint. +# deploy is set to true. +# wait_for_mode_change is set to true. +# +# +# State: merged +# Tests: +# - All tests use deploy-maintenance-mode endpoint. +# 1. Change normal mode switches to maintenance mode using playbook switch-level. +# - top-level config is overridden by switch-level config. +# +# NOTES: +# - Execute either of the following testcases to setup the fabric and switches +# - 00_setup_fabrics_1x_rw.yaml (1x fabric with 2x switches) +# - 00_setup_fabrics_2x_rw.yaml (2x fabrics with 1x switch each) +################################################################################ +################################################################################ +# STEPS +################################################################################ +# SETUP +# 0. Run 00_setup_fabrics_rw.yaml to create read-write fabrics and add switches. +# 1. MERGED - SETUP - Ensure switch mode is maintenance +# TEST +# 1. Change switch mode to normal (switch-level) +# 2. Verify switch mode is normal (switch-level) +# CLEANUP +# No cleanup +################################################################################ +# REQUIREMENTS +################################################################################ +# Example vars for dcnm_maintenance_mode integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: 08_merged_normal_mode_deploy_wait_switch_level +# fabric_name_1: VXLAN_EVPN_Fabric +# fabric_type_1: VXLAN_EVPN +# fabric_name_3: LAN_CLASSIC_Fabric +# fabric_type_3: LAN_CLASSIC +# leaf_1: 172.22.150.103 +# leaf_2: 172.22.150.104 +# nxos_username: admin +# nxos_password: mypassword +################################################################################ +# 1. MERGED - SETUP - Ensure switch mode is maintenance +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "maintenance", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "maintenance", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - SETUP - Ensure switch mode is maintenance + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "maintenance" + - result.diff[2][leaf_2].mode == "maintenance" + +################################################################################ +# 2. MERGED - TEST - Change switch mode to normal (switch-level) +# +# Override top-level config with switch-level config. +################################################################################ +- name: MERGED - TEST - Change switch mode to normal (switch-level) + cisco.dcnm.dcnm_maintenance_mode: + state: merged + config: + deploy: false + mode: maintenance + wait_for_mode_change: false + switches: + - ip_address: "{{ leaf_1 }}" + deploy: true + mode: normal + wait_for_mode_change: true + - ip_address: "{{ leaf_2 }}" + deploy: true + mode: normal + wait_for_mode_change: true + register: result_normal_mode +- debug: + var: result_normal_mode + +################################################################################ +# 3. MERGED - TEST - Verify switch mode is normal (switch-level) +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "sequence_number": 1 +# }, +# { +# "sequence_number": 2 +# }, +# { +# "172.22.150.103": { +# "ip_address": "172.22.150.103", +# "mode": "normal", +# }, +# "172.22.150.104": { +# "ip_address": "172.22.150.104", +# "mode": "normal", +# }, +# "sequence_number": 3 +# } +# ], +- name: MERGED - TEST - Verify switch mode is normal (switch-level) + cisco.dcnm.dcnm_maintenance_mode: + state: query + config: + switches: + - ip_address: "{{ leaf_1 }}" + - ip_address: "{{ leaf_2 }}" + register: result + retries: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "normal" + - result.diff[2][leaf_2].mode == "normal" + +- assert: + that: + - result_normal_mode.failed == false + - result_normal_mode.metadata[2].action == "change_sytem_mode" + - result_normal_mode.metadata[3].action == "change_sytem_mode" + - result_normal_mode.metadata[2].check_mode == False + - result_normal_mode.metadata[3].check_mode == False + - result_normal_mode.metadata[2].state == "merged" + - result_normal_mode.metadata[3].state == "merged" + - result_normal_mode.response[2].DATA.status == "Success" + - result_normal_mode.response[3].DATA.status == "Success" + - result_normal_mode.response[2].METHOD == "DELETE" + - result_normal_mode.response[3].METHOD == "DELETE" + - result_normal_mode.response[2].RETURN_CODE == 200 + - result_normal_mode.response[3].RETURN_CODE == 200 + - result_normal_mode.response[4].DATA.status is match 'Success' + - result_normal_mode.response[5].DATA.status is match 'Success' diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/07_merged_maintenance_mode_no_deploy.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/09_merged_maintenance_mode_no_deploy.yaml similarity index 100% rename from tests/integration/targets/dcnm_maintenance_mode/tests/07_merged_maintenance_mode_no_deploy.yaml rename to tests/integration/targets/dcnm_maintenance_mode/tests/09_merged_maintenance_mode_no_deploy.yaml From 251eb8b66a07ebf9eea598ea8c3226624e1c64e1 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 24 Jun 2024 13:07:11 -1000 Subject: [PATCH 205/230] dcnm_maintenance_mode: IT: Add README.md Adding README.md that provides an example dcnm_tests.yaml which includes all IP tests associated with this module and explains how to run them. --- .../dcnm_maintenance_mode/tests/README.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/README.md diff --git a/tests/integration/targets/dcnm_maintenance_mode/tests/README.md b/tests/integration/targets/dcnm_maintenance_mode/tests/README.md new file mode 100644 index 000000000..4b97c4baa --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/README.md @@ -0,0 +1,55 @@ +# Example dcnm_tests.yaml + +## Description of integration tests in tests/integration/targets/dcnm_maintenance_mode/tests + +Below is example contents for dcnm_tests.yaml to run integration tests assocated +with the ``dcnm_maintenance_mode`` module. + +Replace nxos_username and nxos_password with those used in your local setup. + +1. Run either of the 00_setup_fabrics_* tests first. + - 00_setup_fabrics_1x_rw - Add leaf_1 and leaf_2 to a single fabric. + - 00_setup_fabrics_2x_rw - Add leaf_1 to a VXLAN fabric and leaf_2 to a LAN Classic fabric. + +2. Run one or more of the commented test cases. These are numbered in pairs, + with the odd-numbered cases assuming the switches are currently in "normal" + mode, and the even-numbered cases assuming the switches are currently in + "maintenance" mode. Test case 09_merged_maintenance_mode_no_deploy is + not paired with any other script. It runs all "no_deploy" cases, since + these take very little time to complete. + + +```yaml +--- +- hosts: dcnm + gather_facts: no + connection: ansible.netcommon.httpapi + + vars: + # testcase: 00_setup_fabrics_1x_rw + # testcase: 00_setup_fabrics_2x_rw + # testcase: 01_merged_maintenance_mode_deploy_no_wait_switch_level + # testcase: 02_merged_normal_mode_deploy_no_wait_switch_level + # testcase: 03_merged_maintenance_mode_deploy_no_wait_top_level + # testcase: 04_merged_normal_mode_deploy_no_wait_top_level + # testcase: 05_merged_maintenance_mode_deploy_wait_top_level + # testcase: 06_merged_normal_mode_deploy_wait_top_level + # testcase: 07_merged_maintenance_mode_deploy_wait_switch_level + # testcase: 08_merged_normal_mode_deploy_wait_switch_level + # testcase: 09_merged_maintenance_mode_no_deploy + fabric_name_1: VXLAN_EVPN_Fabric + fabric_type_1: VXLAN_EVPN + fabric_name_2: VXLAN_EVPN_MSD_Fabric + fabric_type_2: VXLAN_EVPN_MSD + fabric_name_3: LAN_CLASSIC_Fabric + fabric_type_3: LAN_CLASSIC + fabric_name_4: IPFM_Fabric + fabric_type_4: IPFM + leaf_1: 192.168.1.2 + leaf_2: 192.168.1.3 + nxos_username: nxosUsername + nxos_password: nxosPassword + + roles: + - dcnm_maintenance_mode +``` \ No newline at end of file From 39305f4be5e6661475c82c2118821b6d00c41734 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 27 Jun 2024 12:14:08 -1000 Subject: [PATCH 206/230] Enable logging via ENV variable --- plugins/modules/dcnm_image_policy.py | 29 +++++++++------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/plugins/modules/dcnm_image_policy.py b/plugins/modules/dcnm_image_policy.py index 6df744710..7b7dc4ab0 100644 --- a/plugins/modules/dcnm_image_policy.py +++ b/plugins/modules/dcnm_image_policy.py @@ -259,7 +259,8 @@ from typing import Dict, List from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import Log +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import \ + Log from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults import \ @@ -803,24 +804,12 @@ def main(): } ansible_module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) - # Create the base/parent logger for the dcnm collection. - # To enable logging, set enable_logging to True. - # log.config can be either a dictionary, or a path to a JSON file - # Both dictionary and JSON file formats must be conformant with - # logging.config.dictConfig and must not log to the console. - # For an example configuration, see: - # $ANSIBLE_COLLECTIONS_PATH/cisco/dcnm/plugins/module_utils/common/logging_config.json - enable_logging = False - log = Log(ansible_module) - if enable_logging is True: - collection_path = ( - "/Users/arobel/repos/collections/ansible_collections/cisco/dcnm" - ) - config_file = ( - f"{collection_path}/plugins/module_utils/common/logging_config.json" - ) - log.config = config_file - log.commit() + # Logging setup + try: + log = Log() + log.commit() + except ValueError as error: + ansible_module.fail_json(str(error)) results = Results() if ansible_module.params["state"] == "deleted": @@ -849,7 +838,7 @@ def main(): results.build_final_result() - if True in results.failed: + if True in results.failed: # pylint: disable=unsupported-membership-test msg = "Module failed." ansible_module.fail_json(msg, **results.final_result) ansible_module.exit_json(**results.final_result) From 1ebf9b2eb6170d04583b6fde48b04bc5a3e7bd6d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 27 Jun 2024 13:06:40 -1000 Subject: [PATCH 207/230] Update integration tests, more... 1. Add correct test name to the comments in each integration test file. 2. Add directory playbooks/roles/dcnm_image_policy with example dcnm_hosts.yaml and dcnm_tests.yaml files. --- .../roles/dcnm_image_policy/dcnm_hosts.yaml | 20 ++++++++++ .../roles/dcnm_image_policy/dcnm_tests.yaml | 38 +++++++++++++++++++ .../tests/dcnm_image_policy_deleted.yaml | 2 +- .../tests/dcnm_image_policy_merged.yaml | 6 +-- .../tests/dcnm_image_policy_overridden.yaml | 2 +- .../tests/dcnm_image_policy_query.yaml | 2 +- .../tests/dcnm_image_policy_replaced.yaml | 2 +- 7 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 playbooks/roles/dcnm_image_policy/dcnm_hosts.yaml create mode 100644 playbooks/roles/dcnm_image_policy/dcnm_tests.yaml diff --git a/playbooks/roles/dcnm_image_policy/dcnm_hosts.yaml b/playbooks/roles/dcnm_image_policy/dcnm_hosts.yaml new file mode 100644 index 000000000..f22bf9dd7 --- /dev/null +++ b/playbooks/roles/dcnm_image_policy/dcnm_hosts.yaml @@ -0,0 +1,20 @@ +all: + vars: + ansible_user: "admin" + ansible_password: "password-secret" + ansible_python_interpreter: python + ansible_httpapi_validate_certs: False + ansible_httpapi_use_ssl: True + children: + dcnm: + vars: + ansible_connection: ansible.netcommon.httpapi + ansible_network_os: cisco.dcnm.dcnm + hosts: + dcnm-instance.example.com: + nxos: + hosts: + n9k-hosta.example.com: + ansible_connection: ansible.netcommon.network_cli + ansible_network_os: cisco.nxos.nxos + ansible_ssh_port: 22 diff --git a/playbooks/roles/dcnm_image_policy/dcnm_tests.yaml b/playbooks/roles/dcnm_image_policy/dcnm_tests.yaml new file mode 100644 index 000000000..60f275830 --- /dev/null +++ b/playbooks/roles/dcnm_image_policy/dcnm_tests.yaml @@ -0,0 +1,38 @@ +--- +# This playbook can be used to execute integration tests for +# the role located in: +# +# tests/integration/targets/dcnm_image_policy +# +# Modify the hosts and vars sections with details for your testing +# setup and uncomment the testcase you want to run. +# +- hosts: dcnm + gather_facts: no + connection: ansible.netcommon.httpapi + + vars: + # testcase: dcnm_image_policy_deleted + # testcase: dcnm_image_policy_merged + # testcase: dcnm_image_policy_overridden + # testcase: dcnm_image_policy_query + # testcase: dcnm_image_policy_replaced + switch_username: admin + switch_password: "foobar" + spine1: 172.22.150.114 + spine2: 172.22.150.115 + leaf1: 172.22.150.103 + leaf2: 172.22.150.104 + leaf3: 172.22.150.108 + leaf4: 172.22.150.109 + image_policy_1: "KR5M" + image_policy_2: "NR3F" + epld_image_1: n9000-epld.10.2.5.M.img + epld_image_2: n9000-epld.10.3.1.F.img + nxos_image_1: n9000-dk9.10.2.5.M.bin + nxos_image_2: n9000-dk9.10.3.1.F.bin + nxos_release_1: 10.2.5_nxos64-cs_64bit + nxos_release_2: 10.3.1_nxos64-cs_64bit + + roles: + - dcnm_image_policy diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted.yaml index 00b48c06b..6295ef78e 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted.yaml @@ -46,7 +46,7 @@ # # vars: # # This testcase field can run any test in the tests directory for the role -# testcase: deleted +# testcase: dcnm_image_policy_deleted # fabric_name: f1 # username: admin # password: "foobar" diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml index 65bb02c1f..f4ade64ee 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml @@ -42,7 +42,7 @@ # # vars: # # This testcase field can run any test in the tests directory for the role -# testcase: deleted +# testcase: dcnm_image_policy_merged # fabric_name: f1 # username: admin # password: "foobar" @@ -328,8 +328,8 @@ cisco.dcnm.dcnm_image_policy: state: deleted config: - - name: NR3F - - name: KR5M + - name: "{{ image_policy_1 }}" + - name: "{{ image_policy_2 }}" register: result - debug: diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml index a6d44130a..eb3a19753 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml @@ -50,7 +50,7 @@ # # vars: # # This testcase field can run any test in the tests directory for the role -# testcase: deleted +# testcase: dcnm_image_policy_overridden # fabric_name: f1 # username: admin # password: "foobar" diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_query.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_query.yaml index 08bcc330e..b1661c6db 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_query.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_query.yaml @@ -50,7 +50,7 @@ # # vars: # # This testcase field can run any test in the tests directory for the role -# testcase: deleted +# testcase: dcnm_image_policy_query # fabric_name: f1 # username: admin # password: "foobar" diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml index 39f629a25..447cb67cb 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml @@ -52,7 +52,7 @@ # # vars: # # This testcase field can run any test in the tests directory for the role -# testcase: deleted +# testcase: dcnm_image_policy_replaced # fabric_name: f1 # username: admin # password: "foobar" From 4a5295a0ead0f35c4959c1fff672c25c6e0e636e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 28 Jun 2024 17:23:53 -1000 Subject: [PATCH 208/230] Initial commit for v2 enhancements for dcnm_image_policy 1. Remove ImagePolicyCommon() module_utils/image_policy.common.py and all of its unit tests. Much if this was no longer needed. And, what was needed is moved into the various other support classes within module_utils/image_policy/*.py 2. Update IT files. 3. Change imports to point to the v2 versions of RestSend, etc. 4. Restructure several support classes to use the @Properties class decorator. 5. Properties().add_params() and Properties().params - added new commonly-used property. 6. EpPolicyDelete() new endpoint class. --- .../rest/policymgnt/policymgnt.py | 43 ++ plugins/module_utils/common/properties.py | 36 + plugins/module_utils/image_policy/common.py | 228 ------ plugins/module_utils/image_policy/create.py | 113 +-- plugins/module_utils/image_policy/delete.py | 171 +++-- .../image_policy/image_policies.py | 170 ++-- .../image_policy/params_spec_v2.py | 225 ++++++ plugins/module_utils/image_policy/payload.py | 185 ++--- plugins/module_utils/image_policy/query.py | 169 ++-- plugins/module_utils/image_policy/replace.py | 231 ++++-- plugins/module_utils/image_policy/update.py | 211 +++-- plugins/modules/dcnm_image_policy.py | 421 ++++++---- .../tests/dcnm_image_policy_deleted.yaml | 158 +--- .../tests/dcnm_image_policy_merged.yaml | 110 +-- .../tests/dcnm_image_policy_overridden.yaml | 235 ++---- .../tests/dcnm_image_policy_query.yaml | 152 ++-- .../tests/dcnm_image_policy_replaced.yaml | 196 ++--- .../payloads_ImagePolicyCreateBulk.json | 2 +- .../fixtures/responses_EpPolicies.json | 82 ++ .../fixtures/responses_EpPolicyCreate.json | 34 + .../fixtures/responses_ImagePolicyCommon.json | 49 -- .../fixtures/results_ImagePolicyCommon.json | 19 - .../test_image_policy_common.py | 726 ------------------ .../test_image_policy_create_bulk.py | 279 ++++--- .../test_image_policy_delete.py | 108 ++- .../test_image_policy_query.py | 80 +- .../modules/dcnm/dcnm_image_policy/utils.py | 61 +- 27 files changed, 2008 insertions(+), 2486 deletions(-) delete mode 100644 plugins/module_utils/image_policy/common.py create mode 100644 plugins/module_utils/image_policy/params_spec_v2.py create mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json delete mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_ImagePolicyCommon.json delete mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/results_ImagePolicyCommon.json delete mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_common.py diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py index cfa68834d..84e411db9 100644 --- a/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py @@ -133,6 +133,49 @@ def verb(self): return "GET" +class EpPolicyDelete(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPolicyDelete() + + ### Description + Delete image policies. + + ### Raises + - None + + ### Path + - ``/rest/policymgnt/policy`` + + ### Verb + - DELETE + + ### Notes + Expects a JSON payload as shown below, where ``policyNames`` is a + comma-separated list of policy names. + + ```json + { + "policyNames": "policyA,policyB,etc" + } + ``` + """ + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.policymgnt}/policy" + + @property + def verb(self): + return "DELETE" + + class EpPolicyAttach(PolicyMgnt): """ ## V1 API - PolicyMgnt().EpPolicyAttach() diff --git a/plugins/module_utils/common/properties.py b/plugins/module_utils/common/properties.py index 1ae51c292..5798aa201 100644 --- a/plugins/module_utils/common/properties.py +++ b/plugins/module_utils/common/properties.py @@ -37,6 +37,34 @@ class Properties: - ``rest_send``: Set and return nn instance of the ``RestSend`` class. - ``results``: Set and return an instance of the ``Results`` class. """ + @property + def params(self): + """ + ### Summary + A dictionary containing the following parameters: + - ``state``: The state of the module. + - ``check_mode``: A boolean indicating whether the module is in check mode. + """ + return self._params + + @params.setter + def params(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "params must be a dictionary. " + msg += f"got {type(value).__name__} for " + msg += f"value {value}" + raise TypeError(msg) + if value.get("state", None) is None: + msg = f"{self.class_name}.{method_name}: " + msg += "params.state is required but missing." + raise ValueError(msg) + if value.get("check_mode", None) is None: + msg = f"{self.class_name}.{method_name}: " + msg += "params.check_mode is required but missing." + raise ValueError(msg) + self._params = value @property def rest_send(self): @@ -106,6 +134,14 @@ def results(self, value): raise TypeError(msg) self._results = value + def add_params(self): + """ + ### Summary + Class decorator method to set the ``params`` property. + """ + self.params = Properties.params + return self + def add_rest_send(self): """ ### Summary diff --git a/plugins/module_utils/image_policy/common.py b/plugins/module_utils/image_policy/common.py deleted file mode 100644 index 3ce87f785..000000000 --- a/plugins/module_utils/image_policy/common.py +++ /dev/null @@ -1,228 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import inspect -import logging -from typing import Any, Dict - - -class ImagePolicyCommon: - """ - Common methods used by the other classes supporting - dcnm_image_policy module - - Usage (where ansible_module is an instance of - AnsibleModule or MockAnsibleModule): - - class MyClass(ImagePolicyCommon): - def __init__(self, module): - super().__init__(module) - ... - """ - - def __init__(self, ansible_module): - self.class_name = self.__class__.__name__ - - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED ImagePolicyCommon()") - - self.ansible_module = ansible_module - self.check_mode = self.ansible_module.check_mode - self.state = ansible_module.params["state"] - - self.params = ansible_module.params - - self.properties: Dict[str, Any] = {} - self.properties["results"] = None - - def _verify_image_policy_ref_count(self, instance, policy_names): - """ - instance: ImagePolicies() instance - policy_names: list of policy names - - Verify that all image policies in policy_names have a - ref_count of 0 (i.e. no devices are using the policy). - - If the ref_count is greater than 0, fail_json with a message - indicating that the policy, or policies, must be detached from - all devices before it/they can be deleted. - """ - method_name = inspect.stack()[0][3] - _non_zero_ref_counts = {} - for policy_name in policy_names: - instance.policy_name = policy_name - msg = f"instance.policy_name: {instance.policy_name}, " - msg += f"instance.ref_count: {instance.ref_count}." - self.log.debug(msg) - # If the policy does not exist on the controller, the ref_count - # will be None. We skip these too. - if instance.ref_count in [0, None]: - continue - _non_zero_ref_counts[policy_name] = instance.ref_count - if len(_non_zero_ref_counts) == 0: - return - msg = f"{self.class_name}.{method_name}: " - msg += "One or more policies have devices attached. " - msg += "Detach these policies from all devices first using " - msg += "the dcnm_image_upgrade module, with state == deleted. " - for policy_name, ref_count in _non_zero_ref_counts.items(): - msg += f"policy_name: {policy_name}, " - msg += f"ref_count: {ref_count}. " - self.ansible_module.fail_json(msg, **self.results.failed_result) - - def _default_policy(self, policy_name): - """ - Return a default policy payload for policy name. - """ - method_name = inspect.stack()[0][3] - if not isinstance(policy_name, str): - msg = f"{self.class_name}.{method_name}: " - msg += "policy_name must be a string. " - msg += f"Got type {type(policy_name).__name__} for " - msg += f"value {policy_name}." - self.log.debug(msg) - self.ansible_module.fail_json(msg, **self.results.failed_result) - - policy = { - "agnostic": False, - "epldImgName": "", - "nxosVersion": "", - "packageName": "", - "platform": "", - "policyDescr": "", - "policyName": policy_name, - "policyType": "PLATFORM", - "rpmimages": "", - } - return policy - - def _handle_response(self, response, verb): - """ - Call the appropriate handler for response based on verb - """ - if verb == "GET": - return self._handle_get_response(response) - if verb in {"POST", "PUT", "DELETE"}: - return self._handle_post_put_delete_response(response) - return self._handle_unknown_request_verbs(response, verb) - - def _handle_unknown_request_verbs(self, response, verb): - method_name = inspect.stack()[0][3] - - msg = f"{self.class_name}.{method_name}: " - msg += f"Unknown request verb ({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, DELETE responses from the controller. - - Returns: dict() with the following keys: - - changed: - - True if changes were made to by the controller - - ERROR key is not present - - MESSAGE == "OK" - - False otherwise - - success: - - False if MESSAGE != "OK" or ERROR key is present - - True otherwise - """ - result = {} - if response.get("ERROR") is not None: - result["success"] = False - result["changed"] = False - return result - if response.get("MESSAGE") != "OK" and response.get("MESSAGE") is not None: - result["success"] = False - result["changed"] = False - return result - result["success"] = True - result["changed"] = True - return result - - def make_boolean(self, value): - """ - Return value converted to boolean, if possible. - Return value, if value cannot be converted. - """ - if isinstance(value, bool): - return value - if isinstance(value, str): - if value.lower() in ["true", "yes"]: - return True - if value.lower() in ["false", "no"]: - return False - return value - - def make_none(self, value): - """ - Return None if value is an empty string, or a string - representation of a None type - Return value otherwise - """ - if value in ["", "none", "None", "NONE", "null", "Null", "NULL"]: - return None - return value - - @property - def results(self): - """ - An instance of the Results class. - """ - return self.properties["results"] - - @results.setter - def results(self, value): - self.properties["results"] = value diff --git a/plugins/module_utils/image_policy/create.py b/plugins/module_utils/image_policy/create.py index 37f1503c3..e5453227a 100644 --- a/plugins/module_utils/image_policy/create.py +++ b/plugins/module_utils/image_policy/create.py @@ -22,40 +22,38 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.common import \ - ImagePolicyCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ - ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.policymgnt.policymgnt import \ + EpPolicyCreate +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ + ImagePolicies -class ImagePolicyCreateCommon(ImagePolicyCommon): +@Properties.add_rest_send +@Properties.add_results +@Properties.add_params +class ImagePolicyCreateCommon: """ Common methods and properties for: - ImagePolicyCreate - ImagePolicyCreateBulk """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): self.class_name = self.__class__.__name__ self.action = "create" self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._image_policies = ImagePolicies(self.ansible_module) + self._image_policies = ImagePolicies() self._image_policies.results = Results() - self.endpoints = ApiEndpoints() - self.rest_send = RestSend(self.ansible_module) - - self.path = self.endpoints.policy_create.get("path") - self.verb = self.endpoints.policy_create.get("verb") + self.endpoint = EpPolicyCreate() + self.path = self.endpoint.path + self.verb = self.endpoint.verb self._payloads_to_commit = [] @@ -64,10 +62,14 @@ def __init__(self, ansible_module): self._mandatory_payload_keys.add("policyName") self._mandatory_payload_keys.add("policyType") + self._params = None + self._payload = None + self._payloads = None + self._rest_send = None + self._results = None + msg = "ENTERED ImagePolicyCreateCommon(): " msg += f"action: {self.action}, " - msg += f"check_mode: {self.check_mode}, " - msg += f"state: {self.state}" self.log.debug(msg) def _verify_payload(self, payload): @@ -80,7 +82,7 @@ def _verify_payload(self, payload): msg += "payload must be a dict. " msg += f"Got type {type(payload).__name__}, " msg += f"value {payload}" - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise TypeError(msg) missing_keys = [] for key in self._mandatory_payload_keys: @@ -92,7 +94,7 @@ def _verify_payload(self, payload): msg = f"{self.class_name}.{method_name}: " msg += "payload is missing mandatory keys: " msg += f"{sorted(missing_keys)}" - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) def _build_payloads_to_commit(self): """ @@ -105,6 +107,7 @@ def _build_payloads_to_commit(self): Populates self._payloads_to_commit with a list of payloads to commit. """ + self._image_policies.rest_send = self.rest_send self._image_policies.refresh() self._payloads_to_commit = [] @@ -122,7 +125,7 @@ def _send_payloads(self): In both cases, update results """ - self.rest_send.check_mode = self.check_mode + self.rest_send.check_mode = self.params.get("check_mode") for payload in self._payloads_to_commit: @@ -145,9 +148,11 @@ def _send_payloads(self): self.results.diff_current = copy.deepcopy(payload) self.results.action = self.action - self.results.state = self.state - self.results.check_mode = self.check_mode - self.results.response_current = copy.deepcopy(self.rest_send.response_current) + self.results.state = self.params.get("state") + self.results.check_mode = self.params.get("check_mode") + self.results.response_current = copy.deepcopy( + self.rest_send.response_current + ) self.results.result_current = copy.deepcopy(self.rest_send.result_current) self.results.register_task_result() @@ -159,7 +164,7 @@ def payloads(self): Payloads must be a list of dict. Each dict is a payload for the image policy create API endpoint. """ - return self.properties["payloads"] + return self._payloads @payloads.setter def payloads(self, value): @@ -169,10 +174,10 @@ def payloads(self, value): msg += "payloads must be a list of dict. " msg += f"got {type(value).__name__} for " msg += f"value {value}" - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise TypeError(msg) for item in value: self._verify_payload(item) - self.properties["payloads"] = value + self._payloads = value class ImagePolicyCreateBulk(ImagePolicyCreateCommon): @@ -213,8 +218,8 @@ class ImagePolicyCreateBulk(ImagePolicyCreateCommon): ] """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): + super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -222,15 +227,6 @@ def __init__(self, ansible_module): msg = "ENTERED ImagePolicyCreateBulk():" self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - Add properties specific to this class - """ - # properties dict is already initialized in the parent class - self.properties["payloads"] = None - def commit(self): """ create policies. Skip any policies that already exist @@ -238,10 +234,25 @@ def commit(self): """ method_name = inspect.stack()[0][3] + if self.params is None: + msg = f"{self.class_name}.{method_name}: " + msg += "params must be set prior to calling commit." + raise ValueError(msg) + if self.payloads is None: msg = f"{self.class_name}.{method_name}: " msg += "payloads must be set prior to calling commit." - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) + + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += "rest_send must be set prior to calling commit." + raise ValueError(msg) + + if self.results is None: + msg = f"{self.class_name}.{method_name}: " + msg += "results must be set prior to calling commit." + raise ValueError(msg) self._build_payloads_to_commit() if len(self._payloads_to_commit) == 0: @@ -283,8 +294,8 @@ class ImagePolicyCreate(ImagePolicyCreateCommon): """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): + super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -293,16 +304,6 @@ def __init__(self, ansible_module): self.log.debug(msg) self.data = {} - self.rest_send = RestSend(self.ansible_module) - - self._init_properties() - - def _init_properties(self): - """ - Add properties specific to this class - """ - # properties is already initialized in the parent class - self.properties["payload"] = None @property def payload(self): @@ -310,13 +311,13 @@ def payload(self): This class expects a properly-defined image policy payload. See class docstring for the payload structure. """ - return self.properties["payload"] + return self._payload @payload.setter def payload(self, value): self._verify_payload(value) - self.properties["payloads"] = [value] - self.properties["payload"] = value + self._payloads = [value] + self._payload = value def commit(self): """ @@ -327,7 +328,7 @@ def commit(self): if self.payload is None: msg = f"{self.class_name}.{method_name}: " msg += "payload must be set prior to calling commit." - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) self._build_payloads_to_commit() diff --git a/plugins/module_utils/image_policy/delete.py b/plugins/module_utils/image_policy/delete.py index ca2af31eb..dadbe7c11 100644 --- a/plugins/module_utils/image_policy/delete.py +++ b/plugins/module_utils/image_policy/delete.py @@ -21,64 +21,114 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.common import \ - ImagePolicyCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ - ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.policymgnt.policymgnt import \ + EpPolicyDelete +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ + ImagePolicies -class ImagePolicyDelete(ImagePolicyCommon): +@Properties.add_rest_send +@Properties.add_results +@Properties.add_params +class ImagePolicyDelete: """ + ### Summary Delete image policies - Usage: + ### Raises + - ``ValueError`` if: + - ``params`` is not set prior to calling commit. + - ``policy_names`` is not set prior to calling commit. + - ``rest_send`` is not set prior to calling commit. + - ``results`` is not set prior to calling commit. + - ``params`` is missing the ``check_mode`` key. + - ``params`` is missing the ``state`` key. + - ``state`` is not one of deleted, merged, overridden, query, replaced. + - One or more policies in ``policy_names`` have devices attached. + - ``TypeError`` if: + - ``policy_names`` is not a list. + - ``policy_names`` is not a list of strings. - instance = ImagePolicyDelete(ansible_module) + ### Usage + ```python + instance = ImagePolicyDelete() instance.policy_names = ["IMAGE_POLICY_1", "IMAGE_POLICY_2"] instance.commit() + ``` """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): self.class_name = self.__class__.__name__ - self.action = "delete" - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._policies_to_delete = [] - self._build_properties() - self.endpoints = ApiEndpoints() - self._image_policies = ImagePolicies(self.ansible_module) - self._image_policies.results = Results() - self.rest_send = RestSend(self.ansible_module) + self.action = "delete" + self.check_mode = None + self.endpoint = EpPolicyDelete() + self.path = self.endpoint.path + self.payload = None + self.state = None + self.verb = self.endpoint.verb - self.path = self.endpoints.policy_delete["path"] - self.verb = self.endpoints.policy_delete["verb"] + self._image_policies = ImagePolicies() + self._image_policies.results = Results() + self._params = None + self._policies_to_delete = [] + self._policy_names = None + self._results = None + self._rest_send = None msg = "ENTERED ImagePolicyDelete(): " msg += f"action: {self.action}, " - msg += f"check_mode: {self.check_mode}, " - msg += f"state: {self.state}" self.log.debug(msg) - def _build_properties(self): + def _verify_image_policy_ref_count(self, instance, policy_names): """ - self.properties holds property values for the class + ### Summary + Verify that all image policies in policy_names have a ref_count of 0 + (i.e. no devices are using the policy). + + ### Raises + - ``ValueError`` if any policy in policy_names has a ref_count + greater than 0 (i.e. devices are using the policy). + + ### Parameters + - ``instance`` : ImagePolicies() instance + - ``policy_names`` : list of policy names """ - # self.properties is already set in the parent class - self.properties["policy_names"] = None + method_name = inspect.stack()[0][3] + _non_zero_ref_counts = {} + for policy_name in policy_names: + instance.policy_name = policy_name + msg = f"{self.class_name}.{method_name}: " + msg += f"instance.policy_name: {instance.policy_name}, " + msg += f"instance.ref_count: {instance.ref_count}." + self.log.debug(msg) + # If the policy does not exist on the controller, the ref_count + # will be None. We skip these too. + if instance.ref_count in [0, None]: + continue + _non_zero_ref_counts[policy_name] = instance.ref_count + if len(_non_zero_ref_counts) == 0: + return + msg = f"{self.class_name}.{method_name}: " + msg += "One or more policies have devices attached. " + msg += "Detach these policies from all devices first using " + msg += "the dcnm_image_upgrade module, with state == deleted. " + for policy_name, ref_count in _non_zero_ref_counts.items(): + msg += f"policy_name: {policy_name}, " + msg += f"ref_count: {ref_count}. " + raise ValueError(msg) def _get_policies_to_delete(self) -> None: """ Retrieve policies from the controller and return the list of controller policies that are in our policy_names list. """ + self._image_policies.rest_send = self.rest_send # pylint: disable=no-member self._image_policies.refresh() self._verify_image_policy_ref_count(self._image_policies, self.policy_names) @@ -90,27 +140,48 @@ def _get_policies_to_delete(self) -> None: self.log.debug(msg) self._policies_to_delete.append(policy_name) + # pylint: disable=no-member def _validate_commit_parameters(self): """ validate the parameters for commit """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] + if self.params is None: + msg = f"{self.class_name}.{method_name}: " + msg += "params must be set prior to calling commit." + raise ValueError(msg) + if self.policy_names is None: msg = f"{self.class_name}.{method_name}: " msg += "policy_names must be set prior to calling commit." - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) + + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += "rest_send must be set prior to calling commit." + raise ValueError(msg) + + if self.results is None: + msg = f"{self.class_name}.{method_name}: " + msg += "results must be set prior to calling commit." + raise ValueError(msg) def commit(self): """ delete each of the image policies in self.policy_names """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] self._validate_commit_parameters() + self.check_mode = self.params.get("check_mode") + self.state = self.params.get("state") + self._get_policies_to_delete() - msg = f"self._policies_to_delete: {self._policies_to_delete}" + msg = f"{self.class_name}.{method_name}: " + msg += f"self._policies_to_delete: {self._policies_to_delete}" self.log.debug(msg) + if len(self._policies_to_delete) != 0: self._send_requests() else: @@ -124,11 +195,13 @@ def commit(self): self.results.failed = False self.results.response_current = {"RETURN_CODE": 200, "MESSAGE": msg} self.log.debug(msg) + self.results.register_task_result() def _send_requests(self): """ - If check_mode is False, send the requests to the controller - If check_mode is True, do not send the requests to the controller + ### Summary + - If check_mode is False, send the requests to the controller + - If check_mode is True, do not send the requests to the controller In both cases, populate the following lists: @@ -139,6 +212,8 @@ def _send_requests(self): - self.result_nok : list of results where success is False - self.diff_nok : list of payloads for which the request failed """ + method_name = inspect.stack()[0][3] + self.rest_send.save_settings() self.rest_send.check_mode = self.check_mode # We don't want RestSend to retry on errors since the likelihood of a @@ -146,7 +221,8 @@ def _send_requests(self): # are cases of permanent errors for which we don't want to retry. self.rest_send.timeout = 1 - msg = f"Deleting policies {self._policies_to_delete}" + msg = f"{self.class_name}.{method_name}: " + msg += f"Deleting policies {self._policies_to_delete}" self.log.debug(msg) self.payload = {"policyNames": self._policies_to_delete} @@ -154,15 +230,15 @@ def _send_requests(self): self.rest_send.verb = self.verb self.rest_send.payload = copy.deepcopy(self.payload) self.rest_send.commit() + self.rest_send.restore_settings() self.register_result() def register_result(self): """ + ### Summary Register the result of the fabric create request """ - msg = f"self.rest_send.result_current: {self.rest_send.result_current}" - self.log.debug(msg) if self.rest_send.result_current["success"]: self.results.failed = False self.results.diff_current = self.payload @@ -180,9 +256,14 @@ def register_result(self): @property def policy_names(self): """ - return the policy names + ### Summary + Return the policy names + + ### Raises + - ``TypeError`` if: + - ``policy_names`` is not a list of strings. """ - return self.properties["policy_names"] + return self._policy_names @policy_names.setter def policy_names(self, value): @@ -191,13 +272,13 @@ def policy_names(self, value): msg = f"{self.class_name}.{method_name}: " msg += "policy_names must be a list. " msg += f"got {type(value).__name__} for " - msg += f"value {value}" - self.ansible_module.fail_json(msg) + msg += f"value {value}." + raise TypeError(msg) for item in value: if not isinstance(item, str): msg = f"{self.class_name}.{method_name}: " msg += "policy_names must be a list of strings. " msg += f"got {type(item).__name__} for " - msg += f"value {item}" - self.ansible_module.fail_json(msg) - self.properties["policy_names"] = value + msg += f"list item {item}." + raise TypeError(msg) + self._policy_names = value diff --git a/plugins/module_utils/image_policy/image_policies.py b/plugins/module_utils/image_policy/image_policies.py index 946fd6e23..6bd51768f 100644 --- a/plugins/module_utils/image_policy/image_policies.py +++ b/plugins/module_utils/image_policy/image_policies.py @@ -20,24 +20,37 @@ import copy import inspect import logging -from typing import Any, AnyStr, Dict -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.common import \ - ImagePolicyCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.policymgnt.policymgnt import \ + EpPolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ + ConversionUtils +from ansible_collections.cisco.dcnm.plugins.module_utils.common.exceptions import \ + ControllerResponseError +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties -class ImagePolicies(ImagePolicyCommon): +@Properties.add_rest_send +@Properties.add_results +class ImagePolicies: """ Retrieve image policy details from the controller and provide property accessors for the policy attributes. - Usage (where module is an instance of AnsibleModule): + ### Usage - instance = ImagePolicies(module).refresh() + ```python + sender = Sender() + sender.ansible_module = ansible_module + rest_send = RestSend(ansible_module.params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + instance = ImagePolicies() + instance.rest_send = rest_send + instance.results = Results() + instance.refresh() instance.policy_name = "NR3F" if instance.name is None: print("policy NR3F does not exist on the controller") @@ -53,44 +66,70 @@ class ImagePolicies(ImagePolicyCommon): /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): self.class_name = self.__class__.__name__ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED ImagePolicies()") - - self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.endpoints = ApiEndpoints() - self.rest_send = RestSend(self.ansible_module) - - # We always want to get the controller's current image policy - # state so we set check_mode to False here so the request will be - # sent to the controller - self.rest_send.check_mode = False - - self._init_properties() + self.conversion = ConversionUtils() + self.endpoint = EpPolicies() + self.data = {} + self._all_policies = None + self._policy_name = None + self._response_data = None + self._results = None + self._rest_send = None - def _init_properties(self): - self.method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - # self.properties is already initialized in the parent class - self.properties["all_policies"] = None - self.properties["response_data"] = None - self.properties["policy_name"] = None + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = f"ENTERED {self.class_name}.{method_name}" + self.log.debug(msg) + # pylint: disable=no-member def refresh(self): """ + ### Summary Refresh the image policy details from the controller and populate self.data with the results. self.data is a dictionary of image policy details, keyed on image policy name. - """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.rest_send.path = self.endpoints.policies_info.get("path") - self.rest_send.verb = self.endpoints.policies_info.get("verb") + ### Raises + - ``ControllerResponseError`` if: + - The controller response is missing the expected data. + - ``ValueError`` if: + - ``rest_send`` is not set. + - ``results`` is not set. + - The controller response cannot be parsed. + + ### Notes + - pylint: disable=no-member is needed because the rest_send, results, + and params properties are dynamically created by the + @Properties class decorators. + """ + method_name = inspect.stack()[0][3] + + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += "instance.rest_send must be set before calling refresh." + raise ValueError(msg) + if self.results is None: + msg = f"{self.class_name}.{method_name}: " + msg += "instance.results must be set before calling refresh." + raise ValueError(msg) + + # We always want to get the controller's current image policy + # state. We set check_mode to False here so the request will be + # sent to the controller. + msg = f"{self.class_name}.{method_name}: " + msg += f"endpoint.verb: {self.endpoint.verb}, " + msg += f"endpoint.path: {self.endpoint.path}, " + self.log.debug(msg) + self.rest_send.save_settings() + self.rest_send.check_mode = False + self.rest_send.path = self.endpoint.path + self.rest_send.verb = self.endpoint.verb self.rest_send.commit() + self.rest_send.restore_settings() data = self.rest_send.response_current.get("DATA", {}).get("lastOperDataObject") @@ -98,14 +137,14 @@ def refresh(self): msg = f"{self.class_name}.{self.method_name}: " msg += "Bad response when retrieving image policy " msg += "information from the controller." - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ControllerResponseError(msg) if len(data) == 0: msg = "the controller has no defined image policies." self.log.debug(msg) - self.properties["response_data"] = {} - self.properties["all_policies"] = {} + self._response_data = {} + self._all_policies = {} self.data = {} for policy in data: @@ -113,51 +152,58 @@ def refresh(self): if policy_name is None: msg = f"{self.class_name}.{self.method_name}: " msg += "Cannot parse policy information from the controller." - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) self.data[policy_name] = policy - self.properties["response_data"][policy_name] = policy + self._response_data[policy_name] = policy - self.properties["all_policies"] = copy.deepcopy( - self.properties["response_data"] + self._all_policies = copy.deepcopy( + self._response_data ) self.results.response_current = self.rest_send.response_current - self.results.response = self.rest_send.response_current self.results.result_current = self.rest_send.result_current - self.results.result = self.rest_send.result_current def _get(self, item): - self.method_name = inspect.stack()[0][3] + """ + ### Summary + Return the value of item from the policy matching self.policy_name. + + ### Raises + - ``ValueError`` if ``policy_name`` is not set.. + """ + method_name = inspect.stack()[0][3] if self.policy_name is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "instance.policy_name must be set before " msg += f"accessing property {item}." - self.ansible_module.fail_json(msg, **self.failed_result) + raise ValueError(msg) - if self.policy_name not in self.properties["response_data"]: + if self.policy_name not in self._response_data: return None if item == "policy": - return self.properties["response_data"][self.policy_name] + return self._response_data[self.policy_name] - if item not in self.properties["response_data"][self.policy_name]: - msg = f"{self.class_name}.{self.method_name}: " + if item not in self._response_data[self.policy_name]: + msg = f"{self.class_name}.{method_name}: " msg += f"{self.policy_name} does not have a key named {item}." - self.ansible_module.fail_json(msg, **self.failed_result) + raise ValueError(msg) - return self.make_boolean( - self.make_none(self.properties["response_data"][self.policy_name][item]) + return self.conversion.make_boolean( + self.conversion.make_none( + self._response_data[self.policy_name][item] + ) ) @property - def all_policies(self) -> Dict[AnyStr, Any]: + def all_policies(self) -> dict: """ Return dict containing all policies, keyed on policy_name """ - if self.properties["all_policies"] is None: + if self._all_policies is None: return {} - return self.properties["all_policies"] + return self._all_policies @property def description(self): @@ -193,11 +239,11 @@ def policy_name(self): This must be set prior to accessing any other properties """ - return self.properties.get("policy_name") + return self._policy_name @policy_name.setter def policy_name(self, value): - self.properties["policy_name"] = value + self._policy_name = value @property def policy(self): @@ -218,13 +264,13 @@ def policy_type(self): return self._get("policyType") @property - def response_data(self) -> Dict[AnyStr, Any]: + def response_data(self) -> dict: """ Return dict containing the DATA portion of a controller response, keyed on policy_name """ - if self.properties["response_data"] is None: + if self._response_data is None: return {} - return self.properties["response_data"] + return self._response_data @property def nxos_version(self): diff --git a/plugins/module_utils/image_policy/params_spec_v2.py b/plugins/module_utils/image_policy/params_spec_v2.py new file mode 100644 index 000000000..e76ac0215 --- /dev/null +++ b/plugins/module_utils/image_policy/params_spec_v2.py @@ -0,0 +1,225 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import inspect +import logging +from typing import Any, Dict + + +class ParamsSpec: + """ + Parameter specifications for the dcnm_image_policy module. + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + self._params_spec: dict = {} + self.valid_states = set() + self.valid_states.add("deleted") + self.valid_states.add("merged") + self.valid_states.add("overridden") + self.valid_states.add("query") + self.valid_states.add("replaced") + + self.log.debug("ENTERED ParamsSpec() v2") + + def commit(self): + """ + Build the parameter specification based on the state + + ## Raises + - ``ValueError`` if params is not set + + """ + method_name = inspect.stack()[0][3] + + if self._params is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"params must be set before calling {method_name}." + raise ValueError(msg) + + if self.params["state"] == "deleted": + self._build_params_spec_for_deleted_state() + if self.params["state"] == "merged": + self._build_params_spec_for_merged_state() + if self.params["state"] == "overridden": + self._build_params_spec_for_overridden_state() + if self.params["state"] == "query": + self._build_params_spec_for_query_state() + if self.params["state"] == "replaced": + self._build_params_spec_for_replaced_state() + + def _build_params_spec_for_merged_state(self) -> None: + """ + Build the specs for the parameters expected when state == merged. + + Caller: _validate_configs() + Return: params_spec, a dictionary containing playbook + parameter specifications. + """ + self._params_spec: dict = {} + + self._params_spec["agnostic"] = {} + self._params_spec["agnostic"]["default"] = False + self._params_spec["agnostic"]["required"] = False + self._params_spec["agnostic"]["type"] = "bool" + + self._params_spec["description"] = {} + self._params_spec["description"]["default"] = "" + self._params_spec["description"]["required"] = False + self._params_spec["description"]["type"] = "str" + + self._params_spec["epld_image"] = {} + self._params_spec["epld_image"]["default"] = "" + self._params_spec["epld_image"]["required"] = False + self._params_spec["epld_image"]["type"] = "str" + + self._params_spec["name"] = {} + self._params_spec["name"]["required"] = True + self._params_spec["name"]["type"] = "str" + + self._params_spec["platform"] = {} + self._params_spec["platform"]["required"] = True + self._params_spec["platform"]["type"] = "str" + self._params_spec["platform"]["choices"] = ["N9K", "N7K", "N77", "N6K", "N5K"] + + self._params_spec["packages"] = {} + self._params_spec["packages"]["default"] = {} + self._params_spec["packages"]["required"] = False + self._params_spec["packages"]["type"] = "dict" + + self._params_spec["packages"]["install"] = {} + self._params_spec["packages"]["install"]["default"] = [] + self._params_spec["packages"]["install"]["required"] = False + self._params_spec["packages"]["install"]["type"] = "list" + + self._params_spec["packages"]["uninstall"] = {} + self._params_spec["packages"]["uninstall"]["default"] = [] + self._params_spec["packages"]["uninstall"]["required"] = False + self._params_spec["packages"]["uninstall"]["type"] = "list" + + self._params_spec["release"] = {} + self._params_spec["release"]["required"] = True + self._params_spec["release"]["type"] = "str" + + self._params_spec["type"] = {} + self._params_spec["type"]["default"] = "PLATFORM" + self._params_spec["type"]["required"] = False + self._params_spec["type"]["type"] = "str" + + def _build_params_spec_for_overridden_state(self) -> None: + self._build_params_spec_for_merged_state() + + def _build_params_spec_for_replaced_state(self) -> None: + self._build_params_spec_for_merged_state() + + def _build_params_spec_for_deleted_state(self) -> None: + """ + Build the specs for the parameters expected when state == deleted. + + Caller: _validate_configs() + Return: params_spec, a dictionary containing playbook + parameter specifications. + """ + self._params_spec: dict = {} + + self._params_spec["name"] = {} + self._params_spec["name"]["required"] = True + self._params_spec["name"]["type"] = "str" + + def _build_params_spec_for_query_state(self) -> None: + """ + Build the specs for the parameters expected when state == query. + + Caller: _validate_configs() + Return: params_spec, a dictionary containing playbook + parameter specifications. + """ + self._params_spec: dict = {} + + self._params_spec["name"] = {} + self._params_spec["name"]["required"] = True + self._params_spec["name"]["type"] = "str" + + def _build_params_spec_for_replaced_state(self) -> None: + self._build_params_spec_for_merged_state() + + @property + def params(self) -> dict: + """ + ### Summary + Expects value to be a dictionary containing, at mimimum, + the key "state" with value of one of: + - deleted + - merged + - overridden + - query + - replaced + + ### Raises + - setter: ``ValueError`` if value is not a dict + - setter: ``ValueError`` if value["state"] is missing + - setter: ``ValueError`` if value["state"] is not a valid state + + ### Details + - Valid params: + - ``{"state": "deleted"}`` + - ``{"state": "merged"}`` + - ``{"state": "overridden"}`` + - ``{"state": "query"}`` + - ``{"state": "replaced"}`` + - getter: return the params + - setter: set the params + """ + return self._params + + @params.setter + def params(self, value: dict) -> None: + """ + - setter: set the params + """ + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}.setter: " + msg += "Invalid type. Expected dict but " + msg += f"got type {type(value).__name__}, " + msg += f"value {value}." + raise TypeError(msg) + + if value.get("state", None) is None: + msg = f"{self.class_name}.{method_name}.setter: " + msg += "params.state is required but missing." + raise ValueError(msg) + + if value["state"] not in self.valid_states: + msg = f"{self.class_name}.{method_name}.setter: " + msg += f"params.state is invalid: {value['state']}. " + msg += f"Expected one of {', '.join(self.valid_states)}." + raise ValueError(msg) + + self._params = value + + @property + def params_spec(self) -> Dict[str, Any]: + """ + return the parameter specification + """ + return self._params_spec diff --git a/plugins/module_utils/image_policy/payload.py b/plugins/module_utils/image_policy/payload.py index 329e60ac9..63c71cb7c 100644 --- a/plugins/module_utils/image_policy/payload.py +++ b/plugins/module_utils/image_policy/payload.py @@ -21,78 +21,92 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.common import \ - ImagePolicyCommon - -class Payload(ImagePolicyCommon): +class Payload: """ Base class for Config2Payload and Payload2Config """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): self.class_name = self.__class__.__name__ - self.ansible_module = ansible_module - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED Payload()") - self._build_properties() + self._config = {} + self._params = {} + self._payload = {} - def _build_properties(self): + msg = "ENTERED Payload()" + self.log.debug(msg) + + @property + def config(self): """ - self.properties holds property values for the class + return the playbook configuration """ - # self.properties is instantiated in ImagePolicyCommon - self.properties["payload"] = {} - self.properties["config"] = {} + return self._config + + @config.setter + def config(self, value): + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}: " + msg += "config must be a dictionary. " + msg += f"got {type(value).__name__} for " + msg += f"value {value}" + raise TypeError(msg) + self._config = value @property - def payload(self): + def params(self): """ - return the payload + return the params dict """ - return self.properties["payload"] + return self._params - @payload.setter - def payload(self, value): + @params.setter + def params(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "payload must be a dictionary. " + msg += "params must be a dictionary. " msg += f"got {type(value).__name__} for " msg += f"value {value}" - self.ansible_module.fail_json(msg) - self.properties["payload"] = value + raise TypeError(msg) + self._params = value @property - def config(self): + def payload(self): """ - return the playbook configuration + return the payload """ - return self.properties["config"] + return self._payload - @config.setter - def config(self, value): + @payload.setter + def payload(self, value): method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}: " - msg += "config must be a dictionary. " + msg += "payload must be a dictionary. " msg += f"got {type(value).__name__} for " msg += f"value {value}" - self.ansible_module.fail_json(msg) - self.properties["config"] = value + raise TypeError(msg) + self._payload = value class Config2Payload(Payload): """ + ### Summary Convert an image_policy configuration into a payload for a POST request to the image-policy API endpoint. + + ### Raises + - ``ValueError`` if: + - self.config is empty + - self.params is is not set prior to calling commit() """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): + super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -104,47 +118,42 @@ def commit(self): """ method_name = inspect.stack()[0][3] + if self.params is None: + msg = f"{self.class_name}.{method_name}: " + msg += "params must be set before calling commit()." + raise ValueError(msg) + msg = f"{self.class_name}.{method_name}: " - msg += f"properties[config] {json.dumps(self.properties['config'], indent=4, sort_keys=True)}" + msg += f"self.config {json.dumps(self.config, indent=4, sort_keys=True)}" self.log.debug(msg) - if self.properties["config"] == {}: + if self.config == {}: msg = f"{self.class_name}.{method_name}: " msg += "config is empty" - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) msg = f"{self.class_name}.{method_name}: " - msg += f"HERE 1 STATE: {self.ansible_module.params['state']}" + msg += f"HERE 1 STATE: {self.params['state']}" self.log.debug(msg) - if self.ansible_module.params["state"] in ["deleted", "query"]: - self.properties["payload"]["policyName"] = self.properties["config"]["name"] + if self.params["state"] in ["deleted", "query"]: + self.payload["policyName"] = self.config["name"] return - self.properties["payload"]["agnostic"] = self.properties["config"]["agnostic"] - self.properties["payload"]["epldImgName"] = self.properties["config"][ - "epld_image" - ] - self.properties["payload"]["nxosVersion"] = self.properties["config"]["release"] - self.properties["payload"]["platform"] = self.properties["config"]["platform"] - self.properties["payload"]["policyDescr"] = self.properties["config"][ - "description" - ] - self.properties["payload"]["policyName"] = self.properties["config"]["name"] - self.properties["payload"]["policyType"] = self.properties["config"].get( - "type", "PLATFORM" - ) - - if len(self.properties["config"].get("packages", {}).get("install", [])) != 0: - self.properties["payload"]["packageName"] = ",".join( - self.properties["config"]["packages"]["install"] - ) - if len(self.properties["config"].get("packages", {}).get("uninstall", [])) != 0: - self.properties["payload"]["rpmimages"] = ",".join( - self.properties["config"]["packages"]["uninstall"] - ) + self.payload["agnostic"] = self.config["agnostic"] + self.payload["epldImgName"] = self.config["epld_image"] + self.payload["nxosVersion"] = self.config["release"] + self.payload["platform"] = self.config["platform"] + self.payload["policyDescr"] = self.config["description"] + self.payload["policyName"] = self.config["name"] + self.payload["policyType"] = self.config.get("type", "PLATFORM") + + if len(self.config.get("packages", {}).get("install", [])) != 0: + self.payload["packageName"] = ",".join(self.config["packages"]["install"]) + if len(self.config.get("packages", {}).get("uninstall", [])) != 0: + self.payload["rpmimages"] = ",".join(self.config["packages"]["uninstall"]) msg = f"{self.class_name}.{method_name}: " - msg += f"properties[payload] {json.dumps(self.properties['payload'], indent=4, sort_keys=True)}" + msg += f"self.payload {json.dumps(self.payload, indent=4, sort_keys=True)}" self.log.debug(msg) @@ -154,8 +163,8 @@ class Payload2Config(Payload): configuration. """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): + super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -163,37 +172,33 @@ def __init__(self, ansible_module): def commit(self): """ + ### Summary build the config from the payload + + ### Raises + - ``ValueError`` if payload is empty """ method_name = inspect.stack()[0][3] - if self.properties["payload"] == {}: + if self.payload == {}: msg = f"{self.class_name}.{method_name}: " msg += "payload is empty" - self.ansible_module.fail_json(msg) - - self.properties["config"]["agnostic"] = self.properties["payload"]["agnostic"] - self.properties["config"]["epld_image"] = self.properties["payload"][ - "epldImgName" - ] - self.properties["config"]["release"] = self.properties["payload"]["nxosVersion"] - self.properties["config"]["platform"] = self.properties["payload"]["platform"] - self.properties["config"]["description"] = self.properties["payload"][ - "policyDescr" - ] - self.properties["config"]["name"] = self.properties["payload"]["policyName"] - self.properties["config"]["type"] = self.properties["payload"]["policyType"] - - self.properties["config"]["packages"] = {} - if self.properties["payload"].get("packageName", "") != "": - self.properties["config"]["packages"]["install"] = self.properties[ - "payload" - ]["packageName"].split(",") + raise ValueError(msg) + + self.config["agnostic"] = self.payload["agnostic"] + self.config["epld_image"] = self.payload["epldImgName"] + self.config["release"] = self.payload["nxosVersion"] + self.config["platform"] = self.payload["platform"] + self.config["description"] = self.payload["policyDescr"] + self.config["name"] = self.payload["policyName"] + self.config["type"] = self.payload["policyType"] + + self.config["packages"] = {} + if self.payload.get("packageName", "") != "": + self.config["packages"]["install"] = self.payload["packageName"].split(",") else: - self.properties["config"]["packages"]["install"] = [] - if self.properties["payload"].get("rpmimages", "") != "": - self.properties["config"]["packages"]["uninstall"] = self.properties[ - "payload" - ]["rpmimages"].split(",") + self.config["packages"]["install"] = [] + if self.payload.get("rpmimages", "") != "": + self.config["packages"]["uninstall"] = self.payload["rpmimages"].split(",") else: - self.properties["config"]["packages"]["uninstall"] = [] + self.config["packages"]["uninstall"] = [] diff --git a/plugins/module_utils/image_policy/query.py b/plugins/module_utils/image_policy/query.py index 1db296d69..eef2080f8 100644 --- a/plugins/module_utils/image_policy/query.py +++ b/plugins/module_utils/image_policy/query.py @@ -20,59 +20,79 @@ import inspect import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.common import \ - ImagePolicyCommon -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ - ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ - Results +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties -class ImagePolicyQuery(ImagePolicyCommon): +@Properties.add_params +@Properties.add_results +class ImagePolicyQuery: """ + ### Summary Query image policies - Usage: - - instance = ImagePolicyQuery(ansible_module) + ### Raises + - ``ValueError`` if: + - params is not set. + - policy_names is not set. + - image_policies is not set. + - ``TypeError`` if: + - policy_names is not a list. + - policy_names contains anything other than strings. + - image_policies is not an instance of ImagePolicies. + + ### Usage + ```python + sender = Sender() + sender.ansible_module = ansible_module + rest_send = RestSend(ansible_module.params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + results = Results() + + image_policies = ImagePolicies() + image_policies.rest_send = rest_send + image_policies.results = results + + instance = ImagePolicyQuery() + instance.image_policies = ImagePolicies() + instance.results = results instance.policy_names = ["IMAGE_POLICY_1", "IMAGE_POLICY_2"] instance.commit() - diff = instance.diff # contains the image policy information - result = instance.result # contains the result(s) of the query - response = instance.response # contains the response(s) from the controller + diff = instance.results.diff_current # contains the image policy information + result = instance.results.result_current # contains the result(s) of the query + response = instance.results.response_current # contains the response(s) from the controller + ``` """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): self.class_name = self.__class__.__name__ self._policies_to_query = [] - self._build_properties() - self._image_policies = ImagePolicies(self.ansible_module) - self._image_policies.results = Results() self.action = "query" + self._results = None self.log = logging.getLogger(f"dcnm.{self.class_name}") msg = "ENTERED ImagePolicyQuery(): " msg += f"action {self.action}, " - msg += f"check_mode {self.check_mode}, " - msg += f"state {self.state}" self.log.debug(msg) - def _build_properties(self): - """ - self.properties holds property values for the class - """ - # self.properties is already set in the parent class - self.properties["policy_names"] = None - @property def policy_names(self): """ + ### Summary return the policy names + + ### Raises + - ``TypeError`` if: + - policy_names is not a list. + - policy_names contains anything other than strings. + - ``ValueError`` if: + - policy_names list is empty. """ - return self.properties["policy_names"] + return self._policy_names @policy_names.setter def policy_names(self, value): @@ -82,55 +102,88 @@ def policy_names(self, value): msg += "policy_names must be a list. " msg += f"got {type(value).__name__} for " msg += f"value {value}" - self.ansible_module.fail_json(msg) + raise TypeError(msg) if len(value) == 0: msg = f"{self.class_name}.{method_name}: " msg += "policy_names must be a list of at least one string. " msg += f"got {value}." - self.ansible_module.fail_json(msg) + raise ValueError(msg) for item in value: if not isinstance(item, str): msg = f"{self.class_name}.{method_name}: " msg += "policy_names must be a list of strings. " msg += f"got {type(item).__name__} for " msg += f"value {item}" - self.ansible_module.fail_json(msg) - self.properties["policy_names"] = value + raise TypeError(msg) + self._policy_names = value + # pylint: disable=no-member def commit(self): """ + ### Summary query each of the image policies in self.policy_names + + ### Raises + - ``ValueError`` if: + - params is not set. + - policy_names is not set. + - image_policies is not set. + + ### Notes + - pylint: disable=no-member is needed due to the rest_send property + being dynamically created by the @Properties.add_results decorator. """ method_name = inspect.stack()[0][3] + if self.params is None: + msg = f"{self.class_name}.{method_name}: " + msg += "params must be set prior to calling commit." + raise ValueError(msg) + if self.policy_names is None: msg = f"{self.class_name}.{method_name}: " msg += "policy_names must be set prior to calling commit." - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) + + if self.image_policies is None: + msg = f"{self.class_name}.{method_name}: " + msg += "image_policies must be set to an instance of " + msg += "ImagePolicies() before calling commit." + raise ValueError(msg) - self._image_policies.refresh() + self.image_policies.refresh() self.results.action = self.action - self.results.check_mode = self.check_mode - self.results.state = self.state + self.results.check_mode = self.params.get("check_mode") + self.results.state = self.params.get("state", None) - if self._image_policies.results.result_current.get("success") is False: + if self.image_policies.results.result_current.get("success") is False: self.results.diff_current = {} self.results.failed = True - self.results.response_current = copy.deepcopy(self._image_policies.results.response_current) - self.results.result_current = copy.deepcopy(self._image_policies.results.result_current) + self.results.response_current = copy.deepcopy( + self.image_policies.results.response_current + ) + self.results.result_current = copy.deepcopy( + self.image_policies.results.result_current + ) self.results.register_task_result() return self.results.failed = False registered_a_result = False for policy_name in self.policy_names: - if policy_name not in self._image_policies.all_policies: + if policy_name not in self.image_policies.all_policies: continue - self.results.diff_current = copy.deepcopy(self._image_policies.all_policies[policy_name]) - self.results.response_current = copy.deepcopy(self._image_policies.results.response_current) - self.results.result_current = copy.deepcopy(self._image_policies.results.result_current) - self.results.register_task_result() registered_a_result = True + self.results.diff_current = copy.deepcopy( + self.image_policies.all_policies[policy_name] + ) + self.results.response_current = copy.deepcopy( + self.image_policies.results.response_current + ) + self.results.result_current = copy.deepcopy( + self.image_policies.results.result_current + ) + self.results.register_task_result() if registered_a_result is False: self.results.failed = False @@ -138,3 +191,31 @@ def commit(self): # Avoid a failed result if none of the policies were found self.results.result_current = {"success": True} self.results.register_task_result() + + @property + def image_policies(self): + """ + ### Summary + Return the image_policies instance + + ### Raises + - ``TypeError`` if image_policies is not an instance of ImagePolicies + """ + return self._image_policies + + @image_policies.setter + def image_policies(self, value): + method_name = inspect.stack()[0][3] + _class_have = None + _class_need = "ImagePolicies" + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be an instance of {_class_need}. " + msg += f"Got value {value} of type {type(value).__name__}." + try: + _class_have = value.class_name + except AttributeError as error: + msg += f" Error detail: {error}." + raise TypeError(msg) from error + if _class_have != _class_need: + raise TypeError(msg) + self._image_policies = value diff --git a/plugins/module_utils/image_policy/replace.py b/plugins/module_utils/image_policy/replace.py index 5f6469bfb..5daf2ded4 100644 --- a/plugins/module_utils/image_policy/replace.py +++ b/plugins/module_utils/image_policy/replace.py @@ -22,28 +22,30 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend -from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ MergeDicts -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.common import \ - ImagePolicyCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ - Results - -class ImagePolicyReplaceBulk(ImagePolicyCommon): +@Properties.add_rest_send +@Properties.add_results +@Properties.add_params +class ImagePolicyReplaceBulk: """ + ### Summary Handle Ansible replaced state for image policies Given a list of payloads, bulk-replace the image policies therein. The payload format is given below. + ``` agnostic bool(), optional. true or false epldImgName str(), optional. name of an EPLD image to install. nxosVersion str(), required. NX-OS version as version_type_arch @@ -53,9 +55,11 @@ class ImagePolicyReplaceBulk(ImagePolicyCommon): policyName: str(), required. Name of the image policy. policyType str(), required. PLATFORM or UMBRELLA rpmimages: str(), optional. A comma-separated list of packages to uninstall + ``` - Example (replacing two policies)): + ### Example usage (replacing two policies)): + ```python policies = [ { "agnostic": false, @@ -70,32 +74,37 @@ class ImagePolicyReplaceBulk(ImagePolicyCommon): }, { "policyDescr": "new policy description for BAR", - "policyName": "BAR, + "policyName": "BAR }, ] - bulk_replace = ImagePolicyReplaceBulk(ansible_module) - bulk_replace.payloads = policies - bulk_replace.commit() + + sender = Sender() + sender.ansible_module = ansible_module + rest_send = RestSend(ansible_module.params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + instance = ImagePolicyReplaceBulk() + instance.payloads = policies + instance.rest_send = rest_send + instance.params = rest_send.params + instance.commit() + ``` """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): self.class_name = self.__class__.__name__ self.action = "replace" self.log = logging.getLogger(f"dcnm.{self.class_name}") msg = "ENTERED ImagePolicyReplaceBulk(): " msg += f"action: {self.action}, " - msg += f"check_mode: {self.check_mode}" - msg += f"state: {self.state}" self.log.debug(msg) self.endpoints = ApiEndpoints() - self._image_policies = ImagePolicies(self.ansible_module) + self._image_policies = ImagePolicies() self._image_policies.results = Results() - self.rest_send = RestSend(self.ansible_module) - self._payloads_to_commit = [] self.path = self.endpoints.policy_edit.get("path") @@ -106,18 +115,19 @@ def __init__(self, ansible_module): self._mandatory_payload_keys.add("policyName") self._mandatory_payload_keys.add("policyType") - self._build_properties() - - def _build_properties(self): - """ - self.properties holds property values for the class - """ - # self.properties is already set in the parent class - self.properties["payloads"] = None + self._params = None + self._payloads = None + self._rest_send = None + self._results = None def _verify_payload(self, payload): """ - Verify that the payload is a dict and contains all mandatory keys + ### Summary + Verify that the payload is a dict and contains all mandatory keys. + + ### Raises + - ``TypeError`` if payload is not a dict. + - ``ValueError`` if payload is missing mandatory keys. """ method_name = inspect.stack()[0][3] if not isinstance(payload, dict): @@ -125,7 +135,7 @@ def _verify_payload(self, payload): msg += "payload must be a dict. " msg += f"Got type {type(payload).__name__}, " msg += f"value {payload}" - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise TypeError(msg) missing_keys = [] for key in self._mandatory_payload_keys: @@ -137,21 +147,93 @@ def _verify_payload(self, payload): msg = f"{self.class_name}.{method_name}: " msg += "payload is missing mandatory keys: " msg += f"{sorted(missing_keys)}" - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) + + def _verify_image_policy_ref_count(self, instance, policy_names): + """ + ### Summary + Verify that all image policies in policy_names have a ref_count of 0 + (i.e. no devices are using the policy). + + ### Raises + - ``ValueError`` if any policy in policy_names has a ref_count + greater than 0. + + ### Parameters + - ``instance`` : ImagePolicies() instance + - ``policy_names`` : list of policy names + """ + method_name = inspect.stack()[0][3] + _non_zero_ref_counts = {} + for policy_name in policy_names: + instance.policy_name = policy_name + msg = f"instance.policy_name: {instance.policy_name}, " + msg += f"instance.ref_count: {instance.ref_count}." + self.log.debug(msg) + # If the policy does not exist on the controller, the ref_count + # will be None. We skip these too. + if instance.ref_count in [0, None]: + continue + _non_zero_ref_counts[policy_name] = instance.ref_count + if len(_non_zero_ref_counts) == 0: + return + msg = f"{self.class_name}.{method_name}: " + msg += "One or more policies have devices attached. " + msg += "Detach these policies from all devices first using " + msg += "the dcnm_image_upgrade module, with state == deleted. " + for policy_name, ref_count in _non_zero_ref_counts.items(): + msg += f"policy_name: {policy_name}, " + msg += f"ref_count: {ref_count}. " + raise ValueError(msg) + + def _default_policy(self, policy_name): + """ + ### Summary + Return a default policy payload for policy name. + + ### Raises + - ``TypeError`` if policy_name is not a string. + """ + method_name = inspect.stack()[0][3] + if not isinstance(policy_name, str): + msg = f"{self.class_name}.{method_name}: " + msg += "policy_name must be a string. " + msg += f"Got type {type(policy_name).__name__} for " + msg += f"value {policy_name}." + self.log.debug(msg) + raise TypeError(msg) + + policy = { + "agnostic": False, + "epldImgName": "", + "nxosVersion": "", + "packageName": "", + "platform": "", + "policyDescr": "", + "policyName": policy_name, + "policyType": "PLATFORM", + "rpmimages": "", + } + return policy def _build_payloads_to_commit(self): """ + ### Summary Build the payloads to commit to the controller. Populates the list self._payloads_to_commit - Caller: commit() + ### Raises + - ``ValueError`` if: + - ``payloads`` is not set prior to calling commit. + - ref_count for any policy is not 0. """ method_name = inspect.stack()[0][3] if self.payloads is None: msg = f"{self.class_name}.{method_name}: " msg += "payloads must be set prior to calling commit." - self.ansible_module.fail_json(msg) + raise ValueError(msg) + self._image_policies.rest_send = self.rest_send # pylint: disable=no-member self._image_policies.refresh() msg = f"self.payloads: {json.dumps(self.payloads, indent=4, sort_keys=True)}" @@ -168,16 +250,22 @@ def _build_payloads_to_commit(self): msg = f"controller_policies: {json.dumps(controller_policies, indent=4, sort_keys=True)}" self.log.debug(msg) - # fail_json if the ref_count for any policy is not 0 (i.e. the policy is in use - # and cannot be replaced) - self._verify_image_policy_ref_count(self._image_policies, policy_names) + # raise ValueError if the ref_count for any policy is not 0 (i.e. the policy is + # in use and cannot be replaced) + try: + self._verify_image_policy_ref_count(self._image_policies, policy_names) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error while verifying image policy ref counts. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error # If we made it this far, the ref_counts for all policies are 0 # Merge the default image policy with the user's payload to create a # complete playload and add it to self._payloads_to_commit self._payloads_to_commit = [] for payload in controller_policies: - merge = MergeDicts(self.ansible_module) + merge = MergeDicts() merge.dict1 = copy.deepcopy(self._default_policy(payload["policyName"])) merge.dict2 = payload msg = f"merge.dict1: {json.dumps(merge.dict1, indent=4, sort_keys=True)}" @@ -192,20 +280,41 @@ def _build_payloads_to_commit(self): def _send_payloads(self): """ + ### Summary Send the payloads in self._payloads_to_commit to the controller - Caller: commit() + ### Raises + - ``ValueError`` if any payload is not sent successfully. """ - self.rest_send.check_mode = self.check_mode + method_name = inspect.stack()[0][3] + self.rest_send.check_mode = self.params.get( # pylint: disable=no-member + "check_mode" + ) for payload in self._payloads_to_commit: - self._send_payload(payload) - + try: + self._send_payload(payload) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error while sending payloads. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error + + # pylint: disable=no-member def _send_payload(self, payload): """ + ### Summary Send one payload to the controller + + ### Raises + - ``ValueError`` if the payload is not sent successfully. + + ### Notes + - pylint: disable=no-member is needed because the rest_send, results, + and params properties are dynamically created by the + @Properties class decorators. """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] msg = f"{self.class_name}.{method_name}: " msg += f"verb: {self.verb}, path: {self.path}, " msg += f"payload: {json.dumps(payload, indent=4, sort_keys=True)}" @@ -214,12 +323,26 @@ def _send_payload(self, payload): # We don't want RestSend to retry on errors since the likelihood of a # timeout error when updating image policies is low, and there are # many cases of permanent errors for which we don't want to retry. - self.rest_send.timeout = 1 - - self.rest_send.path = self.path - self.rest_send.verb = self.verb - self.rest_send.payload = payload - self.rest_send.commit() + try: + self.rest_send.timeout = 1 + self.rest_send.path = self.path + self.rest_send.verb = self.verb + self.rest_send.payload = payload + self.rest_send.commit() + except (TypeError, ValueError) as error: + self.results.diff_current = {} + self.results.action = self.action + self.results.check_mode = self.params.get("check_mode") + self.results.state = self.params.get("state") + self.results.response_current = copy.deepcopy( + self.rest_send.response_current + ) + self.results.result_current = copy.deepcopy(self.rest_send.result_current) + self.results.register_task_result() + msg = f"{self.class_name}.{method_name}: " + msg += "Error while sending payload. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error if self.rest_send.result_current["success"] is False: self.results.diff_current = {} @@ -228,8 +351,8 @@ def _send_payload(self, payload): # self.send_payload_result[payload["FABRIC_NAME"]] = self.rest_send.result_current["success"] self.results.action = self.action - self.results.check_mode = self.check_mode - self.results.state = self.state + self.results.check_mode = self.params.get("check_mode") + self.results.state = self.params.get("state") self.results.response_current = copy.deepcopy(self.rest_send.response_current) self.results.result_current = copy.deepcopy(self.rest_send.result_current) self.results.register_task_result() @@ -246,7 +369,7 @@ def payloads(self): """ return the policy payloads """ - return self.properties["payloads"] + return self._payloads @payloads.setter def payloads(self, value): @@ -256,7 +379,7 @@ def payloads(self, value): msg += "payloads must be a list of dict. " msg += f"got {type(value).__name__} for " msg += f"value {value}" - self.ansible_module.fail_json(msg) + raise TypeError(msg) for item in value: self._verify_payload(item) - self.properties["payloads"] = value + self._payloads = value diff --git a/plugins/module_utils/image_policy/update.py b/plugins/module_utils/image_policy/update.py index 022939810..ae5a1afaa 100644 --- a/plugins/module_utils/image_policy/update.py +++ b/plugins/module_utils/image_policy/update.py @@ -22,39 +22,38 @@ import json import logging -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ - RestSend -from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ MergeDicts -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.common import \ - ImagePolicyCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ - Results -class ImagePolicyUpdateCommon(ImagePolicyCommon): +@Properties.add_rest_send +@Properties.add_results +@Properties.add_params +class ImagePolicyUpdateCommon: """ Common methods and properties for: - ImagePolicyUpdate - ImagePolicyUpdateBulk """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): self.class_name = self.__class__.__name__ self.action = "update" self.log = logging.getLogger(f"dcnm.{self.class_name}") - self._image_policies = ImagePolicies(self.ansible_module) + self._image_policies = ImagePolicies() self._image_policies.results = Results() self.endpoints = ApiEndpoints() - self.rest_send = RestSend(self.ansible_module) self.path = self.endpoints.policy_edit.get("path") self.verb = self.endpoints.policy_edit.get("verb") @@ -66,10 +65,14 @@ def __init__(self, ansible_module): self._mandatory_payload_keys.add("policyName") self._mandatory_payload_keys.add("policyType") + self._params = None + self._payload = None + self._payloads = None + self._rest_send = None + self._results = None + msg = "ENTERED ImagePolicyUpdateCommon(): " msg += f"action: {self.action}, " - msg += f"check_mode: {self.check_mode}, " - msg += f"state: {self.state}" self.log.debug(msg) def _verify_payload(self, payload): @@ -82,7 +85,7 @@ def _verify_payload(self, payload): msg += "payload must be a dict. " msg += f"Got type {type(payload).__name__}, " msg += f"value {payload}" - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise TypeError(msg) missing_keys = [] for key in self._mandatory_payload_keys: @@ -94,7 +97,7 @@ def _verify_payload(self, payload): msg = f"{self.class_name}.{method_name}: " msg += "payload is missing mandatory keys: " msg += f"{sorted(missing_keys)}" - self.ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) def _build_payloads_to_commit(self): """ @@ -107,6 +110,8 @@ def _build_payloads_to_commit(self): Populates self._payloads_to_commit with a list of payloads to commit. """ + method_name = inspect.stack()[0][3] + self._image_policies.rest_send = self.rest_send # pylint: disable=no-member self._image_policies.refresh() _payloads = [] @@ -124,11 +129,19 @@ def _build_payloads_to_commit(self): # in _payloads take precedence. self._payloads_to_commit = [] for payload in _payloads: - merge = MergeDicts(self.ansible_module) - merge.dict1 = self._image_policies.all_policies.get(payload["policyName"]) - merge.dict2 = payload - merge.commit() - updated_payload = copy.deepcopy(merge.dict_merged) + try: + merge = MergeDicts() + merge.dict1 = self._image_policies.all_policies.get( + payload["policyName"] + ) + merge.dict2 = payload + merge.commit() + updated_payload = copy.deepcopy(merge.dict_merged) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}._build_payloads_to_commit: " + msg += "Error merging payload and policy. " + msg += f"Error detail: {error}." + raise ValueError(msg) from error # ref_count, imageName, and platformPolicies are returned # by the controller, but are not valid parameters for the # edit-policy endpoint. @@ -136,9 +149,48 @@ def _build_payloads_to_commit(self): updated_payload.pop("imageName", None) updated_payload.pop("platformPolicies", None) self._payloads_to_commit.append(copy.deepcopy(updated_payload)) - msg = f"self._payloads_to_commit: {json.dumps(self._payloads_to_commit, indent=4, sort_keys=True)}" + msg = f"{self.class_name}.{method_name}: " + msg += "self._payloads_to_commit: " + msg += f"{json.dumps(self._payloads_to_commit, indent=4, sort_keys=True)}" self.log.debug(msg) + def _verify_image_policy_ref_count(self, instance, policy_names): + """ + ### Summary + Verify that all image policies in policy_names have a ref_count of 0 + (i.e. no devices are using the policy). + + ### Raises + - ``ValueError`` if any policy in policy_names has a ref_count + greater than 0. + + ### Parameters + - ``instance`` : ImagePolicies() instance + - ``policy_names`` : list of policy names + """ + method_name = inspect.stack()[0][3] + _non_zero_ref_counts = {} + for policy_name in policy_names: + instance.policy_name = policy_name + msg = f"instance.policy_name: {instance.policy_name}, " + msg += f"instance.ref_count: {instance.ref_count}." + self.log.debug(msg) + # If the policy does not exist on the controller, the ref_count + # will be None. We skip these too. + if instance.ref_count in [0, None]: + continue + _non_zero_ref_counts[policy_name] = instance.ref_count + if len(_non_zero_ref_counts) == 0: + return + msg = f"{self.class_name}.{method_name}: " + msg += "One or more policies have devices attached. " + msg += "Detach these policies from all devices first using " + msg += "the dcnm_image_upgrade module, with state == deleted. " + for policy_name, ref_count in _non_zero_ref_counts.items(): + msg += f"policy_name: {policy_name}, " + msg += f"ref_count: {ref_count}. " + raise ValueError(msg) + def _send_payloads(self): """ If check_mode is False, send the payloads to the controller @@ -146,14 +198,25 @@ def _send_payloads(self): In both cases, update results """ - self.rest_send.check_mode = self.check_mode + self.rest_send.check_mode = self.params.get( # pylint: disable=no-member + "check_mode" + ) for payload in self._payloads_to_commit: self._send_payload(payload) + # pylint: disable=no-member def _send_payload(self, payload): """ + ### Summary Send one image policy update payload + + ### Raises + + ### Notes + - pylint: disable=no-member is needed because the rest_send, results, + and params properties are dynamically created by the + @Properties class decorators. """ method_name = inspect.stack()[0][3] @@ -165,12 +228,13 @@ def _send_payload(self, payload): # We don't want RestSend to retry on errors since the likelihood of a # timeout error when updating an image policy is low, and there are # cases of permanent errors for which we don't want to retry. + self.rest_send.save_settings() self.rest_send.timeout = 1 - self.rest_send.path = self.path self.rest_send.verb = self.verb self.rest_send.payload = payload self.rest_send.commit() + self.rest_send.restore_settings() if self.rest_send.result_current["success"] is False: self.results.diff_current = {} @@ -178,8 +242,8 @@ def _send_payload(self, payload): self.results.diff_current = copy.deepcopy(payload) self.results.action = self.action - self.results.check_mode = self.check_mode - self.results.state = self.state + self.results.check_mode = self.params.get("check_mode") + self.results.state = self.params.get("state") self.results.response_current = copy.deepcopy(self.rest_send.response_current) self.results.result_current = copy.deepcopy(self.rest_send.result_current) self.results.register_task_result() @@ -192,7 +256,7 @@ def payloads(self): Payloads must be a list of dict. Each dict is a payload for the image policy update API endpoint. """ - return self.properties["payloads"] + return self._payloads @payloads.setter def payloads(self, value): @@ -202,17 +266,19 @@ def payloads(self, value): msg += "payloads must be a list of dict. " msg += f"got {type(value).__name__} for " msg += f"value {value}" - self.ansible_module.fail_json(msg) + raise TypeError(msg) for item in value: self._verify_payload(item) - self.properties["payloads"] = value + self._payloads = value class ImagePolicyUpdateBulk(ImagePolicyUpdateCommon): """ + ### Summary Given a list of payloads, bulk-update the image policies therein. The payload format is given below. + ``` agnostic bool(), optional. true or false epldImgName str(), optional. name of an EPLD image to install. nxosVersion str(), required. NX-OS version as version_type_arch @@ -222,9 +288,11 @@ class ImagePolicyUpdateBulk(ImagePolicyUpdateCommon): policyName: str(), required. Name of the image policy. policyType str(), required. PLATFORM or UMBRELLA rpmimages: str(), optional. A comma-separated list of packages to uninstall + ``` - Example (updating two policies)): + ### Usage example (updating two policies) + ```python policies = [ { "agnostic": false, @@ -242,13 +310,23 @@ class ImagePolicyUpdateBulk(ImagePolicyUpdateCommon): "policyName": "BAR, }, ] - bulk_update = ImagePolicyUpdateBulk(ansible_module) + sender = Sender() + sender.ansible_module = ansible_module + rest_send = RestSend(ansible_module.params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + bulk_update = ImagePolicyUpdateBulk() bulk_update.payloads = policies + bulk_update.results = Results() + bulk_update.rest_send = rest_send + bulk_update.params = rest_send.params bulk_update.commit() + ``` """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): + super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -256,25 +334,37 @@ def __init__(self, ansible_module): msg = "ENTERED ImagePolicyUpdateBulk(): " self.log.debug(msg) - self._build_properties() - - def _build_properties(self): - """ - self.properties holds property values for the class - """ - # self.properties is already set in the parent class - self.properties["payloads"] = None - + # pylint: disable=no-member def commit(self): """ + ### Summary Update policies. Skip any policies that do not exist on the controller. + + ### Raises + - ``ValueError`` if: + - payloads is None + - results is None + - rest_send is None + + ### Notes + - pylint: disable=no-member is needed becase the rest_send, results, + and params properties are dynamically created by the + @Properties class decorators. """ method_name = inspect.stack()[0][3] if self.payloads is None: msg = f"{self.class_name}.{method_name}: " - msg += "payloads must be set prior to calling commit." - self.ansible_module.fail_json(msg, **self.results.failed_result) + msg += f"payloads must be set prior to calling {method_name}." + raise ValueError(msg) + if self.results is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"results must be set prior to calling {method_name}." + raise ValueError(msg) + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"rest_send must be set prior to calling {method_name}." + raise ValueError(msg) self._build_payloads_to_commit() if len(self._payloads_to_commit) == 0: @@ -316,8 +406,8 @@ class ImagePolicyUpdate(ImagePolicyUpdateCommon): update.commit() """ - def __init__(self, ansible_module): - super().__init__(ansible_module) + def __init__(self): + super().__init__() self.class_name = self.__class__.__name__ self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -328,31 +418,20 @@ def __init__(self, ansible_module): self._mandatory_keys = set() self._mandatory_keys.add("policyName") - self.rest_send = RestSend(self.ansible_module) - - self._init_properties() - - def _init_properties(self): - """ - Add properties specific to this class - """ - # properties is already initialized in the parent class - self.properties["payload"] = None - @property def payload(self): """ This class expects a properly-defined image policy payload. See class docstring for the payload structure and example usage. """ - return self.properties["payload"] + return self._payload @payload.setter def payload(self, value): self._verify_payload(value) - self.properties["payload"] = value + self._payload = value # ImagePolicyUpdateCommon expects a list of payloads - self.properties["payloads"] = [value] + self._payloads = [value] def commit(self): """ @@ -362,8 +441,16 @@ def commit(self): method_name = inspect.stack()[0][3] if self.payload is None: msg = f"{self.class_name}.{method_name}: " - msg += "payload must be set prior to calling commit." - self.ansible_module.fail_json(msg, **self.results.failed_result) + msg += f"payload must be set prior to calling {method_name}." + raise ValueError(msg) + if self.results is None: # pylint: disable=no-member + msg = f"{self.class_name}.{method_name}: " + msg += f"results must be set prior to calling {method_name}." + raise ValueError(msg) + if self.rest_send is None: # pylint: disable=no-member + msg = f"{self.class_name}.{method_name}: " + msg += f"rest_send must be set prior to calling {method_name}." + raise ValueError(msg) self._build_payloads_to_commit() diff --git a/plugins/modules/dcnm_image_policy.py b/plugins/modules/dcnm_image_policy.py index 7b7dc4ab0..8ed8e3163 100644 --- a/plugins/modules/dcnm_image_policy.py +++ b/plugins/modules/dcnm_image_policy.py @@ -261,18 +261,28 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import \ Log -from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ +# from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ +# MergeDicts +# from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults import \ +# ParamsMergeDefaults +# from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ +# ParamsValidate +from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ MergeDicts -from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults_v2 import \ ParamsMergeDefaults -from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate_v2 import \ ParamsValidate -from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.common import \ - ImagePolicyCommon +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import \ + Sender from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.create import \ ImagePolicyCreateBulk from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.delete import \ @@ -281,7 +291,7 @@ ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.params_spec import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.params_spec_v2 import \ ParamsSpec from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.payload import \ Config2Payload @@ -300,46 +310,60 @@ def json_pretty(msg): return json.dumps(msg, indent=4, sort_keys=True) -class Common(ImagePolicyCommon): +@Properties.add_rest_send +class Common: """ Common methods for all states """ - def __init__(self, ansible_module): + def __init__(self, params): self.class_name = self.__class__.__name__ - super().__init__(ansible_module) - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - self.state = self.ansible_module.params.get("state") - if self.ansible_module.params.get("check_mode") is True: - self.check_mode = True + method_name = inspect.stack()[0][3] + self.params = params + self.endpoints = ApiEndpoints() self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = "ENTERED Common(): " - msg += f"state: {self.state}, " - msg += f"check_mode: {self.check_mode}" - self.log.debug(msg) - - self.endpoints = ApiEndpoints() + self.check_mode = self.params.get("check_mode", None) + if self.check_mode is None: + msg = f"{self.class_name}.{method_name}: " + msg += "check_mode is required." + raise ValueError(msg) - self._implemented_states = set() self._valid_states = ["deleted", "merged", "overridden", "query", "replaced"] self._states_require_config = {"merged", "overridden", "replaced", "query"} - self.params = ansible_module.params - self.rest_send = RestSend(self.ansible_module) + self.state = self.params.get("state", None) + if self.state is None: + msg = f"{self.class_name}.{method_name}: " + msg += "params is missing state parameter." + raise ValueError(msg) + if self.state not in self._valid_states: + msg = f"{self.class_name}.{method_name}: " + msg += f"Invalid state: {self.state}. " + msg += f"Expected one of: {','.join(self._valid_states)}." + raise ValueError(msg) + + self.config = self.params.get("config", None) + if self.state in self._states_require_config: + if self.config is None: + msg = f"{self.class_name}.{method_name}: " + msg += "params is missing config parameter." + raise ValueError(msg) + if not isinstance(self.config, list): + msg = f"{self.class_name}.{method_name}: " + msg += "Expected list of dict for self.config. " + msg += f"Got {type(self.config).__name__}" + raise TypeError(msg) - self.config = ansible_module.params.get("config") + self.results = Results() + self.results.state = self.state + self.results.check_mode = self.check_mode - if self.state in self._states_require_config and not self.config: - msg = f"'config' parameter is required for state {self.state}" - self.ansible_module.fail_json(msg, **self.rest_send.failed_result) + self._rest_send = None self.validated = [] - self.have = {} self.want = [] - self.query = [] - self.idempotent_want = None # policies to created self.need_create = [] @@ -351,13 +375,10 @@ def __init__(self, ansible_module): self.need_query = [] self.validated_configs = [] - self.build_properties() - - def build_properties(self): - """ - self.properties holds property values for the class - """ - self.properties["results"] = None + msg = "ENTERED Common(): " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) def get_have(self) -> None: """ @@ -365,9 +386,11 @@ def get_have(self) -> None: self.have consists of the current image policies on the controller """ - self.log.debug("ENTERED") - self.have = ImagePolicies(self.ansible_module) + msg = f"ENTERED {self.class_name}.get_have()" + self.log.debug(msg) + self.have = ImagePolicies() self.have.results = self.results + self.have.rest_send = self.rest_send self.have.refresh() def get_want(self) -> None: @@ -381,13 +404,14 @@ def get_want(self) -> None: msg = "ENTERED" self.log.debug(msg) # Generate the params_spec used to validate the configs - params_spec = ParamsSpec(self.ansible_module) + params_spec = ParamsSpec() + params_spec.params = self.params params_spec.commit() # If a parameter is missing from the config, and it has a default # value, add it to the config. merged_configs = [] - merge_defaults = ParamsMergeDefaults(self.ansible_module) + merge_defaults = ParamsMergeDefaults() merge_defaults.params_spec = params_spec.params_spec for config in self.config: merge_defaults.parameters = config @@ -396,7 +420,7 @@ def get_want(self) -> None: # validate the merged configs self.validated_configs = [] - validator = ParamsValidate(self.ansible_module) + validator = ParamsValidate() validator.params_spec = params_spec.params_spec for config in merged_configs: validator.parameters = config @@ -406,43 +430,37 @@ def get_want(self) -> None: # convert the validated configs to payloads to more easily compare them # to self.have (the current image policies on the controller). for config in self.validated_configs: - payload = Config2Payload(self.ansible_module) + payload = Config2Payload() payload.config = config + payload.params = self.params payload.commit() self.want.append(payload.payload) - # Exit if there's nothing to do - if len(self.want) == 0: - self.ansible_module.exit_json(**self.results.ok_result) - - @property - def results(self): - return self.properties["results"] - - @results.setter - def results(self, value): - self.properties["results"] = value - class Replaced(Common): """ Handle replaced state """ - def __init__(self, ansible_module): + def __init__(self, params): self.class_name = self.__class__.__name__ - super().__init__(ansible_module) + try: + super().__init__(params) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error during super().__init__(). " + msg += f"Error detail: {error}" + raise ValueError(msg) from error method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.replace = ImagePolicyReplaceBulk() msg = "ENTERED Replaced(): " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) - self._implemented_states.add("replaced") - def commit(self) -> None: """ Replace all policies on the controller that are in want @@ -453,10 +471,11 @@ def commit(self) -> None: self.get_want() self.get_have() - image_policy_replace = ImagePolicyReplaceBulk(self.ansible_module) - image_policy_replace.results = self.results - image_policy_replace.payloads = self.want - image_policy_replace.commit() + self.replace.results = self.results + self.replace.payloads = self.want + self.replace.rest_send = self.rest_send + self.replace.params = self.params + self.replace.commit() class Deleted(Common): @@ -464,22 +483,26 @@ class Deleted(Common): Handle deleted state """ - def __init__(self, ansible_module): + def __init__(self, params): self.class_name = self.__class__.__name__ - super().__init__(ansible_module) + try: + super().__init__(params) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error during super().__init__(). " + msg += f"Error detail: {error}" + raise ValueError(msg) from error method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.image_policy_delete = ImagePolicyDelete(self.ansible_module) + self.delete = ImagePolicyDelete() msg = "ENTERED Deleted(): " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) - self._implemented_states.add("deleted") - def commit(self) -> None: """ If config is present, delete all policies in self.want that exist on the controller @@ -487,9 +510,11 @@ def commit(self) -> None: """ self.results.state = self.state self.results.check_mode = self.check_mode - self.image_policy_delete.policy_names = self.get_policies_to_delete() - self.image_policy_delete.results = self.results - self.image_policy_delete.commit() + self.delete.policy_names = self.get_policies_to_delete() + self.delete.results = self.results + self.delete.rest_send = self.rest_send + self.delete.params = self.params + self.delete.commit() def get_policies_to_delete(self) -> List[str]: """ @@ -517,63 +542,92 @@ class Query(Common): Handle query state """ - def __init__(self, ansible_module): + def __init__(self, params): self.class_name = self.__class__.__name__ - super().__init__(ansible_module) - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] + + try: + super().__init__(params) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error during super().__init__(). " + msg += f"Error detail: {error}" + raise ValueError(msg) from error self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.query = ImagePolicyQuery() + self.image_policies = ImagePolicies() - msg = "ENTERED Query(): " + msg = f"ENTERED {self.class_name}.{method_name}: " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) - self._implemented_states.add("query") - def commit(self) -> None: """ 1. query the fabrics in self.want that exist on the controller """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable - + method_name = inspect.stack()[0][3] self.results.state = self.state self.results.check_mode = self.check_mode self.get_want() + # self.get_have() + + if len(self.want) == 0: + msg = f"{self.class_name}.{method_name}: " + msg += "Nothing to query." + return - image_policy_query = ImagePolicyQuery(self.ansible_module) - image_policy_query.results = self.results + self.image_policies.results = Results() + self.image_policies.rest_send = self.rest_send + + self.query.params = self.params + self.query.results = self.results + self.query.rest_send = self.rest_send + self.query.image_policies = self.image_policies policy_names_to_query = [] for want in self.want: policy_names_to_query.append(want["policyName"]) - image_policy_query.policy_names = policy_names_to_query - image_policy_query.commit() + self.query.policy_names = policy_names_to_query + self.query.commit() class Overridden(Common): """ + ### Summary Handle overridden state + + ### Raises + - ``ValueError`` if: + - ``Common().__init__()`` raises ``TypeError`` or ``ValueError``. """ - def __init__(self, ansible_module): + def __init__(self, params): self.class_name = self.__class__.__name__ - super().__init__(ansible_module) + try: + super().__init__(params) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error during super().__init__(). " + msg += f"Error detail: {error}" + raise ValueError(msg) from error method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.delete = ImagePolicyDelete() + msg = "ENTERED Overridden(): " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) - self._implemented_states.add("overridden") - def commit(self) -> None: """ - 1. Delete all policies on the controller that are not in self.want - 2. Instantiate Merged() and call Merged().commit() + ### Summary + - Delete all policies on the controller that are not in self.want + - Instantiate`` Merged()`` and call ``Merged().commit()`` """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable @@ -592,15 +646,15 @@ def commit(self) -> None: self.log.debug(msg) self._delete_policies_not_in_want() - task = Merged(self.ansible_module) + task = Merged(self.params) + task.rest_send = self.rest_send task.results = self.results task.commit() def _delete_policies_not_in_want(self) -> None: """ + ### Summary Delete all policies on the controller that are not in self.want - - Caller: handle_overridden_state() """ method_name = inspect.stack()[0][3] # pylint: disable=unused-variable want_policy_names = set() @@ -622,32 +676,47 @@ def _delete_policies_not_in_want(self) -> None: msg += f"policy_names_to_delete: {policy_names_to_delete}" self.log.debug(msg) - instance = ImagePolicyDelete(self.ansible_module) - instance.results = self.results - instance.policy_names = policy_names_to_delete - instance.commit() + self.results.state = self.state + self.results.check_mode = self.check_mode + self.delete.policy_names = policy_names_to_delete + self.delete.results = self.results + self.delete.rest_send = self.rest_send + self.delete.params = self.params + self.delete.commit() class Merged(Common): """ + ### Summary Handle merged state + + ### Raises + - ``ValueError`` if: + - ``params`` is missing ``config`` key. + - ``commit()`` is issued before setting mandatory properties """ - def __init__(self, ansible_module): + def __init__(self, params): self.class_name = self.__class__.__name__ - super().__init__(ansible_module) + try: + super().__init__(params) + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error during super().__init__(). " + msg += f"Error detail: {error}" + raise ValueError(msg) from error method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = f"params: {json_pretty(self.ansible_module.params)}" + msg = f"params: {json_pretty(self.params)}" self.log.debug(msg) - if not ansible_module.params.get("config"): + if not params.get("config"): msg = f"playbook config is required for {self.state}" - ansible_module.fail_json(msg, **self.results.failed_result) + raise ValueError(msg) - self.image_policy_create = ImagePolicyCreateBulk(self.ansible_module) - self.image_policy_update = ImagePolicyUpdateBulk(self.ansible_module) + self.create = ImagePolicyCreateBulk() + self.update = ImagePolicyUpdateBulk() msg = f"ENTERED {self.class_name}.{method_name}: " msg += f"state: {self.state}, " @@ -655,28 +724,27 @@ def __init__(self, ansible_module): self.log.debug(msg) # new policies to be created - self.need_create: List[Dict] = [] + self.need_create: list = [] # existing policies to be updated - self.need_update: List[Dict] = [] - - self._implemented_states.add("merged") + self.need_update: list = [] def get_need(self): """ - Caller: commit() - + ### Summary Build self.need for merged state - 1. Populate self.need_create with items from self.want that are + + ### Description + - Populate self.need_create with items from self.want that are not in self.have - 2. Populate self.need_update with updated policies. We update - policies as follows: - a. If a policy is in both self.want amd self.have, and they - contain differences, merge self.want into self.have, - with self.want keys taking precedence and append the - merged policy to self.need_update. - b. If a policy is in both self.want and self.have, and they - are identical, do not append the policy to self.need_update - (i.e. do nothing). + - Populate self.need_update with updated policies. Policies are + updated as follows: + - If a policy is in both self.want amd self.have, and they + contain differences, merge self.want into self.have, + with self.want keys taking precedence and append the + merged policy to self.need_update. + - If a policy is in both self.want and self.have, and they + are identical, do not append the policy to self.need_update + (i.e. do nothing). """ for want in self.want: self.have.policy_name = want.get("policyName") @@ -710,14 +778,13 @@ def commit(self) -> None: def _prepare_for_merge(self, have: Dict, want: Dict): """ - 1. Remove fields in "have" that are not part of a request payload i.e. + ### Summary + - Remove fields in "have" that are not part of a request payload i.e. imageName and ref_count. - 2. The controller returns "N9K/N3K" for the platform, but it expects + - The controller returns "N9K/N3K" for the platform, but it expects "N9K" in the payload. We change "N9K/N3K" to "N9K" in have so that the compare works. - 3. Remove all fields that are not set in both "have" and "want" - - Caller: self._merge_policies() + - Remove all fields that are not set in both "have" and "want" """ # Remove keys that the controller adds which are not part # of a request payload. @@ -739,20 +806,26 @@ def _prepare_for_merge(self, have: Dict, want: Dict): want.pop(key, None) return (have, want) - def _merge_policies(self, have: Dict, want: Dict) -> Dict: + def _merge_policies(self, have: dict, want: dict) -> dict: """ + ### Summary Merge the parameters in want with the parameters in have. - - Caller: self.commit() """ + method_name = inspect.stack()[0][3] (have, want) = self._prepare_for_merge(have, want) # Merge the parameters in want with the parameters in have. # The parameters in want take precedence. - merge = MergeDicts(self.ansible_module) - merge.dict1 = have - merge.dict2 = want - merge.commit() + try: + merge = MergeDicts() + merge.dict1 = have + merge.dict2 = want + merge.commit() + except (TypeError, ValueError) as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"Error during MergeDicts(). " + msg += f"Error detail: {error}" + raise ValueError(msg) from error merged = copy.deepcopy(merge.dict_merged) needs_update = False @@ -764,25 +837,27 @@ def _merge_policies(self, have: Dict, want: Dict) -> Dict: def send_need_create(self) -> None: """ + ### Summary Create the policies in self.need_create - Callers: - - self.handle_merged_state() """ - self.image_policy_create.results = self.results - self.image_policy_create.payloads = self.need_create - self.image_policy_create.commit() + self.create.results = self.results + self.create.payloads = self.need_create + self.create.rest_send = self.rest_send + self.create.params = self.params + self.create.commit() def send_need_update(self) -> None: """ + ### Summary Update the policies in self.need_update - Callers: - - self.handle_merged_state() """ - self.image_policy_update.results = self.results - self.image_policy_update.payloads = self.need_update - self.image_policy_update.commit() + self.update.results = self.results + self.update.payloads = self.need_update + self.update.rest_send = self.rest_send + self.update.params = self.params + self.update.commit() def main(): @@ -790,7 +865,7 @@ def main(): main entry point for module execution """ - element_spec = { + argument_spec = { "config": { "required": False, "type": "list", @@ -802,7 +877,12 @@ def main(): "choices": ["deleted", "merged", "overridden", "query", "replaced"], }, } - ansible_module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) + ansible_module = AnsibleModule( + argument_spec=argument_spec, supports_check_mode=True + ) + + params = copy.deepcopy(ansible_module.params) + params["check_mode"] = ansible_module.check_mode # Logging setup try: @@ -811,37 +891,38 @@ def main(): except ValueError as error: ansible_module.fail_json(str(error)) - results = Results() - if ansible_module.params["state"] == "deleted": - task = Deleted(ansible_module) - task.results = results - task.commit() - elif ansible_module.params["state"] == "merged": - task = Merged(ansible_module) - task.results = results - task.commit() - elif ansible_module.params["state"] == "overridden": - task = Overridden(ansible_module) - task.results = results - task.commit() - elif ansible_module.params["state"] == "query": - task = Query(ansible_module) - task.results = results - task.commit() - elif ansible_module.params["state"] == "replaced": - task = Replaced(ansible_module) - task.results = results + sender = Sender() + sender.ansible_module = ansible_module + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + # pylint: disable=attribute-defined-outside-init + try: + task = None + if params["state"] == "deleted": + task = Deleted(params) + if params["state"] == "merged": + task = Merged(params) + if params["state"] == "overridden": + task = Overridden(params) + if params["state"] == "query": + task = Query(params) + if params["state"] == "replaced": + task = Replaced(params) + if task is None: + ansible_module.fail_json(f"Invalid state: {params['state']}") + task.rest_send = rest_send task.commit() - else: - msg = f"Unknown state {task.ansible_module.params['state']}" - ansible_module.fail_json(msg) + except ValueError as error: + ansible_module.fail_json(f"{error}", **task.results.failed_result) - results.build_final_result() + task.results.build_final_result() - if True in results.failed: # pylint: disable=unsupported-membership-test + if True in task.results.failed: # pylint: disable=unsupported-membership-test msg = "Module failed." - ansible_module.fail_json(msg, **results.final_result) - ansible_module.exit_json(**results.final_result) + ansible_module.fail_json(msg, **task.results.final_result) + ansible_module.exit_json(**task.results.final_result) if __name__ == "__main__": diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted.yaml index 6295ef78e..8bbb97fd0 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted.yaml @@ -3,8 +3,9 @@ ################################################################################ # Recent run times (MM:SS.ms): -# 00:30.937 -# 00:24.057 +# 00:18.960 +# 00:19.240 +# 00:18.836 ################################################################################ # STEPS ################################################################################ @@ -60,7 +61,7 @@ # leaf4: 172.22.150.109 # # for dcnm_image_policy role # image_policy_1: "KR5M" -# image_policy_2: "NR3F" +# image_policy_2: "NR1F" # epld_image_1: n9000-epld.10.2.5.M.img # epld_image_2: n9000-epld.10.3.1.F.img # nxos_image_1: n9000-dk9.10.2.5.M.bin @@ -105,8 +106,8 @@ # "epldImgName": "n9000-epld.10.3.1.F.img", # "nxosVersion": "10.3.1_nxos64-cs_64bit", # "platform": "N9K", -# "policyDescr": "NR3F", -# "policyName": "NR3F", +# "policyDescr": "NR1F", +# "policyName": "NR1F", # "policyType": "PLATFORM", # "sequence_number": 2 # } @@ -128,18 +129,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Policy created successfully.", # "MESSAGE": "OK", # "METHOD": "POST", @@ -206,20 +195,30 @@ - result.diff[0].policyDescr == image_policy_1 - result.diff[0].epldImgName == epld_image_1 - result.diff[0].nxosVersion == nxos_release_1 + - result.diff[0].sequence_number == 1 - result.diff[1].policyName == image_policy_2 - result.diff[1].policyDescr == image_policy_2 - result.diff[1].epldImgName == epld_image_2 - result.diff[1].nxosVersion == nxos_release_2 - - (result.response | length) == 3 + - result.diff[1].sequence_number == 2 + - (result.metadata | length) == 2 + - result.metadata[0].action == "create" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - result.metadata[1].action == "create" + - result.metadata[1].check_mode == False + - result.metadata[1].sequence_number == 2 + - result.metadata[1].state == "merged" + - (result.response | length) == 2 - result.response[0].MESSAGE == "OK" - - result.response[0].METHOD == "GET" + - result.response[0].METHOD == "POST" - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 - result.response[1].MESSAGE == "OK" - result.response[1].METHOD == "POST" - result.response[1].RETURN_CODE == 200 - - result.response[2].MESSAGE == "OK" - - result.response[2].METHOD == "POST" - - result.response[2].RETURN_CODE == 200 + - result.response[1].sequence_number == 2 ################################################################################ # DELETED - TEST - Delete first image policy (image_policy_1) and verify @@ -247,54 +246,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [ -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.2.5.M.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.2.5.M.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.2.5_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "KR5M", -# "policyName": "KR5M", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# }, -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.3.1.F.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.3.1.F.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.3.1_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "NR3F", -# "policyName": "NR3F", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# } -# ], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, # { # "DATA": "Selected policy(s) deleted successfully.", # "MESSAGE": "OK", @@ -306,11 +257,6 @@ # ], # "result": [ # { -# "found": true, -# "sequence_number": 0, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 1, # "success": true @@ -334,18 +280,19 @@ - result.failed == false - (result.diff | length) == 1 - image_policy_1 in result.diff[0].policyNames - - (result.response | length) == 2 + - (result.response | length) == 1 - result.response[0].MESSAGE == "OK" - - result.response[0].METHOD == "GET" + - result.response[0].METHOD == "DELETE" - result.response[0].RETURN_CODE == 200 - - result.response[1].MESSAGE == "OK" - - result.response[1].METHOD == "DELETE" - - result.response[1].RETURN_CODE == 200 - (result.metadata | length) == 1 - result.metadata[0].action == "delete" - result.metadata[0].check_mode == False - result.metadata[0].sequence_number == 1 - result.metadata[0].state == "deleted" + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true ################################################################################ # DELETED - TEST - Delete remaining policy (image_policy_2) and verify @@ -357,7 +304,7 @@ # "diff": [ # { # "policyNames": [ -# "NR3F" +# "NR1F" # ], # "sequence_number": 1 # } @@ -373,37 +320,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [ -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.3.1.F.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.3.1.F.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.3.1_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "NR3F", -# "policyName": "NR3F", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# } -# ], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Selected policy(s) deleted successfully.", # "MESSAGE": "OK", # "METHOD": "DELETE", @@ -414,11 +330,6 @@ # ], # "result": [ # { -# "found": true, -# "sequence_number": 0, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 1, # "success": true @@ -431,7 +342,7 @@ cisco.dcnm.dcnm_image_policy: state: deleted config: - - name: NR3F + - name: "{{ image_policy_2 }}" register: result - debug: @@ -443,15 +354,16 @@ - result.failed == false - (result.diff | length) == 1 - image_policy_2 in result.diff[0].policyNames - - (result.response | length) == 2 + - (result.response | length) == 1 - result.response[0].MESSAGE == "OK" - - result.response[0].METHOD == "GET" + - result.response[0].METHOD == "DELETE" - result.response[0].RETURN_CODE == 200 - - result.response[1].MESSAGE == "OK" - - result.response[1].METHOD == "DELETE" - - result.response[1].RETURN_CODE == 200 - (result.metadata | length) == 1 - result.metadata[0].action == "delete" - result.metadata[0].check_mode == False - result.metadata[0].sequence_number == 1 - result.metadata[0].state == "deleted" + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml index f4ade64ee..eeca77e1d 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml @@ -1,12 +1,14 @@ ################################################################################ # RUNTIME ################################################################################ - +# # Recent run times (MM:SS.ms): +# 00:14.039 +# 00:14.253 ################################################################################ # STEPS ################################################################################ - +# # SETUP # 1. The following images must already be uploaded to the controller # See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml @@ -24,11 +26,11 @@ # - image_policy_2 # CLEANUP # 7. Delete the image policies created in the test - +# ################################################################################ # REQUIREMENTS ################################################################################ - +# # 1. The following images must already be uploaded to the controller # See vars: section below # - nxos_image_1 @@ -56,14 +58,14 @@ # leaf4: 172.22.150.109 # # for dcnm_image_policy role # image_policy_1: "KR5M" -# image_policy_2: "NR3F" +# image_policy_2: "NR1F" # epld_image_1: n9000-epld.10.2.5.M.img # epld_image_2: n9000-epld.10.3.1.F.img # nxos_image_1: n9000-dk9.10.2.5.M.bin # nxos_image_2: n9000-dk9.10.3.1.F.bin # nxos_release_1: 10.2.5_nxos64-cs_64bit # nxos_release_2: 10.3.1_nxos64-cs_64bit - +# ################################################################################ # SETUP ################################################################################ @@ -102,8 +104,8 @@ # "epldImgName": "n9000-epld.10.3.1.F.img", # "nxosVersion": "10.3.1_nxos64-cs_64bit", # "platform": "N9K", -# "policyDescr": "NR3F", -# "policyName": "NR3F", +# "policyDescr": "NR1F", +# "policyName": "NR1F", # "policyType": "PLATFORM", # "sequence_number": 2 # } @@ -125,18 +127,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Policy created successfully.", # "MESSAGE": "OK", # "METHOD": "POST", @@ -155,11 +145,6 @@ # ], # "result": [ # { -# "found": true, -# "sequence_number": 0, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 1, # "success": true @@ -224,6 +209,23 @@ - result.metadata[1].check_mode == False - result.metadata[1].sequence_number == 2 - result.metadata[1].state == "merged" + - (result.response | length) == 2 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "POST" + - result.response[0].RETURN_CODE == 200 + - result.response[0].DATA == "Policy created successfully." + - result.response[1].MESSAGE == "OK" + - result.response[1].METHOD == "POST" + - result.response[1].RETURN_CODE == 200 + - result.response[1].DATA == "Policy created successfully." + - (result.result | length) == 2 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 + - result.result[1].changed == true + - result.result[1].success == true + - result.result[1].sequence_number == 2 + ################################################################################ # MERGED - CLEANUP - Delete image policies @@ -235,7 +237,7 @@ # "diff": [ # { # "policyNames": [ -# "NR3F", +# "NR1F", # "KR5M" # ], # "sequence_number": 1 @@ -252,55 +254,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [ -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.2.5.M.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.2.5.M.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.2.5_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "KR5M", -# "policyName": "KR5M", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# }, -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.3.1.F.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.3.1.F.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.3.1_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "NR3F", -# "policyName": "NR3F", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# } -# ], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Selected policy(s) deleted successfully.", # "MESSAGE": "OK", # "METHOD": "DELETE", @@ -342,13 +295,10 @@ - (result.diff | length) == 1 - image_policy_1 in result.diff[0].policyNames - image_policy_2 in result.diff[0].policyNames - - (result.response | length) == 2 + - (result.response | length) == 1 - result.response[0].MESSAGE == "OK" - - result.response[0].METHOD == "GET" + - result.response[0].METHOD == "DELETE" - result.response[0].RETURN_CODE == 200 - - result.response[1].MESSAGE == "OK" - - result.response[1].METHOD == "DELETE" - - result.response[1].RETURN_CODE == 200 - (result.metadata | length) == 1 - result.metadata[0].action == "delete" - result.metadata[0].check_mode == False diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml index eb3a19753..6c633853f 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml @@ -3,13 +3,14 @@ ################################################################################ # # Recent run times (MM:SS.ms): -# 00.27.549 -# 00.27.943 - +# 00:20.978 +# 00:22.217 +# 00:21.880 +# ################################################################################ # STEPS ################################################################################ - +# # SETUP # 1. The following images must already be uploaded to the controller # See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml @@ -32,11 +33,11 @@ # CLEANUP # # 6. Delete the image policies created in the test - +# ################################################################################ # REQUIREMENTS ################################################################################ - +# # 1. The following images must already be uploaded to the controller # See vars: section below # - nxos_image_1 @@ -64,14 +65,14 @@ # leaf4: 172.22.150.109 # # for dcnm_image_policy role # image_policy_1: "KR5M" -# image_policy_2: "NR3F" +# image_policy_2: "NR1F" # epld_image_1: n9000-epld.10.2.5.M.img # epld_image_2: n9000-epld.10.3.1.F.img # nxos_image_1: n9000-dk9.10.2.5.M.bin # nxos_image_2: n9000-dk9.10.3.1.F.bin # nxos_release_1: 10.2.5_nxos64-cs_64bit # nxos_release_2: 10.3.1_nxos64-cs_64bit - +# ################################################################################ # SETUP ################################################################################ @@ -110,8 +111,8 @@ # "epldImgName": "n9000-epld.10.3.1.F.img", # "nxosVersion": "10.3.1_nxos64-cs_64bit", # "platform": "N9K", -# "policyDescr": "NR3F", -# "policyName": "NR3F", +# "policyDescr": "NR1F", +# "policyName": "NR1F", # "policyType": "PLATFORM", # "sequence_number": 2 # } @@ -133,18 +134,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Policy created successfully.", # "MESSAGE": "OK", # "METHOD": "POST", @@ -163,11 +152,6 @@ # ], # "result": [ # { -# "found": true, -# "sequence_number": 0, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 1, # "success": true @@ -221,16 +205,33 @@ - result.diff[1].platform == "N9K" - result.diff[0].policyType == "PLATFORM" - result.diff[1].policyType == "PLATFORM" - - (result.response | length) == 3 + - result.diff[0].sequence_number == 1 + - result.diff[1].sequence_number == 2 + - (result.metadata | length) == 2 + - result.metadata[0].action == "create" + - result.metadata[0].check_mode == false + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - result.metadata[1].action == "create" + - result.metadata[1].check_mode == false + - result.metadata[1].sequence_number == 2 + - result.metadata[1].state == "merged" + - (result.response | length) == 2 - result.response[0].MESSAGE == "OK" - - result.response[0].METHOD == "GET" + - result.response[0].METHOD == "POST" - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 - result.response[1].MESSAGE == "OK" - result.response[1].METHOD == "POST" - result.response[1].RETURN_CODE == 200 - - result.response[2].MESSAGE == "OK" - - result.response[2].METHOD == "POST" - - result.response[2].RETURN_CODE == 200 + - result.response[1].sequence_number == 2 + - (result.result | length) == 2 + - result.result[0].changed == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true + - result.result[1].changed == true + - result.result[1].sequence_number == 2 + - result.result[1].success == true ################################################################################ # OVERRIDDEN - TEST - override image_policy_1 which will delete image_policy_2 @@ -242,7 +243,7 @@ # "diff": [ # { # "policyNames": [ -# "NR3F" +# "NR1F" # ], # "sequence_number": 1 # }, @@ -280,55 +281,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [ -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.2.5.M.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.2.5.M.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.2.5_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "KR5M", -# "policyName": "KR5M", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# }, -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.3.1.F.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.3.1.F.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.3.1_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "NR3F", -# "policyName": "NR3F", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# } -# ], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Selected policy(s) deleted successfully.", # "MESSAGE": "OK", # "METHOD": "DELETE", @@ -337,37 +289,6 @@ # "sequence_number": 1 # }, # { -# "DATA": { -# "lastOperDataObject": [ -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.2.5.M.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.2.5.M.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.2.5_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "KR5M", -# "policyName": "KR5M", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# } -# ], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 1 -# }, -# { # "DATA": "Policy updated successfully.", # "MESSAGE": "OK", # "METHOD": "POST", @@ -378,21 +299,11 @@ # ], # "result": [ # { -# "found": true, -# "sequence_number": 0, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 1, # "success": true # }, # { -# "found": true, -# "sequence_number": 1, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 2, # "success": true @@ -422,6 +333,7 @@ - result.failed == false - (result.diff | length) == 2 - image_policy_2 in result.diff[0].policyNames + - result.diff[0].sequence_number == 1 - result.diff[1].agnostic == false - result.diff[1].policyName == image_policy_1 - result.diff[1].policyDescr == image_policy_1 + " overridden" @@ -429,6 +341,7 @@ - result.diff[1].nxosVersion == nxos_release_1 - result.diff[1].platform == "N9K" - result.diff[1].policyType == "PLATFORM" + - result.diff[1].sequence_number == 2 - (result.metadata | length) == 2 - result.metadata[0].action == "delete" - result.metadata[1].action == "update" @@ -438,19 +351,22 @@ - result.metadata[1].check_mode == False - result.metadata[0].sequence_number == 1 - result.metadata[1].sequence_number == 2 - - (result.response | length) == 4 + - (result.response | length) == 2 - result.response[0].MESSAGE == "OK" - - result.response[0].METHOD == "GET" + - result.response[0].METHOD == "DELETE" - result.response[0].RETURN_CODE == 200 - result.response[1].MESSAGE == "OK" - - result.response[1].METHOD == "DELETE" + - result.response[1].METHOD == "POST" - result.response[1].RETURN_CODE == 200 - - result.response[2].MESSAGE == "OK" - - result.response[2].METHOD == "GET" - - result.response[2].RETURN_CODE == 200 - - result.response[3].MESSAGE == "OK" - - result.response[3].METHOD == "POST" - - result.response[3].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - result.response[1].sequence_number == 2 + - (result.result | length) == 2 + - result.result[0].changed == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true + - result.result[1].changed == true + - result.result[1].sequence_number == 2 + - result.result[1].success == true ################################################################################ # OVERRIDDEN - CLEANUP - Delete image policies and verify @@ -478,37 +394,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [ -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.2.5.M.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.2.5.M.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.2.5_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "KR5M overridden", -# "policyName": "KR5M", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# } -# ], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Selected policy(s) deleted successfully.", # "MESSAGE": "OK", # "METHOD": "DELETE", @@ -519,11 +404,6 @@ # ], # "result": [ # { -# "found": true, -# "sequence_number": 0, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 1, # "success": true @@ -547,17 +427,20 @@ - result.changed == true - result.failed == false - (result.diff | length) == 1 + - result.diff[0].sequence_number == 1 - image_policy_1 in result.diff[0].policyNames - image_policy_2 not in result.diff[0].policyNames - - (result.response | length) == 2 - - result.response[0].MESSAGE == "OK" - - result.response[0].METHOD == "GET" - - result.response[0].RETURN_CODE == 200 - - result.response[1].MESSAGE == "OK" - - result.response[1].METHOD == "DELETE" - - result.response[1].RETURN_CODE == 200 - (result.metadata | length) == 1 - result.metadata[0].action == "delete" - - result.metadata[0].check_mode == False + - result.metadata[0].check_mode == false - result.metadata[0].sequence_number == 1 - result.metadata[0].state == "deleted" + - (result.response | length) == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "DELETE" + - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_query.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_query.yaml index b1661c6db..8cbbf55b1 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_query.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_query.yaml @@ -3,13 +3,13 @@ ################################################################################ # # Recent run times (MM:SS.ms): -# 00.26.844 -# 00.25.253 - +# 00:17.067 +# 00:16.317 +# ################################################################################ # STEPS ################################################################################ - +# # SETUP # 1. The following images must already be uploaded to the controller # See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml @@ -32,11 +32,11 @@ # CLEANUP # # 6. Delete the image policies created in the test - +# ################################################################################ # REQUIREMENTS ################################################################################ - +# # 1. The following images must already be uploaded to the controller # See vars: section below # - nxos_image_1 @@ -71,7 +71,7 @@ # nxos_image_2: n9000-dk9.10.3.1.F.bin # nxos_release_1: 10.2.5_nxos64-cs_64bit # nxos_release_2: 10.3.1_nxos64-cs_64bit - +# ################################################################################ # QUERY - SETUP - Delete image policies if they exist ################################################################################ @@ -134,18 +134,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Policy created successfully.", # "MESSAGE": "OK", # "METHOD": "POST", @@ -164,11 +152,6 @@ # ], # "result": [ # { -# "found": true, -# "sequence_number": 0, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 1, # "success": true @@ -222,7 +205,31 @@ - result.diff[1].platform == "N9K" - result.diff[0].policyType == "PLATFORM" - result.diff[1].policyType == "PLATFORM" - - (result.response | length) == 3 + - (result.metadata | length) == 2 + - result.metadata[0].action == "create" + - result.metadata[0].check_mode == false + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - result.metadata[1].action == "create" + - result.metadata[1].check_mode == false + - result.metadata[1].sequence_number == 2 + - result.metadata[1].state == "merged" + - (result.response | length) == 2 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "POST" + - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - result.response[1].MESSAGE == "OK" + - result.response[1].METHOD == "POST" + - result.response[1].RETURN_CODE == 200 + - result.response[1].sequence_number == 2 + - (result.result | length) == 2 + - result.result[0].changed == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true + - result.result[1].changed == true + - result.result[1].sequence_number == 2 + - result.result[1].success == true ################################################################################ # QUERY - TEST - query image policies and verify results @@ -431,15 +438,31 @@ - result.diff[1].policyType == "PLATFORM" - result.diff[0].ref_count == 0 - result.diff[1].ref_count == 0 + - result.diff[0].sequence_number == 1 + - result.diff[1].sequence_number == 2 + - (result.metadata | length) == 2 + - result.metadata[0].action == "query" + - result.metadata[0].check_mode == false + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "query" + - result.metadata[1].action == "query" + - result.metadata[1].check_mode == false + - result.metadata[1].sequence_number == 2 + - result.metadata[1].state == "query" + - (result.response | length) == 2 - result.response[0].MESSAGE == "OK" - result.response[0].METHOD == "GET" - result.response[0].RETURN_CODE == 200 - result.response[1].MESSAGE == "OK" - result.response[1].METHOD == "GET" - result.response[1].RETURN_CODE == 200 - - (result.response | length) == 2 - - result.metadata[0].action == "query" - - result.metadata[1].action == "query" + - (result.result | length) == 2 + - result.result[0].found == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true + - result.result[1].found == true + - result.result[1].sequence_number == 2 + - result.result[1].success == true ################################################################################ # QUERY - CLEANUP - Delete image policies and verify @@ -468,55 +491,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [ -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.2.5.M.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.2.5.M.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.2.5_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "KR5M", -# "policyName": "KR5M", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# }, -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.3.1.F.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.3.1.F.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.3.1_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "NR3F", -# "policyName": "NR3F", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# } -# ], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Selected policy(s) deleted successfully.", # "MESSAGE": "OK", # "METHOD": "DELETE", @@ -527,11 +501,6 @@ # ], # "result": [ # { -# "found": true, -# "sequence_number": 0, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 1, # "success": true @@ -539,6 +508,7 @@ # ] # } # } + - name: QUERY - CLEANUP - Delete image policies and verify cisco.dcnm.dcnm_image_policy: state: deleted @@ -557,10 +527,18 @@ - (result.diff | length) == 1 - image_policy_1 in result.diff[0].policyNames - image_policy_2 in result.diff[0].policyNames + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "delete" + - result.metadata[0].check_mode == false + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "deleted" + - (result.response | length) == 1 - result.response[0].MESSAGE == "OK" - - result.response[0].METHOD == "GET" + - result.response[0].METHOD == "DELETE" - result.response[0].RETURN_CODE == 200 - - result.response[1].MESSAGE == "OK" - - result.response[1].METHOD == "DELETE" - - result.response[1].RETURN_CODE == 200 - - (result.response | length) == 2 + - result.response[0].sequence_number == 1 + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml index 447cb67cb..a15cce612 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml @@ -3,13 +3,13 @@ ################################################################################ # # Recent run times (MM:SS.ms): -# 00.25.904 -# 00.25.215 - +# 00:17.898 +# 00:17.676 +# ################################################################################ # STEPS ################################################################################ - +# # SETUP # 1. The following images must already be uploaded to the controller # See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml @@ -34,11 +34,11 @@ # CLEANUP # # 6. Delete the image policies created in the test - +# ################################################################################ # REQUIREMENTS ################################################################################ - +# # 1. The following images must already be uploaded to the controller # See vars: section below # - nxos_image_1 @@ -66,14 +66,14 @@ # leaf4: 172.22.150.109 # # for dcnm_image_policy role # image_policy_1: "KR5M" -# image_policy_2: "NR3F" +# image_policy_2: "NR1F" # epld_image_1: n9000-epld.10.2.5.M.img # epld_image_2: n9000-epld.10.3.1.F.img # nxos_image_1: n9000-dk9.10.2.5.M.bin # nxos_image_2: n9000-dk9.10.3.1.F.bin # nxos_release_1: 10.2.5_nxos64-cs_64bit # nxos_release_2: 10.3.1_nxos64-cs_64bit - +# ################################################################################ # SETUP ################################################################################ @@ -113,8 +113,8 @@ # "epldImgName": "n9000-epld.10.3.1.F.img", # "nxosVersion": "10.3.1_nxos64-cs_64bit", # "platform": "N9K", -# "policyDescr": "NR3F", -# "policyName": "NR3F", +# "policyDescr": "NR1F", +# "policyName": "NR1F", # "policyType": "PLATFORM", # "sequence_number": 2 # } @@ -136,18 +136,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Policy created successfully.", # "MESSAGE": "OK", # "METHOD": "POST", @@ -166,11 +154,6 @@ # ], # "result": [ # { -# "found": true, -# "sequence_number": 0, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 1, # "success": true @@ -234,6 +217,22 @@ - result.metadata[1].check_mode == False - result.metadata[1].sequence_number == 2 - result.metadata[1].state == "merged" + - (result.response | length) == 2 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "POST" + - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - result.response[1].MESSAGE == "OK" + - result.response[1].METHOD == "POST" + - result.response[1].RETURN_CODE == 200 + - result.response[1].sequence_number == 2 + - (result.result | length) == 2 + - result.result[0].changed == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true + - result.result[1].changed == true + - result.result[1].sequence_number == 2 + - result.result[1].success == true ################################################################################ # REPLACED - TEST - replace image_policy_1, will leave image_policy_2 untouched @@ -267,55 +266,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [ -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.2.5.M.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.2.5.M.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.2.5_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "KR5M", -# "policyName": "KR5M", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# }, -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.3.1.F.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.3.1.F.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.3.1_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "NR3F", -# "policyName": "NR3F", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# } -# ], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Policy updated successfully.", # "MESSAGE": "OK", # "METHOD": "POST", @@ -326,11 +276,6 @@ # ], # "result": [ # { -# "found": true, -# "sequence_number": 0, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 1, # "success": true @@ -368,18 +313,21 @@ - result.diff[0].nxosVersion == nxos_release_1 - result.diff[0].platform == "N9K" - result.diff[0].policyType == "PLATFORM" - - (result.response | length) == 2 - - result.response[0].MESSAGE == "OK" - - result.response[0].METHOD == "GET" - - result.response[0].RETURN_CODE == 200 - - result.response[1].MESSAGE == "OK" - - result.response[1].METHOD == "POST" - - result.response[1].RETURN_CODE == 200 + - result.diff[0].sequence_number == 1 - (result.metadata | length) == 1 - result.metadata[0].action == "replace" - result.metadata[0].check_mode == False - result.metadata[0].sequence_number == 1 - result.metadata[0].state == "replaced" + - (result.response | length) == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "POST" + - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true ################################################################################ # REPLACED - CLEANUP - Delete image policies and verify @@ -392,7 +340,7 @@ # { # "policyNames": [ # "KR5M", -# "NR3F" +# "NR1F" # ], # "sequence_number": 1 # } @@ -408,55 +356,6 @@ # ], # "response": [ # { -# "DATA": { -# "lastOperDataObject": [ -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.2.5.M.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.2.5.M.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.2.5_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "KR5M replaced", -# "policyName": "KR5M", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": "", -# "unInstall": false -# }, -# { -# "agnostic": false, -# "epldImgName": "n9000-epld.10.3.1.F.img", -# "fabricPolicyName": null, -# "imageName": "nxos64-cs.10.3.1.F.bin", -# "imagePresent": "Present", -# "nxosVersion": "10.3.1_nxos64-cs_64bit", -# "packageName": "", -# "platform": "N9K/N3K", -# "platformPolicies": "", -# "policyDescr": "NR3F", -# "policyName": "NR3F", -# "policyType": "PLATFORM", -# "ref_count": 0, -# "role": null, -# "rpmimages": null, -# "unInstall": false -# } -# ], -# "message": "", -# "status": "SUCCESS" -# }, -# "MESSAGE": "OK", -# "METHOD": "GET", -# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", -# "RETURN_CODE": 200, -# "sequence_number": 0 -# }, -# { # "DATA": "Selected policy(s) deleted successfully.", # "MESSAGE": "OK", # "METHOD": "DELETE", @@ -467,11 +366,6 @@ # ], # "result": [ # { -# "found": true, -# "sequence_number": 0, -# "success": true -# }, -# { # "changed": true, # "sequence_number": 1, # "success": true @@ -498,15 +392,17 @@ - (result.diff | length) == 1 - image_policy_1 in result.diff[0].policyNames - image_policy_2 in result.diff[0].policyNames - - (result.response | length) == 2 - - result.response[0].MESSAGE == "OK" - - result.response[0].METHOD == "GET" - - result.response[0].RETURN_CODE == 200 - - result.response[1].MESSAGE == "OK" - - result.response[1].METHOD == "DELETE" - - result.response[1].RETURN_CODE == 200 + - result.diff[0].sequence_number == 1 - (result.metadata | length) == 1 - result.metadata[0].action == "delete" - result.metadata[0].check_mode == False - result.metadata[0].sequence_number == 1 - result.metadata[0].state == "deleted" + - (result.response | length) == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "DELETE" + - result.response[0].RETURN_CODE == 200 + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyCreateBulk.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyCreateBulk.json index 54ad7669f..9699d77d6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyCreateBulk.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyCreateBulk.json @@ -3,7 +3,7 @@ "Mocked payloads for ImagePolicyCreateBulk unit tests.", "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py" ], - "test_image_policy_create_bulk_00020a": [ + "test_image_policy_create_bulk_00010a": [ { "agnostic": false, "epldImgName": "n9000-epld.10.3.2.F.img", diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json new file mode 100644 index 000000000..2fa5b0f8b --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json @@ -0,0 +1,82 @@ +{ + "TEST_NOTES": [ + "Mocked responses for endpoint EpPolicies class used in the following unit tests.", + "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py" + ], + "test_image_policy_create_bulk_00035a": { + "TEST_NOTES": [ + "No image policies exist on the controller." + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "test_image_policy_create_bulk_00036a": { + "TEST_NOTES": [ + "No image policies exist on the controller." + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "test_image_policy_create_bulk_00037a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json new file mode 100644 index 000000000..1c18dbcf8 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json @@ -0,0 +1,34 @@ +{ + "TEST_NOTES": [ + "Mocked responses for EpPolicyCreate endpoint.", + "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py" + ], + "test_image_policy_create_bulk_00035a": { + "DATA": "Policy created successfully.", + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_create_bulk_00036a": { + "DATA": "Internal server error.", + "MESSAGE": "NOK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", + "RETURN_CODE": 500 + }, + "test_image_policy_create_bulk_00037b": { + "DATA": "Policy created successfully.", + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_create_bulk_00037c": { + "DATA": "Internal server error.", + "MESSAGE": "NOK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", + "RETURN_CODE": 500 + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_ImagePolicyCommon.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_ImagePolicyCommon.json deleted file mode 100644 index 6c8c395f8..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_ImagePolicyCommon.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "TEST_NOTES": [ - "Mocked responses for ImagePolicyCreate unit tests.", - "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create.py" - ], - "test_image_policy_common_00020a": { - "DATA": "NA", - "MESSAGE": "OK", - "METHOD": "GET", - "REQUEST_PATH": "https://foo/bar/endpoint", - "RETURN_CODE": 200 - }, - "test_image_policy_common_00021a": { - "DATA": "NA", - "MESSAGE": "Not Found", - "METHOD": "GET", - "REQUEST_PATH": "https://foo/bar/endpoint", - "RETURN_CODE": 404 - }, - "test_image_policy_common_00022a": { - "DATA": "NA", - "MESSAGE": "Internal Server Error", - "METHOD": "GET", - "REQUEST_PATH": "https://foo/bar/endpoint", - "RETURN_CODE": 500 - }, - "test_image_policy_common_00030a": { - "DATA": "NA", - "MESSAGE": "OK", - "METHOD": "POST", - "REQUEST_PATH": "https://foo/bar/endpoint", - "RETURN_CODE": 200 - }, - "test_image_policy_common_00031a": { - "DATA": "NA", - "MESSAGE": "NOK", - "METHOD": "POST", - "REQUEST_PATH": "https://foo/bar/endpoint", - "RETURN_CODE": 200 - }, - "test_image_policy_common_00032a": { - "DATA": "NA", - "ERROR": "Oh no!", - "MESSAGE": "OK", - "METHOD": "POST", - "REQUEST_PATH": "https://foo/bar/endpoint", - "RETURN_CODE": 200 - } -} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/results_ImagePolicyCommon.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/results_ImagePolicyCommon.json deleted file mode 100644 index c35c40995..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/results_ImagePolicyCommon.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "TEST_NOTES": [ - "Mocked results for ImagePolicyCommon unit tests.", - "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_common.py" - ], - "test_image_policy_common_00090a": { - "changed": false, - "failed": true, - "diff": [ - {} - ], - "response": [ - {} - ], - "result": [ - {} - ] - } -} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_common.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_common.py deleted file mode 100644 index f517e3533..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_common.py +++ /dev/null @@ -1,726 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# See the following regarding *_fixture imports -# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html -# Due to the above, we also need to disable unused-import -# Also, fixtures need to use *args to match the signature of the function they are mocking -# pylint: disable=unused-import -# pylint: disable=redefined-outer-name -# pylint: disable=protected-access -# pylint: disable=unused-argument -# pylint: disable=invalid-name - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - -import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ - Results -from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - does_not_raise, image_policy_common_fixture, responses_image_policy_common, - results_image_policy_common) - - -def test_image_policy_common_00010(image_policy_common) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - Summary - Verify that the class attributes are initialized to expected values - and that fail_json is not called. - - Test - - Class attributes are initialized to expected values - - fail_json is not called - """ - with does_not_raise(): - instance = image_policy_common - instance.results = Results() - assert instance.class_name == "ImagePolicyCommon" - assert len(instance.results.changed) == 0 - assert len(instance.results.failed) == 0 - assert instance.results.response == [] - assert instance.results.response_current == {"sequence_number": 0} - assert instance.results.result == [] - assert instance.results.result_current == {"sequence_number": 0} - assert instance.results.diff_current == {"sequence_number": 0} - assert instance.results.diff == [] - assert instance.results.response_data == [] - - -def test_image_policy_common_00020(image_policy_common) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - _handle_response() - - _handle_get_response() - - Summary - Verify that _handle_response() calls the appropriate methods when verb == GET - and response is successful (RETURN_CODE == 200) and that a proper result is - returned. - - Setup - - verb is set to GET - - response RETURN_CODE == 200 - - response MESSAGE == "OK" - - Test - - _handle_response() calls _handle_response_get() - - _handle_response_get() returns a proper result - - fail_json is not called - """ - key = "test_image_policy_common_00020a" - verb = "GET" - - with does_not_raise(): - instance = image_policy_common - result = instance._handle_response(responses_image_policy_common(key), verb) - assert result == {"success": True, "found": True} - - -def test_image_policy_common_00021(image_policy_common) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - _handle_response() - - _handle_get_response() - - Summary - Verify that _handle_response() returns a proper result when verb == GET - and response is unsuccessful (RETURN_CODE == 404 and MESSAGE == "Not Found"). - - Setup - - verb is set to GET - - response RETURN_CODE == 404 - - response MESSAGE == "Not Found" - - Test - - _handle_response() calls _handle_response_get() - - _handle_response_get() returns a proper result - - fail_json is not called - """ - key = "test_image_policy_common_00021a" - verb = "GET" - - with does_not_raise(): - instance = image_policy_common - result = instance._handle_response(responses_image_policy_common(key), verb) - assert result == {"success": True, "found": False} - - -def test_image_policy_common_00022(image_policy_common) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - _handle_response() - - _handle_get_response() - - Summary - Verify that _handle_response() returns a proper result when verb == GET - and response is unsuccessful (RETURN_CODE == 500 and MESSAGE == "Internal Server Error"). - - Setup - - verb is set to GET - - response RETURN_CODE == 500 - - response MESSAGE == "Internal Server Error" - - Test - - _handle_response() calls _handle_response_get() - - _handle_response_get() returns a proper result - - fail_json is not called - """ - key = "test_image_policy_common_00022a" - verb = "GET" - - with does_not_raise(): - instance = image_policy_common - result = instance._handle_response(responses_image_policy_common(key), verb) - assert result == {"success": False, "found": False} - - -def test_image_policy_common_00023(image_policy_common) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - _handle_response() - - _handle_unknown_request_verbs() - - Summary - Verify that _handle_response() calls _handle_unknown_request_verbs() when verb - is unknown and that _handle_unknown_request_verbs() calls fail_json. - - Setup - - verb is set to FOOBAR - - Test - - _handle_response() calls _handle_unknown_request_verbs() - - _handle_unknown_request_verbs() calls fail_json - - instance.result is unchanged from initialized value - """ - key = "test_image_policy_common_00023a" - verb = "FOOBAR" - match = r"ImagePolicyCommon\._handle_unknown_request_verbs: Unknown request verb \(FOOBAR\)" - with does_not_raise(): - instance = image_policy_common - instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): - instance._handle_response(responses_image_policy_common(key), verb) - assert instance.results.result == [] - - -@pytest.mark.parametrize("verb", ["POST", "PUT", "DELETE"]) -def test_image_policy_common_00030(image_policy_common, verb) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - _handle_response() - - _handle_post_put_delete_response() - - Summary - Verify that _handle_response() calls the appropriate methods when verb == POST - and response is successful (MESSAGE = "OK") and that a proper result is - returned. - - Setup - - verb == POST - - response MESSAGE == "OK" - - Test - - _handle_response() calls _handle_post_put_delete_response() - - _handle_post_put_delete_response() returns a proper result - - fail_json is not called - - Discussion - RESULT_CODE is not checked or used in the code, so it is not tested. - """ - key = "test_image_policy_common_00030a" - - with does_not_raise(): - instance = image_policy_common - result = instance._handle_response(responses_image_policy_common(key), verb) - assert result == {"success": True, "changed": True} - - -@pytest.mark.parametrize("verb", ["POST", "PUT", "DELETE"]) -def test_image_policy_common_00031(image_policy_common, verb) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - _handle_response() - - _handle_post_put_delete_response() - - Summary - Verify that _handle_response() calls the appropriate methods when verb == POST - and response is unsuccessful (MESSAGE != "OK") and that a proper result is - returned. - - Setup - - verb == POST - - response MESSAGE == "NOK" - - Test - - _handle_response() calls _handle_post_put_delete_response() - - _handle_post_put_delete_response() returns a proper result - - fail_json is not called - - Discussion - RESULT_CODE is not checked or used in the code, so it is not tested. - """ - key = "test_image_policy_common_00031a" - - with does_not_raise(): - instance = image_policy_common - result = instance._handle_response(responses_image_policy_common(key), verb) - assert result == {"success": False, "changed": False} - - -@pytest.mark.parametrize("verb", ["POST", "PUT", "DELETE"]) -def test_image_policy_common_00032(image_policy_common, verb) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - _handle_response() - - _handle_post_put_delete_response() - - Summary - Verify that _handle_response() calls the appropriate methods when verb == POST - and response is unsuccessful (ERROR key is present) and that a proper result is - returned. - - Setup - - verb == POST - - response ERROR == "Oh no!" - - Test - - _handle_response() calls _handle_post_put_delete_response() - - _handle_post_put_delete_response() returns a proper result - - fail_json is not called - - Discussion - RESULT_CODE is not checked or used in the code, so it is not tested. - """ - key = "test_image_policy_common_00032a" - - with does_not_raise(): - instance = image_policy_common - result = instance._handle_response(responses_image_policy_common(key), verb) - assert result == {"success": False, "changed": False} - - -@pytest.mark.parametrize( - "arg, return_value", - [ - (True, True), - (False, False), - ("True", True), - ("False", False), - ("true", True), - ("false", False), - (1, 1), - ("tru", "tru"), - ("fals", "fals"), - (None, None), - ({"foo"}, {"foo"}), - ], -) -def test_image_policy_common_00040(image_policy_common, arg, return_value) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - make_boolean() - - Summary - Verify that make_boolean() returns expected values for various inputs. - - Test - - make_boolean() returns expected values - - fail_json is not called - """ - with does_not_raise(): - instance = image_policy_common - value = instance.make_boolean(arg) - assert value == return_value - - -@pytest.mark.parametrize( - "arg, return_value", - [ - ("", None), - ("none", None), - ("None", None), - ("NONE", None), - ("null", None), - ("Null", None), - ("NULL", None), - (None, None), - ("False", "False"), - ("true", "true"), - (1, 1), - ({"foo"}, {"foo"}), - (True, True), - (False, False), - ], -) -def test_image_policy_common_00050(image_policy_common, arg, return_value) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - make_none() - - Summary - Verify that make_none() returns expected values for various inputs. - - Test - - make_none() returns expected values - - fail_json is not called - """ - with does_not_raise(): - instance = image_policy_common - value = instance.make_none(arg) - assert value == return_value - - -MATCH_00060 = r"Results\.changed: instance\.changed must be a bool\." - - -@pytest.mark.parametrize( - "arg, expected, flag", - [ - (True, does_not_raise(), True), - (False, does_not_raise(), True), - (None, pytest.raises(TypeError, match=MATCH_00060), False), - ("FOO", pytest.raises(TypeError, match=MATCH_00060), False), - ], -) -def test_image_policy_common_00060(image_policy_common, arg, expected, flag) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - instance.results.changed getter/setter - - Summary - Verify that instance.changed returns expected values and - calls fail_json appropriately. - - Test - - instance.results.changed returns expected values - - fail_json is called when unexpected values are passed - - fail_json is not called when expected values are passed - """ - with does_not_raise(): - instance = image_policy_common - instance.results = Results() - with expected: - instance.results.changed = arg - if flag is True: - assert arg in instance.results.changed - else: - assert len(instance.results.changed) == 0 - - -MATCH_00070 = r"Results\.diff: instance\.diff must be a dict\." - - -@pytest.mark.parametrize( - "arg, return_value, expected, flag", - [ - ({}, [{"sequence_number": 0}], does_not_raise(), True), - ( - {"foo": "bar"}, - [{"foo": "bar", "sequence_number": 0}], - does_not_raise(), - True, - ), - (None, None, pytest.raises(TypeError, match=MATCH_00070), False), - ("FOO", None, pytest.raises(TypeError, match=MATCH_00070), False), - ], -) -def test_image_policy_common_00070( - image_policy_common, arg, return_value, expected, flag -) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - @diff getter/setter - - Summary - Verify that instance.diff returns expected values and - calls fail_json appropriately. - - Test - - @diff returns expected values - - fail_json is called when unexpected values are passed - - fail_json is not called when expected values are passed - """ - with does_not_raise(): - instance = image_policy_common - instance.results = Results() - with expected: - instance.results.diff = arg - if flag is True: - assert instance.results.diff == return_value - else: - assert instance.results.diff == [] - - -MATCH_00080 = r"Results\.failed: instance\.failed must be a bool\." - - -@pytest.mark.parametrize( - "arg, expected, flag", - [ - (True, does_not_raise(), True), - (False, does_not_raise(), True), - (None, pytest.raises(TypeError, match=MATCH_00080), False), - ("FOO", pytest.raises(TypeError, match=MATCH_00080), False), - ], -) -def test_image_policy_common_00080(image_policy_common, arg, expected, flag) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - @failed getter/setter - - Summary - Verify that instance.failed returns expected values and - calls fail_json appropriately. - - Test - - @failed returns expected values - - fail_json is called when unexpected values are passed - - fail_json is not called when expected values are passed - """ - with does_not_raise(): - instance = image_policy_common - instance.results = Results() - with expected: - instance.results.failed = arg - if flag is True: - assert arg in instance.results.failed - else: - assert True in instance.results.failed - - -def test_image_policy_common_00090(image_policy_common) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - @failed_result getter - - Summary - Verify that failed_result returns expected value. - - Test - - @failed_result returns expected value - - fail_json is not called - """ - key = "test_image_policy_common_00090a" - with does_not_raise(): - instance = image_policy_common - instance.results = Results() - value = instance.results.failed_result - assert value == results_image_policy_common(key) - - -MATCH_00100 = r"Results\.response_current: instance\.response_current must be a dict\." - - -@pytest.mark.parametrize( - "arg, return_value, expected, flag", - [ - ({}, {"sequence_number": 0}, does_not_raise(), True), - ({"foo": "bar"}, {"foo": "bar", "sequence_number": 0}, does_not_raise(), True), - (None, None, pytest.raises(TypeError, match=MATCH_00100), False), - ("FOO", None, pytest.raises(TypeError, match=MATCH_00100), False), - ], -) -def test_image_policy_common_00100( - image_policy_common, arg, return_value, expected, flag -) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - @response_current getter/setter - - Summary - Verify that instance.results.response_current returns expected values and - raises TypeError appropriately. - - Test - - instance.results.response_current returns expected values - - TypeError is raised when unexpected values are passed - - TypeError is not raised when expected values are passed - """ - with does_not_raise(): - instance = image_policy_common - instance.results = Results() - with expected: - instance.results.response_current = arg - if flag is True: - assert instance.results.response_current == return_value - else: - assert instance.results.response_current == {"sequence_number": 0} - - -MATCH_00110 = r"Results\.response: instance\.response must be a dict\." - - -@pytest.mark.parametrize( - "arg, return_value, expected, flag", - [ - ({}, [{"sequence_number": 0}], does_not_raise(), True), - ( - {"foo": "bar"}, - [{"foo": "bar", "sequence_number": 0}], - does_not_raise(), - True, - ), - (None, None, pytest.raises(TypeError, match=MATCH_00110), False), - ("FOO", None, pytest.raises(TypeError, match=MATCH_00110), False), - ], -) -def test_image_policy_common_00110( - image_policy_common, arg, return_value, expected, flag -) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - @response getter/setter - - Summary - Verify that instance.results.response returns expected values and - raises TypeError appropriately. - - Test - - instance.results.response returns expected value - - TypeError is raised when unexpected values are passed - - TypeError is not raised when expected values are passed - """ - with does_not_raise(): - instance = image_policy_common - instance.results = Results() - with expected: - instance.results.response = arg - if flag is True: - assert instance.results.response == return_value - else: - assert instance.results.response == [] - - -@pytest.mark.parametrize( - "arg, return_value", - [ - ({}, [{}]), - ({"foo": "bar"}, [{"foo": "bar"}]), - (None, [None]), - ("FOO", ["FOO"]), - (1, [1]), - (True, [True]), - (False, [False]), - ([], [[]]), - ([1, 2, 3], [[1, 2, 3]]), - ], -) -def test_image_policy_common_00120(image_policy_common, arg, return_value) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - @results - - Summary - Verify that instance.results.response_data returns expected values and - never calls fail_json. - - Test - - instance.results.response_datea returns expected values - - fail_json is not called - """ - with does_not_raise(): - instance = image_policy_common - instance.results = Results() - instance.results.response_data = arg - assert instance.results.response_data == return_value - - -MATCH_00130 = r"Results\.result: instance\.result must be a dict\." - - -@pytest.mark.parametrize( - "arg, return_value, expected, flag", - [ - ({}, [{"sequence_number": 0}], does_not_raise(), True), - ( - {"foo": "bar"}, - [{"foo": "bar", "sequence_number": 0}], - does_not_raise(), - True, - ), - (None, None, pytest.raises(TypeError, match=MATCH_00130), False), - ("FOO", None, pytest.raises(TypeError, match=MATCH_00130), False), - ], -) -def test_image_policy_common_00130( - image_policy_common, arg, return_value, expected, flag -) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - @result getter/setter - - Summary - Verify that instance.results.result returns expected values and - raises TypeError appropriately. - - Test - - instance.results.result returns expected values - - TypeError is raised when unexpected values are passed - - TypeError is not raised when expected values are passed - """ - with does_not_raise(): - instance = image_policy_common - instance.results = Results() - with expected: - instance.results.result = arg - if flag is True: - assert instance.results.result == return_value - else: - assert instance.results.result == [] - - -MATCH_00140 = r"Results\.result_current: instance\.result_current must be a dict\." - - -@pytest.mark.parametrize( - "arg, return_value, expected, flag", - [ - ({}, {"sequence_number": 0}, does_not_raise(), True), - ({"foo": "bar"}, {"foo": "bar", "sequence_number": 0}, does_not_raise(), True), - (None, None, pytest.raises(TypeError, match=MATCH_00140), False), - ("FOO", None, pytest.raises(TypeError, match=MATCH_00140), False), - ], -) -def test_image_policy_common_00140( - image_policy_common, arg, return_value, expected, flag -) -> None: - """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - instance.results.result_current getter/setter - - Summary - Verify that instance.result_current returns expected values and - calls fail_json appropriately. - - Test - - instance.results.result_current returns expected values - - TypeError is raised when unexpected values are passed - - TypeError is not raised when expected values are passed - """ - with does_not_raise(): - instance = image_policy_common - instance.results = Results() - with expected: - instance.results.result_current = arg - if flag is True: - assert instance.results.result_current == return_value - else: - assert instance.results.result_current == {"sequence_number": 0} diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py index 5e2c0d277..27ea40445 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py @@ -29,23 +29,27 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -from typing import Any, Dict +import inspect import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - GenerateResponses, MockImagePolicies, does_not_raise, - image_policies_all_policies, image_policy_create_bulk_fixture, - payloads_image_policy_create_bulk, responses_image_policy_create_bulk, - rest_send_result_current, results_image_policy_create_bulk) + MockAnsibleModule, MockImagePolicies, does_not_raise, + image_policy_create_bulk_fixture, params, + payloads_image_policy_create_bulk, responses_ep_policies, + responses_ep_policy_create, rest_send_result_current) -def test_image_policy_create_bulk_00010(image_policy_create_bulk) -> None: +def test_image_policy_create_bulk_00000(image_policy_create_bulk) -> None: """ Classes and Methods - ImagePolicyCreateCommon @@ -64,11 +68,10 @@ def test_image_policy_create_bulk_00010(image_policy_create_bulk) -> None: instance = image_policy_create_bulk assert instance.class_name == "ImagePolicyCreateBulk" assert instance.action == "create" - assert instance.state == "merged" - assert instance.check_mode is False - assert isinstance(instance.endpoints, ApiEndpoints) - assert instance.path == ApiEndpoints().policy_create["path"] - assert instance.verb == ApiEndpoints().policy_create["verb"] + assert instance.params.get("state") == "merged" + assert instance.params.get("check_mode") is False + assert instance.endpoint.class_name == "EpPolicyCreate" + assert instance.endpoint.verb == "POST" assert instance._mandatory_payload_keys == { "nxosVersion", "policyName", @@ -78,24 +81,25 @@ def test_image_policy_create_bulk_00010(image_policy_create_bulk) -> None: assert instance._payloads_to_commit == [] -def test_image_policy_create_bulk_00020(image_policy_create_bulk) -> None: +def test_image_policy_create_bulk_00010(image_policy_create_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - __init__() - payloads setter - ImagePolicyCreateBulk - __init__() - Summary + ### Summary Verify that the payloads setter sets the payloads attribute to the expected value. - Test + ### Test - payloads is set to expected value - fail_json is not called """ - key = "test_image_policy_create_bulk_00020a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_create_bulk @@ -105,28 +109,31 @@ def test_image_policy_create_bulk_00020(image_policy_create_bulk) -> None: def test_image_policy_create_bulk_00021(image_policy_create_bulk) -> None: """ - Classes and Methods - - ImagePolicyCreateCommon - - __init__() - - payloads setter - - ImagePolicyCreateBulk - - __init__() - - Summary - Verify that the payloads setter calls fail_json when payloads is not a list of dict - - Test - - fail_json is called because payloads is not a list of dict + ### Classes and Methods + - ImagePolicyCreateCommon + - __init__() + - payloads.setter + - ImagePolicyCreateBulk + - __init__() + + ### Summary + Verify that the payloads setter raises TypeError when payloads is not + a list of dict. + + ### Test + - TypeError is raised because payloads is not a list of dict - instance.payloads is not modified, hence it retains its initial value of None """ - key = "test_image_policy_create_bulk_00021a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + match = "ImagePolicyCreateBulk.payloads: " match += "payloads must be a list of dict. got dict for value" with does_not_raise(): instance = image_policy_create_bulk instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): + with pytest.raises(TypeError, match=match): instance.payloads = payloads_image_policy_create_bulk(key) assert instance.payloads is None @@ -141,32 +148,34 @@ def test_image_policy_create_bulk_00021(image_policy_create_bulk) -> None: ) def test_image_policy_create_bulk_00022(image_policy_create_bulk, key, match) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - __init__() - payloads setter - ImagePolicyCreateBulk - __init__() - Summary + ### Summary Verify that the payloads setter calls fail_json when a payload in the payloads list - is missing a mandatory key + is missing a mandatory key. - Test - - fail_json is called because a payload in the payloads list is missing a mandatory key - - instance.payloads is not modified, hence it retains its initial value of None + ### Test + - ``ValueError`` is raised because a payload in the payloads list is + missing a mandatory key. + - instance.payloads is not modified, hence it retains its initial value + of None. """ with does_not_raise(): instance = image_policy_create_bulk instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): + with pytest.raises(ValueError, match=match): instance.payloads = payloads_image_policy_create_bulk(key) assert instance.payloads is None def test_image_policy_create_bulk_00030(monkeypatch, image_policy_create_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - __init__() - payloads setter @@ -174,22 +183,23 @@ def test_image_policy_create_bulk_00030(monkeypatch, image_policy_create_bulk) - - ImagePolicyCreateBulk - __init__() - Summary + ### Summary Verify behavior when the user sends an image create payload for an image policy that already exists on the controller. - Setup + ### Setup - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyCreateCommon().payloads is set to contain one payload (KR5M) that is present in all_policies. - Test + ### Test - payloads_to_commit will an empty list because all payloads in instance.payloads exist on the controller. """ - key = "test_image_policy_create_bulk_00030a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" instance = image_policy_create_bulk instance.results = Results() @@ -203,7 +213,7 @@ def test_image_policy_create_bulk_00030(monkeypatch, image_policy_create_bulk) - def test_image_policy_create_bulk_00031(monkeypatch, image_policy_create_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - __init__() - payloads setter @@ -211,23 +221,24 @@ def test_image_policy_create_bulk_00031(monkeypatch, image_policy_create_bulk) - - ImagePolicyCreateBulk - __init__() - Summary + ### Summary Verify that instance._build_payloads_to_commit() adds a payload to the payloads_to_commit list when a request is made to create an image policy that does not exist on the controller. - Setup + ### Setup - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyCreateCommon().payloads is set to contain one payload containing an image policy (FOO) that is not present in all_policies. - Test + ### Test - _payloads_to_commit will equal instance.payloads since none of the image policies in instance.payloads exist on the controller. """ - key = "test_image_policy_create_bulk_00031a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_create_bulk @@ -241,14 +252,14 @@ def test_image_policy_create_bulk_00031(monkeypatch, image_policy_create_bulk) - def test_image_policy_create_bulk_00032(monkeypatch, image_policy_create_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - payloads setter - _build_payloads_to_commit() - ImagePolicyCreateBulk - __init__() - Setup + ### Setup - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. @@ -256,12 +267,13 @@ def test_image_policy_create_bulk_00032(monkeypatch, image_policy_create_bulk) - an image policy (FOO) that is not present in all_policies and one payload containing an image policy (KR5M) that does exist on the controller. - Test + ### Test - _payloads_to_commit will contain one payload - The policyName for this payload will be "FOO", which is the image policy that does not exist on the controller """ - key = "test_image_policy_create_bulk_00032a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" instance = image_policy_create_bulk instance.payloads = payloads_image_policy_create_bulk(key) @@ -273,20 +285,20 @@ def test_image_policy_create_bulk_00032(monkeypatch, image_policy_create_bulk) - def test_image_policy_create_bulk_00033(image_policy_create_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateBulk - commit() - fail_json - Summary - Verify that ImagePolicyCreateBulk.commit() calls fail_json when + ### Summary + Verify that ImagePolicyCreateBulk.commit() raises ``ValueError`` when payloads is None. - Setup + ### Setup - ImagePolicyCreateCommon().payloads is not set - Test - - fail_json is called because payloads is None + ### Test + - ValueError is called because payloads is None """ with does_not_raise(): results = Results() @@ -296,25 +308,30 @@ def test_image_policy_create_bulk_00033(image_policy_create_bulk) -> None: match = ( "ImagePolicyCreateBulk.commit: payloads must be set prior to calling commit." ) - with pytest.raises(AnsibleFailJson, match=match): + with pytest.raises(ValueError, match=match): instance.commit() def test_image_policy_create_bulk_00034(monkeypatch, image_policy_create_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - payloads setter - ImagePolicyCreateBulk - commit() - Setup + ### Summary + Verify that ImagePolicyCreateBulk.commit() returns without doing anything + if payloads is an empty list. + + ### Setup - ImagePolicyCreateCommon().payloads is set to an empty list - Test - - ImagePolicyCreateBulk().commit returns without doing anything + ### Test + - ImagePolicyCreateBulk().results.changed is empty. """ - key = "test_image_policy_create_bulk_00034a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_create_bulk @@ -323,12 +340,15 @@ def test_image_policy_create_bulk_00034(monkeypatch, image_policy_create_bulk) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) with does_not_raise(): + instance.rest_send = RestSend(params) + instance.results = Results() instance.commit() + assert len(instance.results.changed) == 0 -def test_image_policy_create_bulk_00035(monkeypatch, image_policy_create_bulk) -> None: +def test_image_policy_create_bulk_00035(image_policy_create_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - _build_payloads_to_commit() - _send_payloads() @@ -336,18 +356,18 @@ def test_image_policy_create_bulk_00035(monkeypatch, image_policy_create_bulk) - - payloads setter - commit() - Summary - Verify that ImagePolicyCreateBulk.commit() behaves as expected when the - controller responds to an image create request with a 200 response. + ### Summary + Verify ImagePolicyCreateBulk.commit() happy path. Controller responds + to an image create request with a 200 response. - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), - is mocked to indicate that no policies exist on the controller. + ### Setup responses + - EpPolicies endpoint response contains DATA indicating no image policies + exist on the controller. - ImagePolicyCreateCommon().payloads is set to contain one payload that contains an image policy (FOO) which does not exist on the controller. - - RestSend.dcnm_send is mocked to return a successful (200) response. + - EpPolicyCreate endpoint response contains a 200 response. - Test + ### Test - commit calls _build_payloads_to_commit which returns one payload. - commit calls _send_payloads, which calls rest_send, which populates diff_current with the payload due to result_current indicating @@ -357,27 +377,33 @@ def test_image_policy_create_bulk_00035(monkeypatch, image_policy_create_bulk) - - results.response_current is set to the expected value - results.action is set to "create" """ - key = "test_image_policy_create_bulk_00035a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_create_bulk(key) + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_create(key) - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + gen = ResponseGenerator(responses()) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_create_bulk instance.results = Results() - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) + instance.rest_send = rest_send + instance.params = params with does_not_raise(): instance.payloads = payloads_image_policy_create_bulk(key) instance.commit() - response_current = responses_image_policy_create_bulk(key) + response_current = responses_ep_policy_create(key) response_current["sequence_number"] = 1 result_current = rest_send_result_current(key) @@ -403,7 +429,7 @@ def mock_dcnm_send(*args, **kwargs): def test_image_policy_create_bulk_00036(monkeypatch, image_policy_create_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - payloads setter - _build_payloads_to_commit() @@ -411,18 +437,18 @@ def test_image_policy_create_bulk_00036(monkeypatch, image_policy_create_bulk) - - ImagePolicyCreateBulk - commit() - Summary - Verify behavior when the controller returns a 500 response to an - image policy create request + ### Summary + Verify ImagePolicyCreateBulk.commit() sad path. Controller returns a 500 + response to an image policy create request. - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), - is mocked to indicate that no policies exist on the controller. + ### Setup + - EpPolicies endpoint response contains DATA indicating no image policies + exist on the controller. - ImagePolicyCreateBulk().payloads is set to contain one payload that contains an image policy (FOO) which does not exist on the controller. - - dcnm_send is mocked to return a failure (500) response. + - EpPolicyCreate endpoint response contains a 500 response. - Test + ### Test - A sequence_number key is added to instance.results.response_current - instance.results.diff_current is set to a dict with only the key "sequence_number", since no changes were made @@ -433,27 +459,32 @@ def test_image_policy_create_bulk_00036(monkeypatch, image_policy_create_bulk) - - The value of instance.results.metadata "state" is "merged" - The value of instance.results.metadata "sequence_number" is 1 """ - key = "test_image_policy_create_bulk_00036a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_create(key) - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + gen = ResponseGenerator(responses()) - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_create_bulk(key) + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True with does_not_raise(): instance = image_policy_create_bulk - instance.rest_send.unit_test = True instance.results = Results() + instance.rest_send = rest_send + instance.params = params instance.payloads = payloads_image_policy_create_bulk(key) - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) - - with does_not_raise(): instance.commit() - response_current = responses_image_policy_create_bulk(key) + response_current = responses_ep_policy_create(key) response_current["sequence_number"] = 1 assert instance.results.response_current == response_current assert instance.results.diff_current == {"sequence_number": 1} @@ -471,20 +502,20 @@ def mock_dcnm_send(*args, **kwargs): def test_image_policy_create_bulk_00037(monkeypatch, image_policy_create_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - _process_responses() - ImagePolicyCreateBulk - __init__() - Summary + ### Summary Simulate a succussful response from the controller, followed by a bad response from the controller during policy create. - Setup + ### Setup - instance.payloads is set to contain two payloads - Test + ### Test - Both successful and bad responses are recorded with separate sequence_numbers. - instance.results.failed will be a set() containing both True and False - instance.results.changed will be a set() containing both True and False @@ -492,8 +523,6 @@ def test_image_policy_create_bulk_00037(monkeypatch, image_policy_create_bulk) - - instance.results.result contains two results - instance.results.diff contains two diffs """ - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" key_policies = "test_image_policy_create_bulk_00037a" key_ok = "test_image_policy_create_bulk_00037b" @@ -501,25 +530,25 @@ def test_image_policy_create_bulk_00037(monkeypatch, image_policy_create_bulk) - key_payloads = "test_image_policy_create_bulk_00037d" def responses(): - yield responses_image_policy_create_bulk(key_policies) - yield responses_image_policy_create_bulk(key_ok) - yield responses_image_policy_create_bulk(key_nok) + yield responses_ep_policies(key_policies) + yield responses_ep_policy_create(key_ok) + yield responses_ep_policy_create(key_nok) - gen = GenerateResponses(responses()) + gen = ResponseGenerator(responses()) - def mock_dcnm_send(*args, **kwargs): - item = gen.next - return item + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True with does_not_raise(): instance = image_policy_create_bulk - instance.rest_send.unit_test = True instance.results = Results() + instance.rest_send = rest_send instance.payloads = payloads_image_policy_create_bulk(key_payloads) - - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) - - with does_not_raise(): instance.commit() assert len(instance.results.diff) == 2 diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py index a0d369ccc..f47be00cc 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py @@ -43,17 +43,15 @@ def test_image_policy_delete_00010(image_policy_delete) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() + ### Classes and Methods - ImagePolicyDelete - __init__() - Summary + ### Summary Verify that the class attributes are initialized to expected values and that fail_json is not called. - Test + ### Test - Class attributes are initialized to expected values - fail_json is not called """ @@ -73,21 +71,19 @@ def test_image_policy_delete_00010(image_policy_delete) -> None: def test_image_policy_delete_00020(image_policy_delete) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() + ### Classes and Methods - ImagePolicyDelete - __init__() - policy_names setter - Summary + ### Summary policy_names is set correctly to a list of strings. Verify that instance.policy_names is set to the expected value and that fail_json is not called. - Test - - policy_names is set to expected value - - fail_json is not called + ### Test + - policy_names is set to expected value. + - No exceptions are raised. """ policy_names = ["FOO", "BAR"] with does_not_raise(): @@ -98,18 +94,16 @@ def test_image_policy_delete_00020(image_policy_delete) -> None: def test_image_policy_delete_00021(image_policy_delete) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() + ### Classes and Methods - ImagePolicyDelete - __init__() - policy_names setter - Summary + ### Summary policy_names should be a list of strings, but it set to a string. Verify that fail_json is called with appropriate message. - Test + ### Test - fail_json is called because policy_names is not a list - instance.policy_names is not modified, hence it retains its initial value of None """ @@ -125,18 +119,16 @@ def test_image_policy_delete_00021(image_policy_delete) -> None: def test_image_policy_delete_00022(image_policy_delete) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() + ### Classes and Methods - ImagePolicyDelete - __init__() - policy_names setter - Summary + ### Summary policy_names is set to a list of non-strings. Verify that fail_json is called with appropriate message. - Test + ### Test - fail_json is called because policy_names is a list with a non-string element - instance.policy_names is not modified, hence it retains its initial value of None """ @@ -152,26 +144,24 @@ def test_image_policy_delete_00022(image_policy_delete) -> None: def test_image_policy_delete_00030(monkeypatch, image_policy_delete) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - _verify_image_policy_ref_count() + ### Classes and Methods - ImagePolicyDelete - __init__() + - _verify_image_policy_ref_count() - policy_names setter - _get_policies_to_delete() - Summary + ### Summary The requested policy to delete does not exist on the controller. Verify that instance._policies_to_delete is an empty list. - Setup + ### Setup - ImagePolicies().all_policies, is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyDelete.policy_names is set to contain one policy_name (FOO) that does not exist on the controller. - Test + ### Test - instance._policies_to_delete will an empty list because all of the policy_names in instance.policy_names do not exist on the controller and, hence, nothing needs to be deleted. @@ -187,23 +177,23 @@ def test_image_policy_delete_00030(monkeypatch, image_policy_delete) -> None: def test_image_policy_delete_00031(monkeypatch, image_policy_delete) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyDelete - __init__() - policy_names setter - _get_policies_to_delete() - Summary + ### Summary One policy (KR5M) is requested to be deleted and it exists on the controller. Verify that instance._policies_to_delete contains the policy name KR5M. - Setup + ### Setup - ImagePolicies().all_policies is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyDelete.policy_names is set to contain one policy_name (KR5M) that exists on the controller. - Test + ### Test - instance._policies_to_delete will contain one policy name (KR5M) """ key = "test_image_policy_delete_00031a" @@ -217,17 +207,17 @@ def test_image_policy_delete_00031(monkeypatch, image_policy_delete) -> None: def test_image_policy_delete_00032(monkeypatch, image_policy_delete) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyDelete - policy_names setter - _get_policies_to_delete() - Summary + ### Summary Of two policies being requested to delete, one policy exists on the controller and one policy does not exist on the controller. Verify that only the policy that exists on the controller is added to instance._policies_to_delete. - Setup + ### Setup - ImagePolicies().all_policies, is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyDelete().policy_names is set to contain one image policy name (FOO) @@ -248,18 +238,18 @@ def test_image_policy_delete_00032(monkeypatch, image_policy_delete) -> None: def test_image_policy_delete_00033(image_policy_delete) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyDelete - commit() - fail_json - Summary + ### Summary commit() is called without first setting policy_names. - Setup + ### Setup - ImagePolicyDelete().policy_names is not set - Test + ### Test - fail_json is called because policy_names is None """ with does_not_raise(): @@ -274,21 +264,21 @@ def test_image_policy_delete_00033(image_policy_delete) -> None: def test_image_policy_delete_00034(monkeypatch, image_policy_delete) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyDelete - policy_names setter - commit() - Summary + ### Summary commit() is called with policy_names set to an empty list. - Setup + ### Setup - ImagePolicyDelete().policy_names is set to an empty list - ImagePolicies.all_policies is mocked to indicate that no policies exist on the controller. - RestSend.dcnm_send is mocked to return a successful (200) response. - Test + ### Test - ImagePolicyDelete().commit returns without doing anything - fail_json is not called - instance.results.changed set() contains False @@ -318,20 +308,20 @@ def mock_dcnm_send(*args, **kwargs): def test_image_policy_delete_00036(monkeypatch, image_policy_delete) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyDelete - policy_names setter - _get_policies_to_delete() - commit() - Summary + ### Summary commit() is called with policy_names set to a policy_name that does not exist on the controller. - Setup + ### Setup - ImagePolicies().all_policies is mocked to indicate that no policies exist on the controller. - ImagePolicyDelete().policy_names is set a policy_name that is not on the controller. - Test + ### Test - ImagePolicyDelete()._get_policies_to_delete return an empty list - ImagePolicyDelete().commit returns without doing anything - instance.results.changed set() contains False @@ -353,26 +343,23 @@ def test_image_policy_delete_00036(monkeypatch, image_policy_delete) -> None: def test_image_policy_delete_00037(monkeypatch, image_policy_delete) -> None: """ - Classes and Methods - - ImagePolicyCommon: - - __init__() - - _handle_response() + ### Classes and Methods - ImagePolicyDelete - _get_policies_to_delete() - policy_names setter - commit() - Summary + ### Summary commit() is called with policy_names set to a policy_name that exists on the controller, and the controller returns a success (200) response. - Setup + ### Setup - ImagePolicies().all_policies is mocked to indicate policy (KR5M) exists on the controller. - ImagePolicyDelete().policy_names is set to contain policy_name KR5M. - dcnm_send is mocked to return a successful (200) response. - Test + ### Test - fail_json is not called - commit calls _get_policies_to_delete which returns a list containing policy_name (KR5M) - commit calls the mocked dcnm_send, which populates instance.response_current @@ -409,26 +396,23 @@ def mock_dcnm_send(*args, **kwargs): def test_image_policy_delete_00038(monkeypatch, image_policy_delete) -> None: """ - Classes and Methods - - ImagePolicyCommon: - - __init__() - - _handle_response() + ### Classes and Methods - ImagePolicyDelete - _get_policies_to_delete() - policy_names setter - commit() - Summary + ### Summary commit() is called with policy_names set to a policy_name that exists on the controller, and the controller returns a failure (500) response. - Setup + ### Setup - ImagePolicies().all_policies is mocked to indicate policy (KR5M) exists on the controller. - ImagePolicyDelete().policy_names is set to contain one payload (KR5M). - dcnm_send is mocked to return a failure (500) response. - Test + ### Test - fail_json is called - commit calls _get_policies_to_delete which returns a list containing policy_name (KR5M) diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_query.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_query.py index 3edbc9061..e5a51d5b8 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_query.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_query.py @@ -44,13 +44,11 @@ def test_image_policy_query_00010(image_policy_query) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() + ### Classes and Methods - ImagePolicyQuery - __init__() - Test + ### Test - Class attributes are initialized to expected values - fail_json is not called """ @@ -66,14 +64,12 @@ def test_image_policy_query_00010(image_policy_query) -> None: def test_image_policy_query_00020(image_policy_query) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() + ### Classes and Methods - ImagePolicyQuery - __init__() - policy_names setter - Test + ### Test - policy_names is set to expected value - fail_json is not called """ @@ -86,14 +82,12 @@ def test_image_policy_query_00020(image_policy_query) -> None: def test_image_policy_query_00021(image_policy_query) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() + ### Classes and Methods - ImagePolicyQuery - __init__() - policy_names setter - Test + ### Test - fail_json is called because policy_names is not a list - instance.policy_names is not modified, hence it retains its initial value of None """ @@ -109,14 +103,12 @@ def test_image_policy_query_00021(image_policy_query) -> None: def test_image_policy_query_00022(image_policy_query) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() + ### Classes and Methods - ImagePolicyQuery - __init__() - policy_names setter - Test + ### Test - fail_json is called because policy_names is a list with a non-string element - instance.policy_names is not modified, hence it retains its initial value of None """ @@ -132,17 +124,15 @@ def test_image_policy_query_00022(image_policy_query) -> None: def test_image_policy_query_00023(image_policy_query) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() + ### Classes and Methods - ImagePolicyQuery - __init__() - policy_names setter - Summary + ### Summary Verify behavior when policy_names is not set prior to calling commit - Test + ### Test - fail_json is called because policy_names is not set prior to calling commit - instance.policy_names is not modified, hence it retains its initial value of None """ @@ -159,17 +149,17 @@ def test_image_policy_query_00023(image_policy_query) -> None: def test_image_policy_query_00024(image_policy_query) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyQuery - policy_names setter - Summary + ### Summary Verify behavior when policy_names is set to an empty list - Setup + ### Setup - ImagePolicyQuery().policy_names is set to an empty list - Test + ### Test - fail_json is called from policy_names setter """ match = "ImagePolicyQuery.policy_names: policy_names must be a list of " @@ -181,26 +171,24 @@ def test_image_policy_query_00024(image_policy_query) -> None: def test_image_policy_query_00030(monkeypatch, image_policy_query) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() - - _verify_image_policy_ref_count() + ### Classes and Methods - ImagePolicyQuery - __init__() + - _verify_image_policy_ref_count() - policy_names setter - _get_policies_to_query() - commit() - Summary + ### Summary Verify behavior when user queries a policy that does not exist on the controller - Setup + ### Setup - ImagePolicies().all_policies, is mocked to indicate that one image policy (KR5M) exist on the controller. - ImagePolicyQuery.policy_names is set to contain one policy_name (FOO) that does not exist on the controller. - Test + ### Test - ImagePolicyQuery.commit() calls _get_policies_to_query() which sets instance._policies_to_query to an empty list. - instance.results.changed set() contains False @@ -246,25 +234,23 @@ def mock_dcnm_send(*args, **kwargs): def test_image_policy_query_00031(monkeypatch, image_policy_query) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() + ### Classes and Methods - ImagePolicyQuery - __init__() - policy_names setter - _get_policies_to_query() - commit() - Summary + ### Summary Verify behavior when user queries a policy that exists on the controller - Setup + ### Setup - ImagePolicies().all_policies is mocked to indicate that one image policy (KR5M) exists on the controller. - ImagePolicyQuery.policy_names is set to contain one policy_name (KR5M) that exists on the controller. - Test + ### Test - instance.diff is a list containing one dict with keys action == "query" and policyName == "KR5M" - instance.response is a list with one element @@ -310,24 +296,24 @@ def mock_dcnm_send(*args, **kwargs): def test_image_policy_query_00032(monkeypatch, image_policy_query) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyQuery - policy_names setter - _get_policies_to_query() - commit() - Summary + ### Summary Verify behavior when user queries multiple policies, some of which exist on the controller and some of which do not exist on the controller. - Setup + ### Setup - ImagePolicies().all_policies, is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyQuery().policy_names is set to contain one image policy name (FOO) that does not exist on the controller and two image policy names (KR5M, NR3F) that do exist on the controller. - Test + ### Test - instance.diff is a list containing two elements - instance.diff[0] contains keys action == "query" and policyName == "KR5M" - instance.diff[1] contains keys action == "query" and policyName == "NR3F" @@ -376,26 +362,24 @@ def mock_dcnm_send(*args, **kwargs): def test_image_policy_query_00033(monkeypatch, image_policy_query) -> None: """ - Classes and Methods - - ImagePolicyCommon - - __init__() + ### Classes and Methods - ImagePolicyQuery - __init__() - policy_names setter - _get_policies_to_query() - commit() - Summary + ### Summary Verify behavior when no image policies exist on the controller and the user queries for an image policy that, of course, does not exist. - Setup + ### Setup - ImagePolicies().all_policies, is mocked to indicate that no image policies exist on the controller. - ImagePolicyQuery.policy_names is set to contain one policy_name (FOO) that does not exist on the controller. - Test + ### Test - commit() calls _get_policies_to_query() which sets instance._policies_to_query to an empty list. - commit() sets instance.changed to False diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py index 093de7318..910ac4081 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py @@ -40,6 +40,18 @@ from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.fixture import \ load_fixture +params = { + "state": "merged", + "check_mode": False, + "config": [ + { + "name": "NR1F", + "agnostic": False, + "description": "NR1F", + "platform": "N9K", + "type": "PLATFORM"} + ] +} class GenerateResponses: """ @@ -212,15 +224,6 @@ def results(self, value): # https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html -@pytest.fixture(name="image_policy_common") -def image_policy_common_fixture(): - """ - mock ImagePolicyCommon - """ - instance = MockAnsibleModule() - instance.state = "merged" - return ImagePolicyCommon(instance) - @pytest.fixture(name="image_policy_create") def image_policy_create_fixture(): @@ -237,9 +240,9 @@ def image_policy_create_bulk_fixture(): """ mock ImagePolicyCreateBulk """ - instance = MockAnsibleModule() - instance.state = "merged" - return ImagePolicyCreateBulk(instance) + instance = ImagePolicyCreateBulk() + instance.params = params + return instance @pytest.fixture(name="image_policy_delete") @@ -382,22 +385,32 @@ def payloads_image_policy_update_bulk(key: str) -> Dict[str, str]: return data -def responses_image_policies(key: str) -> Dict[str, str]: +def responses_ep_policies(key: str) -> Dict[str, str]: """ - Return responses for ImagePolicies - Used in MockImagePolicies + Return responses for EpPolicies() endpoint """ - data_file = "responses_ImagePolicies" + data_file = "responses_EpPolicies" data = load_fixture(data_file).get(key) print(f"{data_file}: {key} : {data}") return data -def responses_image_policy_common(key: str) -> Dict[str, str]: +def responses_ep_policy_create(key: str) -> Dict[str, str]: """ - Return responses for ImagePolicyCommon + Return responses for EpPolicyCreate() endpoint """ - data_file = "responses_ImagePolicyCommon" + data_file = "responses_EpPolicyCreate" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def responses_image_policies(key: str) -> Dict[str, str]: + """ + Return responses for ImagePolicies + Used in MockImagePolicies + """ + data_file = "responses_ImagePolicies" data = load_fixture(data_file).get(key) print(f"{data_file}: {key} : {data}") return data @@ -474,16 +487,6 @@ def results_image_policies(key: str) -> Dict[str, str]: return data -def results_image_policy_common(key: str) -> Dict[str, str]: - """ - Return results for ImagePolicyCommon - """ - data_file = "results_ImagePolicyCommon" - data = load_fixture(data_file).get(key) - print(f"{data_file}: {key} : {data}") - return data - - def results_image_policy_create_bulk(key: str) -> Dict[str, str]: """ Return results for ImagePolicyCreateBulk From d6f56face4d8b0fcd44aa2ed8676894d9835ddf4 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 29 Jun 2024 08:51:59 -1000 Subject: [PATCH 209/230] image_policy_create* unit tests updates. 1. Update unit tests to reflect changes in the last commit. - image_policy_create.py: - image_policy_create_bulk.py: - utils.py 2. Update docstrings. - create.py 3. Convert private methods to public. --- plugins/module_utils/image_policy/create.py | 69 ++-- .../fixtures/payloads_ImagePolicyCreate.json | 15 +- .../fixtures/responses_EpPolicies.json | 28 ++ .../fixtures/responses_EpPolicyCreate.json | 16 +- .../test_image_policy_create.py | 312 ++++++++++++------ .../test_image_policy_create_bulk.py | 87 ++--- .../modules/dcnm/dcnm_image_policy/utils.py | 12 +- 7 files changed, 361 insertions(+), 178 deletions(-) diff --git a/plugins/module_utils/image_policy/create.py b/plugins/module_utils/image_policy/create.py index e5453227a..aa38fd117 100644 --- a/plugins/module_utils/image_policy/create.py +++ b/plugins/module_utils/image_policy/create.py @@ -72,16 +72,21 @@ def __init__(self): msg += f"action: {self.action}, " self.log.debug(msg) - def _verify_payload(self, payload): + def verify_payload(self, payload): """ - Verify that the payload is a dict and contains all mandatory keys + ### Summary + Verify that the payload is a dict and contains all mandatory keys. + + ### Raises + - ``TypeError`` if payload is not a dict. + - ``ValueError`` if payload is missing mandatory keys. """ method_name = inspect.stack()[0][3] if not isinstance(payload, dict): msg = f"{self.class_name}.{method_name}: " msg += "payload must be a dict. " msg += f"Got type {type(payload).__name__}, " - msg += f"value {payload}" + msg += f"value {payload}." raise TypeError(msg) missing_keys = [] @@ -96,34 +101,47 @@ def _verify_payload(self, payload): msg += f"{sorted(missing_keys)}" raise ValueError(msg) - def _build_payloads_to_commit(self): + def build_payloads_to_commit(self): """ + ### Summary Build a list of payloads to commit. Skip any payloads that already exist on the controller. + ### Raises + None + + ### Notes Expects self.payloads to be a list of dict, with each dict being a payload for the image policy create API endpoint. Populates self._payloads_to_commit with a list of payloads to commit. """ - self._image_policies.rest_send = self.rest_send + method_name = inspect.stack()[0][3] + + self._image_policies.rest_send = self.rest_send # pylint: disable=no-member self._image_policies.refresh() self._payloads_to_commit = [] for payload in self.payloads: - if payload.get("policyName", None) in self._image_policies.all_policies: + if payload.get("policyName") in self._image_policies.all_policies: continue self._payloads_to_commit.append(copy.deepcopy(payload)) - msg = f"self._payloads_to_commit: {json.dumps(self._payloads_to_commit, indent=4, sort_keys=True)}" + msg = f"{self.class_name}.{method_name}: " + msg += "self._payloads_to_commit: " + msg += f"{json.dumps(self._payloads_to_commit, indent=4, sort_keys=True)}" self.log.debug(msg) - def _send_payloads(self): + # pylint: disable=no-member + def send_payloads(self): """ - If check_mode is False, send the payloads to the controller - If check_mode is True, do not send the payloads to the controller + ### Summary + - If check_mode is False, send the payloads to the controller. + - If check_mode is True, do not send the payloads to the controller. + - In both cases, update results. - In both cases, update results + ### Raises + None """ self.rest_send.check_mode = self.params.get("check_mode") @@ -176,7 +194,7 @@ def payloads(self, value): msg += f"value {value}" raise TypeError(msg) for item in value: - self._verify_payload(item) + self.verify_payload(item) self._payloads = value @@ -234,7 +252,7 @@ def commit(self): """ method_name = inspect.stack()[0][3] - if self.params is None: + if self.params is None: # pylint: disable=no-member msg = f"{self.class_name}.{method_name}: " msg += "params must be set prior to calling commit." raise ValueError(msg) @@ -244,20 +262,20 @@ def commit(self): msg += "payloads must be set prior to calling commit." raise ValueError(msg) - if self.rest_send is None: + if self.rest_send is None: # pylint: disable=no-member msg = f"{self.class_name}.{method_name}: " msg += "rest_send must be set prior to calling commit." raise ValueError(msg) - if self.results is None: + if self.results is None: # pylint: disable=no-member msg = f"{self.class_name}.{method_name}: " msg += "results must be set prior to calling commit." raise ValueError(msg) - self._build_payloads_to_commit() + self.build_payloads_to_commit() if len(self._payloads_to_commit) == 0: return - self._send_payloads() + self.send_payloads() class ImagePolicyCreate(ImagePolicyCreateCommon): @@ -308,21 +326,24 @@ def __init__(self): @property def payload(self): """ - This class expects a properly-defined image policy payload. - See class docstring for the payload structure. + ### Summary + An image policy payload. See class docstring for the payload structure. """ return self._payload @payload.setter def payload(self, value): - self._verify_payload(value) + self.verify_payload(value) self._payloads = [value] self._payload = value def commit(self): """ - Create policy. - If policy already exists on the controller, do nothing. + ### Summary + Create policy. If policy already exists on the controller, do nothing. + + ### Raises + - ``ValueError`` if payload is not set prior to calling commit(). """ method_name = inspect.stack()[0][3] if self.payload is None: @@ -330,8 +351,8 @@ def commit(self): msg += "payload must be set prior to calling commit." raise ValueError(msg) - self._build_payloads_to_commit() + self.build_payloads_to_commit() if len(self._payloads_to_commit) == 0: return - self._send_payloads() + self.send_payloads() diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyCreate.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyCreate.json index aa7f05db5..a3872c497 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyCreate.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyCreate.json @@ -3,7 +3,7 @@ "Mocked payloads for ImagePolicyCreate unit tests.", "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create.py" ], - "test_image_policy_create_00020a": { + "test_image_policy_create_00010a": { "agnostic": false, "epldImgName": "n9000-epld.10.3.2.F.img", "nxosVersion": "10.3.1_nxos64-cs_64bit", @@ -88,5 +88,16 @@ "policyName": "FOO", "policyType": "PLATFORM", "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } + }, + "test_image_policy_create_00036a": { + "agnostic": false, + "epldImgName": "n9000-epld.10.3.2.F.img", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", + "platform": "N9K", + "policyDescr": "image policy of 10.3(3)F", + "policyName": "FOO", + "policyType": "PLATFORM", + "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" + } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json index 2fa5b0f8b..b0b18e5e5 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json @@ -3,6 +3,34 @@ "Mocked responses for endpoint EpPolicies class used in the following unit tests.", "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py" ], + "test_image_policy_create_00035a": { + "TEST_NOTES": [ + "No image policies exist on the controller." + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "test_image_policy_create_00036a": { + "TEST_NOTES": [ + "No image policies exist on the controller." + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, "test_image_policy_create_bulk_00035a": { "TEST_NOTES": [ "No image policies exist on the controller." diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json index 1c18dbcf8..68b96f517 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json @@ -3,7 +3,21 @@ "Mocked responses for EpPolicyCreate endpoint.", "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py" ], - "test_image_policy_create_bulk_00035a": { + "test_image_policy_create_00035a": { + "DATA": "Policy created successfully.", + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_create_00036a": { + "DATA": "Internal server error.", + "MESSAGE": "NOK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", + "RETURN_CODE": 500 + }, + "test_image_policy_create_bulk_00035a": { "DATA": "Policy created successfully.", "MESSAGE": "OK", "METHOD": "POST", diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create.py index 804cb9546..8526d5337 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create.py @@ -29,45 +29,49 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -from typing import Any, Dict +import inspect import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - MockImagePolicies, does_not_raise, image_policy_create_fixture, - payloads_image_policy_create, responses_image_policy_create, - rest_send_result_current) + MockAnsibleModule, MockImagePolicies, does_not_raise, + image_policy_create_fixture, params, payloads_image_policy_create, + responses_ep_policies, responses_ep_policy_create, + responses_image_policy_create, rest_send_result_current) -def test_image_policy_create_00010(image_policy_create) -> None: +def test_image_policy_create_00000(image_policy_create) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - __init__ - ImagePolicyCreate - __init__ - Summary + ### Summary Verify that __init__() sets class attributes to the expected values. - Test + ### Test - Class attributes initialized to expected values - - fail_json is not called + - Exceptions are not raised. """ with does_not_raise(): instance = image_policy_create assert instance.class_name == "ImagePolicyCreate" assert instance.action == "create" - assert instance.state == "merged" - assert instance.check_mode is False - assert isinstance(instance.endpoints, ApiEndpoints) - assert instance.path == ApiEndpoints().policy_create["path"] - assert instance.verb == ApiEndpoints().policy_create["verb"] + assert instance.params.get("state") == "merged" + assert instance.params.get("check_mode") is False + assert instance.endpoint.class_name == "EpPolicyCreate" + assert instance.endpoint.verb == "POST" assert instance._mandatory_payload_keys == { "nxosVersion", "policyName", @@ -77,24 +81,25 @@ def test_image_policy_create_00010(image_policy_create) -> None: assert instance._payloads_to_commit == [] -def test_image_policy_create_00020(image_policy_create) -> None: +def test_image_policy_create_00010(image_policy_create) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - __init__ - ImagePolicyCreate - __init__ - payload setter - Summary + ### Summary Verify that the payloads setter sets the payloads attribute to the expected value. - Test + ### Test - payload is set to expected value - - fail_json is not called + - Exceptions are not raised. """ - key = "test_image_policy_create_00020a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_create @@ -104,30 +109,32 @@ def test_image_policy_create_00020(image_policy_create) -> None: def test_image_policy_create_00021(image_policy_create) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - __init__ - ImagePolicyCreate - __init__ - payload setter - Summary - Verify that the payload setter calls fail_json when payload is not a dict + ### Summary + Verify that the payload setter raises TypeError when payload is not a dict. - Setup - - payload is set to a list + ### Setup + - payload is set to a list. - Test - - fail_json is called because payload is not a dict + ### Test + ``TypeError`` is raised because payload is not a dict. """ - key = "test_image_policy_create_00021a" - match = "ImagePolicyCreate._verify_payload: " - match += "payload must be a dict. Got type list" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + match = r"ImagePolicyCreate\.verify_payload:\s+" + match += r"payload must be a dict\. Got type list, value \[\]\." with does_not_raise(): instance = image_policy_create instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): + with pytest.raises(TypeError, match=match): instance.payload = payloads_image_policy_create(key) assert instance.payload is None @@ -142,159 +149,161 @@ def test_image_policy_create_00021(image_policy_create) -> None: ) def test_image_policy_create_00022(image_policy_create, key, match) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - __init__ - ImagePolicyCreate - __init__ - payload setter - Summary - Verify that the payload setter calls fail_json when a payload is missing - a mandatory key + ### Summary + Verify that the payload setter raises ``ValueError`` when a payload is + missing a mandatory key. - Test - - fail_json is called because payload is missing a mandatory key - - instance.payload is not modified, hence it retains its initial value of None + ### Test + - ``ValueError`` is raised because payload is missing a mandatory key. + - ``instance.payload`` retains its initial value of None. """ with does_not_raise(): instance = image_policy_create instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): + with pytest.raises(ValueError, match=match): instance.payload = payloads_image_policy_create(key) assert instance.payload is None def test_image_policy_create_00030(monkeypatch, image_policy_create) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - __init__() - - _build_payloads_to_commit() + - build_payloads_to_commit() - ImagePolicyCreate - __init__() - payload setter - Summary + ### Summary Verify behavior when the user sends an image create payload for an image policy that already exists on the controller. - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), + ### Setup + - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyCreate().payload is set to contain one payload (KR5M) that is present in all_policies. - Test + ### Test - payloads_to_commit will an empty list because the payload in instance.payload exists on the controller. """ - key = "test_image_policy_create_00030a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_create instance.results = Results() instance.payload = payloads_image_policy_create(key) monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() + instance.build_payloads_to_commit() assert instance._payloads_to_commit == [] def test_image_policy_create_00031(monkeypatch, image_policy_create) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - __init__() - - _build_payloads_to_commit() + - build_payloads_to_commit() - ImagePolicyCreate - __init__() - payload setter - Summary - Verify that instance._build_payloads_to_commit() adds a payload to the + ### Summary + Verify that instance.build_payloads_to_commit() adds a payload to the payloads_to_commit list when a request is made to create an image policy that does not exist on the controller. - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), + ### Setup + - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyCreate().payload is set to contain one payload containing an image policy (FOO) that is not present in all_policies. - Test + ### Test - _payloads_to_commit will equal list(instance.payload) since none of the image policies in instance.payloads exist on the controller. """ - key = "test_image_policy_create_00031a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_create instance.results = Results() instance.payload = payloads_image_policy_create(key) monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() + instance.build_payloads_to_commit() assert len(instance._payloads_to_commit) == 1 assert instance._payloads_to_commit == [payloads_image_policy_create(key)] def test_image_policy_create_00033(image_policy_create) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreate - commit() - - fail_json - Summary - Verify that ImagePolicyCreate.commit() calls fail_json when - payload is None. + ### Summary + Verify that ImagePolicyCreate.commit() raises ValueError when payload + is not set. - Setup - - ImagePolicyCreate().payload is not set + ### Setup + - ImagePolicyCreate().payload is not set. - Test - - fail_json is called because payload is None + ### Test + - ``ValueError`` is raised because payload is not set. """ with does_not_raise(): instance = image_policy_create instance.results = Results() match = "ImagePolicyCreate.commit: payload must be set prior to calling commit." - with pytest.raises(AnsibleFailJson, match=match): + with pytest.raises(ValueError, match=match): instance.commit() def test_image_policy_create_00034(monkeypatch, image_policy_create) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - __init__() - - _build_payloads_to_commit() + - build_payloads_to_commit() - ImagePolicyCreate - __init__() - payload setter - commit() - Summary + ### Summary Verify that ImagePolicyCreate.commit() works as expected when the image policy already exists on the controller. This is similar to test_image_policy_create_00030 but tests that the commit method returns when _payloads_to_commit is empty. - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), + ### Setup + - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyCreate().payload is set to contain one payload (KR5M) that is present in all_policies. - Test + ### Test - payloads_to_commit will an empty list because all payloads in instance.payloads exist on the controller. - - commit will return without calling _send_payloads - - fail_json is not called + - commit will return without calling send_payloads + - Exceptions are not raised. """ - key = "test_image_policy_create_00034a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_create @@ -305,55 +314,66 @@ def test_image_policy_create_00034(monkeypatch, image_policy_create) -> None: assert instance._payloads_to_commit == [] -def test_image_policy_create_00035(monkeypatch, image_policy_create) -> None: +def test_image_policy_create_00035(image_policy_create) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - - _build_payloads_to_commit() - - _send_payloads() + - build_payloads_to_commit() + - send_payloads() - ImagePolicyCreate - payload setter - commit() - Summary - Verify that ImagePolicyCreate.commit() behaves as expected when the - controller responds to an image policy create request with a 200 response. + ### Summary + Verify ImagePolicyCreate.commit() happy path. Controller responds + to an image create request with a 200 response. - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), - is mocked to indicate that no policies exist on the controller. - - ImagePolicyCreate().payload is set to contain one payload that + ### Setup + - EpPolicies endpoint response contains DATA indicating no image policies + exist on the controller. + - ImagePolicyCreateCommon().payloads is set to contain one payload that contains an image policy (FOO) which does not exist on the controller. - - dcnm_send is mocked to return a successful (200) response. + - EpPolicyCreate endpoint response contains a 200 response. - Test - - commit calls _build_payloads_to_commit which returns one payload. - - commit calls _send_payloads, which calls rest_send, which populates + ### Test + - commit calls build_payloads_to_commit which returns one payload. + - commit calls send_payloads, which calls rest_send, which populates diff_current with the payload due to result_current indicating success. - - results.result_current is set to the expected value - - results.diff_current is set to the expected value - - results.response_current is set to the expected value - - results.action is set to "create" + - results.result_current is set to the expected value. + - results.diff_current is set to the expected value. + - results.response_current is set to the expected value. + - results.action is set to "create". """ - key = "test_image_policy_create_00035a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_create(key) + + gen_responses = ResponseGenerator(responses()) - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_create(key) + def payloads(): + yield payloads_image_policy_create(key) - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + gen_payloads = ResponseGenerator(payloads()) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_create instance.results = Results() - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) + instance.rest_send = rest_send + instance.params = params with does_not_raise(): - instance.payload = payloads_image_policy_create(key) + instance.payload = gen_payloads.next instance.commit() response_current = responses_image_policy_create(key) @@ -378,3 +398,81 @@ def mock_dcnm_send(*args, **kwargs): assert instance.results.metadata[0]["action"] == "create" assert instance.results.metadata[0]["state"] == "merged" assert instance.results.metadata[0]["sequence_number"] == 1 + + +def test_image_policy_create_00036(image_policy_create) -> None: + """ + ### Classes and Methods + - ImagePolicyCreateCommon + - payloads setter + - build_payloads_to_commit() + - send_payloads() + - ImagePolicyCreate + - commit() + + ### Summary + Verify ImagePolicyCreate.commit() sad path. Controller returns a 500 + response to an image policy create request. + + ### Setup + - EpPolicies endpoint response contains DATA indicating no image policies + exist on the controller. + - ImagePolicyCreate().payloads is set to contain one payload that + contains an image policy (FOO) which does not exist on the controller. + - EpPolicyCreate endpoint response contains a 500 response. + + ### Test + - A sequence_number key is added to instance.results.response_current + - instance.results.diff_current is set to a dict with only + the key "sequence_number", since no changes were made. + - instance.results.failed set() contains True and does not contain False. + - instance.results.changed set() contains False and does not contain True. + - instance.results.metadata contains one dict. + - The value of instance.results.metadata "action" is "create". + - The value of instance.results.metadata "state" is "merged". + - The value of instance.results.metadata "sequence_number" is 1. + """ + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_create(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_create(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.unit_test = True + + with does_not_raise(): + instance = image_policy_create + instance.results = Results() + instance.rest_send = rest_send + instance.params = params + instance.payload = gen_payloads.next + instance.commit() + + response_current = responses_ep_policy_create(key) + response_current["sequence_number"] = 1 + assert instance.results.response_current == response_current + assert instance.results.diff_current == {"sequence_number": 1} + assert True in instance.results.failed + assert False not in instance.results.failed + assert True not in instance.results.changed + assert False in instance.results.changed + assert len(instance.results.metadata) == 1 + assert len(instance.results.diff) == 1 + assert instance.results.diff[0] == {"sequence_number": 1} + assert instance.results.metadata[0]["action"] == "create" + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[0]["sequence_number"] == 1 diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py index 27ea40445..1a77217a7 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py @@ -62,7 +62,7 @@ def test_image_policy_create_bulk_00000(image_policy_create_bulk) -> None: Test - Class attributes are initialized to expected values - - fail_json is not called + - Exceptions are not raised. """ with does_not_raise(): instance = image_policy_create_bulk @@ -96,7 +96,7 @@ def test_image_policy_create_bulk_00010(image_policy_create_bulk) -> None: ### Test - payloads is set to expected value - - fail_json is not called + - Exceptions are not raised. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -156,8 +156,8 @@ def test_image_policy_create_bulk_00022(image_policy_create_bulk, key, match) -> - __init__() ### Summary - Verify that the payloads setter calls fail_json when a payload in the payloads list - is missing a mandatory key. + Verify that the payloads setter raises ``ValueError`` when a payload in + the payloads list is missing a mandatory key. ### Test - ``ValueError`` is raised because a payload in the payloads list is @@ -179,7 +179,7 @@ def test_image_policy_create_bulk_00030(monkeypatch, image_policy_create_bulk) - - ImagePolicyCreateCommon - __init__() - payloads setter - - _build_payloads_to_commit() + - build_payloads_to_commit() - ImagePolicyCreateBulk - __init__() @@ -188,7 +188,7 @@ def test_image_policy_create_bulk_00030(monkeypatch, image_policy_create_bulk) - image policy that already exists on the controller. ### Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), + - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyCreateCommon().payloads is set to contain one payload (KR5M) @@ -205,7 +205,7 @@ def test_image_policy_create_bulk_00030(monkeypatch, image_policy_create_bulk) - instance.results = Results() instance.payloads = payloads_image_policy_create_bulk(key) monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() + instance.build_payloads_to_commit() assert instance._payloads_to_commit == [] assert len(instance.results.failed) == 0 assert len(instance.results.changed) == 0 @@ -217,17 +217,17 @@ def test_image_policy_create_bulk_00031(monkeypatch, image_policy_create_bulk) - - ImagePolicyCreateCommon - __init__() - payloads setter - - _build_payloads_to_commit() + - build_payloads_to_commit() - ImagePolicyCreateBulk - __init__() ### Summary - Verify that instance._build_payloads_to_commit() adds a payload to the + Verify that instance.build_payloads_to_commit() adds a payload to the payloads_to_commit list when a request is made to create an image policy that does not exist on the controller. ### Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), + - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyCreateCommon().payloads is set to contain one payload containing @@ -245,7 +245,7 @@ def test_image_policy_create_bulk_00031(monkeypatch, image_policy_create_bulk) - instance.results = Results() instance.payloads = payloads_image_policy_create_bulk(key) monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() + instance.build_payloads_to_commit() assert len(instance._payloads_to_commit) == 1 assert instance._payloads_to_commit == payloads_image_policy_create_bulk(key) @@ -255,12 +255,12 @@ def test_image_policy_create_bulk_00032(monkeypatch, image_policy_create_bulk) - ### Classes and Methods - ImagePolicyCreateCommon - payloads setter - - _build_payloads_to_commit() + - build_payloads_to_commit() - ImagePolicyCreateBulk - __init__() ### Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), + - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyCreateCommon().payloads is set to contain one payload containing @@ -278,7 +278,7 @@ def test_image_policy_create_bulk_00032(monkeypatch, image_policy_create_bulk) - instance = image_policy_create_bulk instance.payloads = payloads_image_policy_create_bulk(key) monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() + instance.build_payloads_to_commit() assert len(instance._payloads_to_commit) == 1 assert instance._payloads_to_commit[0]["policyName"] == "FOO" @@ -288,26 +288,24 @@ def test_image_policy_create_bulk_00033(image_policy_create_bulk) -> None: ### Classes and Methods - ImagePolicyCreateBulk - commit() - - fail_json ### Summary Verify that ImagePolicyCreateBulk.commit() raises ``ValueError`` when payloads is None. ### Setup - - ImagePolicyCreateCommon().payloads is not set + - ImagePolicyCreateCommon().payloads is not set. ### Test - - ValueError is called because payloads is None + - ValueError is called because payloads is None. """ with does_not_raise(): results = Results() instance = image_policy_create_bulk instance.results = results - match = ( - "ImagePolicyCreateBulk.commit: payloads must be set prior to calling commit." - ) + match = r"ImagePolicyCreateBulk\.commit:\s+" + match += r"payloads must be set prior to calling commit\." with pytest.raises(ValueError, match=match): instance.commit() @@ -350,8 +348,8 @@ def test_image_policy_create_bulk_00035(image_policy_create_bulk) -> None: """ ### Classes and Methods - ImagePolicyCreateCommon - - _build_payloads_to_commit() - - _send_payloads() + - build_payloads_to_commit() + - send_payloads() - ImagePolicyCreateBulk - payloads setter - commit() @@ -368,8 +366,8 @@ def test_image_policy_create_bulk_00035(image_policy_create_bulk) -> None: - EpPolicyCreate endpoint response contains a 200 response. ### Test - - commit calls _build_payloads_to_commit which returns one payload. - - commit calls _send_payloads, which calls rest_send, which populates + - commit calls build_payloads_to_commit which returns one payload. + - commit calls send_payloads, which calls rest_send, which populates diff_current with the payload due to result_current indicating success. - results.result_current is set to the expected value @@ -384,11 +382,16 @@ def responses(): yield responses_ep_policies(key) yield responses_ep_policy_create(key) - gen = ResponseGenerator(responses()) + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_create_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) sender = Sender() sender.ansible_module = MockAnsibleModule() - sender.gen = gen + sender.gen = gen_responses rest_send = RestSend(params) rest_send.response_handler = ResponseHandler() rest_send.sender = sender @@ -400,7 +403,7 @@ def responses(): instance.params = params with does_not_raise(): - instance.payloads = payloads_image_policy_create_bulk(key) + instance.payloads = gen_payloads.next instance.commit() response_current = responses_ep_policy_create(key) @@ -427,13 +430,13 @@ def responses(): assert instance.results.metadata[0]["sequence_number"] == 1 -def test_image_policy_create_bulk_00036(monkeypatch, image_policy_create_bulk) -> None: +def test_image_policy_create_bulk_00036(image_policy_create_bulk) -> None: """ ### Classes and Methods - ImagePolicyCreateCommon - payloads setter - - _build_payloads_to_commit() - - _send_payloads() + - build_payloads_to_commit() + - send_payloads() - ImagePolicyCreateBulk - commit() @@ -466,11 +469,16 @@ def responses(): yield responses_ep_policies(key) yield responses_ep_policy_create(key) - gen = ResponseGenerator(responses()) + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_create_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) sender = Sender() sender.ansible_module = MockAnsibleModule() - sender.gen = gen + sender.gen = gen_responses rest_send = RestSend(params) rest_send.response_handler = ResponseHandler() rest_send.sender = sender @@ -481,7 +489,7 @@ def responses(): instance.results = Results() instance.rest_send = rest_send instance.params = params - instance.payloads = payloads_image_policy_create_bulk(key) + instance.payloads = gen_payloads.next instance.commit() response_current = responses_ep_policy_create(key) @@ -500,7 +508,7 @@ def responses(): assert instance.results.metadata[0]["sequence_number"] == 1 -def test_image_policy_create_bulk_00037(monkeypatch, image_policy_create_bulk) -> None: +def test_image_policy_create_bulk_00037(image_policy_create_bulk) -> None: """ ### Classes and Methods - ImagePolicyCreateCommon @@ -534,11 +542,16 @@ def responses(): yield responses_ep_policy_create(key_ok) yield responses_ep_policy_create(key_nok) - gen = ResponseGenerator(responses()) + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_create_bulk(key_payloads) + + gen_payloads = ResponseGenerator(payloads()) sender = Sender() sender.ansible_module = MockAnsibleModule() - sender.gen = gen + sender.gen = gen_responses rest_send = RestSend(params) rest_send.response_handler = ResponseHandler() rest_send.sender = sender @@ -548,7 +561,7 @@ def responses(): instance = image_policy_create_bulk instance.results = Results() instance.rest_send = rest_send - instance.payloads = payloads_image_policy_create_bulk(key_payloads) + instance.payloads = gen_payloads.next instance.commit() assert len(instance.results.diff) == 2 diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py index 910ac4081..461d61f29 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py @@ -23,8 +23,6 @@ import pytest from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ AnsibleFailJson -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.common import \ - ImagePolicyCommon from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.create import ( ImagePolicyCreate, ImagePolicyCreateBulk) from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.delete import \ @@ -228,17 +226,17 @@ def results(self, value): @pytest.fixture(name="image_policy_create") def image_policy_create_fixture(): """ - mock ImagePolicyCreate + Return ImagePolicyCreate with params set. """ - instance = MockAnsibleModule() - instance.state = "merged" - return ImagePolicyCreate(instance) + instance = ImagePolicyCreate() + instance.params = params + return instance @pytest.fixture(name="image_policy_create_bulk") def image_policy_create_bulk_fixture(): """ - mock ImagePolicyCreateBulk + Return ImagePolicyCreateBulk with params set. """ instance = ImagePolicyCreateBulk() instance.params = params From c2c2aea0a8cd6228d89fb90f5900df40673b723f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 29 Jun 2024 10:48:28 -1000 Subject: [PATCH 210/230] Merged(): FIxed unintentional deletion of parameter values. 1. dcnm_image_policy.py Merged() Fixed issue where parameter values for optional parameters were getting deleted if the parameter was not present in the playbook config. 2. Update integration tests. - Add query to further verify results. - dcnm_image_policy_replaced.yaml - dcnm_image_policy_overridden.yaml - Add test to verify editing image policy - dcnm_image_policy_merged.yaml --- plugins/modules/dcnm_image_policy.py | 46 ++-- .../tests/dcnm_image_policy_merged.yaml | 67 ++++- .../tests/dcnm_image_policy_overridden.yaml | 125 ++++++++- .../tests/dcnm_image_policy_replaced.yaml | 244 +++++++++++++++++- 4 files changed, 447 insertions(+), 35 deletions(-) diff --git a/plugins/modules/dcnm_image_policy.py b/plugins/modules/dcnm_image_policy.py index 8ed8e3163..72e330df6 100644 --- a/plugins/modules/dcnm_image_policy.py +++ b/plugins/modules/dcnm_image_policy.py @@ -261,12 +261,6 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import \ Log -# from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts import \ -# MergeDicts -# from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults import \ -# ParamsMergeDefaults -# from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_validate import \ -# ParamsValidate from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.params_merge_defaults_v2 import \ @@ -362,6 +356,7 @@ def __init__(self, params): self._rest_send = None + self.have = None self.validated = [] self.want = [] @@ -390,7 +385,7 @@ def get_have(self) -> None: self.log.debug(msg) self.have = ImagePolicies() self.have.results = self.results - self.have.rest_send = self.rest_send + self.have.rest_send = self.rest_send # pylint: disable=no-member self.have.refresh() def get_want(self) -> None: @@ -444,6 +439,7 @@ class Replaced(Common): def __init__(self, params): self.class_name = self.__class__.__name__ + method_name = inspect.stack()[0][3] try: super().__init__(params) except (TypeError, ValueError) as error: @@ -451,7 +447,6 @@ def __init__(self, params): msg += "Error during super().__init__(). " msg += f"Error detail: {error}" raise ValueError(msg) from error - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.log = logging.getLogger(f"dcnm.{self.class_name}") self.replace = ImagePolicyReplaceBulk() @@ -485,6 +480,7 @@ class Deleted(Common): def __init__(self, params): self.class_name = self.__class__.__name__ + method_name = inspect.stack()[0][3] try: super().__init__(params) except (TypeError, ValueError) as error: @@ -492,7 +488,6 @@ def __init__(self, params): msg += "Error during super().__init__(). " msg += f"Error detail: {error}" raise ValueError(msg) from error - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -605,6 +600,7 @@ class Overridden(Common): def __init__(self, params): self.class_name = self.__class__.__name__ + method_name = inspect.stack()[0][3] try: super().__init__(params) except (TypeError, ValueError) as error: @@ -612,11 +608,11 @@ def __init__(self, params): msg += "Error during super().__init__(). " msg += f"Error detail: {error}" raise ValueError(msg) from error - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.log = logging.getLogger(f"dcnm.{self.class_name}") self.delete = ImagePolicyDelete() + self.merged = Merged(params) msg = "ENTERED Overridden(): " msg += f"state: {self.state}, " @@ -629,7 +625,7 @@ def commit(self) -> None: - Delete all policies on the controller that are not in self.want - Instantiate`` Merged()`` and call ``Merged().commit()`` """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] self.results.state = self.state self.results.check_mode = self.check_mode @@ -646,17 +642,20 @@ def commit(self) -> None: self.log.debug(msg) self._delete_policies_not_in_want() - task = Merged(self.params) - task.rest_send = self.rest_send - task.results = self.results - task.commit() + #task = Merged(self.params) + # pylint: disable=attribute-defined-outside-init + self.merged.rest_send = ( + self.rest_send + ) + self.merged.results = self.results + self.merged.commit() def _delete_policies_not_in_want(self) -> None: """ ### Summary Delete all policies on the controller that are not in self.want """ - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + method_name = inspect.stack()[0][3] want_policy_names = set() for want in self.want: want_policy_names.add(want["policyName"]) @@ -698,6 +697,7 @@ class Merged(Common): def __init__(self, params): self.class_name = self.__class__.__name__ + method_name = inspect.stack()[0][3] try: super().__init__(params) except (TypeError, ValueError) as error: @@ -705,7 +705,6 @@ def __init__(self, params): msg += "Error during super().__init__(). " msg += f"Error detail: {error}" raise ValueError(msg) from error - method_name = inspect.stack()[0][3] # pylint: disable=unused-variable self.log = logging.getLogger(f"dcnm.{self.class_name}") @@ -791,19 +790,10 @@ def _prepare_for_merge(self, have: Dict, want: Dict): for key in ["imageName", "ref_count", "platformPolicies"]: have.pop(key, None) - # Change "N9K/N3K" to "N9K" in have to match the request payload. + # Change "N9K/N3K" to "N9K" in "have" to match the request payload. if have.get("platform", None) == "N9K/N3K": have["platform"] = "N9K" - # If keys are not set in both have and want, remove them. - for key in ["agnostic", "epldImgName", "packageName", "rpmimages"]: - if have.get(key, None) is None and want.get(key, None) is None: - have.pop(key, None) - want.pop(key, None) - - if have.get(key, None) == "" and want.get(key, None) == "": - have.pop(key, None) - want.pop(key, None) return (have, want) def _merge_policies(self, have: dict, want: dict) -> dict: @@ -823,7 +813,7 @@ def _merge_policies(self, have: dict, want: dict) -> dict: merge.commit() except (TypeError, ValueError) as error: msg = f"{self.class_name}.{method_name}: " - msg += f"Error during MergeDicts(). " + msg += "Error during MergeDicts(). " msg += f"Error detail: {error}" raise ValueError(msg) from error merged = copy.deepcopy(merge.dict_merged) diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml index eeca77e1d..65381e0c6 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_merged.yaml @@ -24,8 +24,9 @@ # 4. Create image policies using merged state and verify result # - image_policy_1 # - image_policy_2 +# 5. Edit image_policy_1 using merged state and verify result # CLEANUP -# 7. Delete the image policies created in the test +# 6. Delete the image policies created in the test # ################################################################################ # REQUIREMENTS @@ -227,6 +228,70 @@ - result.result[1].sequence_number == 2 +- name: MERGED - TEST - Edit the two image policies + cisco.dcnm.dcnm_image_policy: + state: merged + config: + - name: "{{ image_policy_1 }}" + description: "{{ image_policy_1 }} edited" + epld_image: "{{ epld_image_2 }}" + platform: N9K + release: "{{ nxos_release_1 }}" + - name: "{{ image_policy_2 }}" + description: "{{ image_policy_2 }} edited" + epld_image: "" + platform: N9K + release: "{{ nxos_release_2 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 2 + - result.diff[0].policyName == image_policy_1 + - result.diff[1].policyName == image_policy_2 + - result.diff[0].policyDescr == image_policy_1 + " edited" + - result.diff[1].policyDescr == image_policy_2 + " edited" + - result.diff[0].agnostic == false + - result.diff[1].agnostic == false + - result.diff[0].epldImgName == epld_image_2 + - result.diff[1].epldImgName == "" + - result.diff[0].nxosVersion == nxos_release_1 + - result.diff[1].nxosVersion == nxos_release_2 + - result.diff[0].platform == "N9K" + - result.diff[1].platform == "N9K" + - result.diff[0].policyType == "PLATFORM" + - result.diff[1].policyType == "PLATFORM" + - (result.metadata | length) == 2 + - result.metadata[0].action == "update" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - result.metadata[1].action == "update" + - result.metadata[1].check_mode == False + - result.metadata[1].sequence_number == 2 + - result.metadata[1].state == "merged" + - (result.response | length) == 2 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "POST" + - result.response[0].RETURN_CODE == 200 + - result.response[0].DATA == "Policy updated successfully." + - result.response[1].MESSAGE == "OK" + - result.response[1].METHOD == "POST" + - result.response[1].RETURN_CODE == 200 + - result.response[1].DATA == "Policy updated successfully." + - (result.result | length) == 2 + - result.result[0].changed == true + - result.result[0].success == true + - result.result[0].sequence_number == 1 + - result.result[1].changed == true + - result.result[1].success == true + - result.result[1].sequence_number == 2 + ################################################################################ # MERGED - CLEANUP - Delete image policies ################################################################################ diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml index 6c633853f..0c77fd3e4 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_overridden.yaml @@ -3,9 +3,8 @@ ################################################################################ # # Recent run times (MM:SS.ms): -# 00:20.978 -# 00:22.217 -# 00:21.880 +# 00:22.116 +# 00:21.854 # ################################################################################ # STEPS @@ -368,6 +367,126 @@ - result.result[1].sequence_number == 2 - result.result[1].success == true +################################################################################ +# OVERRIDDEN - TEST - query image policies and verify results +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "agnostic": false, +# "epldImgName": "n9000-epld.10.2.5.M.img", +# "fabricPolicyName": null, +# "imageName": "nxos64-cs.10.2.5.M.bin", +# "imagePresent": "Present", +# "nxosVersion": "10.2.5_nxos64-cs_64bit", +# "packageName": "", +# "platform": "N9K/N3K", +# "platformPolicies": "", +# "policyDescr": "KR5M overridden", +# "policyName": "KR5M", +# "policyType": "PLATFORM", +# "ref_count": 0, +# "role": null, +# "rpmimages": null, +# "sequence_number": 1, +# "unInstall": false +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "query", +# "check_mode": false, +# "sequence_number": 1, +# "state": "query" +# } +# ], +# "response": [ +# { +# "DATA": { +# "lastOperDataObject": [ +# { +# "agnostic": false, +# "epldImgName": "n9000-epld.10.2.5.M.img", +# "fabricPolicyName": null, +# "imageName": "nxos64-cs.10.2.5.M.bin", +# "imagePresent": "Present", +# "nxosVersion": "10.2.5_nxos64-cs_64bit", +# "packageName": "", +# "platform": "N9K/N3K", +# "platformPolicies": "", +# "policyDescr": "KR5M overridden", +# "policyName": "KR5M", +# "policyType": "PLATFORM", +# "ref_count": 0, +# "role": null, +# "rpmimages": null, +# "unInstall": false +# } +# ], +# "message": "", +# "status": "SUCCESS" +# }, +# "MESSAGE": "OK", +# "METHOD": "GET", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "found": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +################################################################################ + +- name: OVERRIDDEN - TEST - query image policies and verify results + cisco.dcnm.dcnm_image_policy: + state: query + config: + - name: "{{ image_policy_1 }}" + - name: "{{ image_policy_2 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == false + - result.failed == false + - (result.diff | length) == 1 + - result.diff[0].agnostic == false + - result.diff[0].policyName == image_policy_1 + - result.diff[0].policyDescr == image_policy_1 + " overridden" + - result.diff[0].epldImgName == epld_image_1 + - result.diff[0].nxosVersion == nxos_release_1 + - result.diff[0].platform == "N9K/N3K" + - result.diff[0].policyType == "PLATFORM" + - result.diff[0].ref_count == 0 + - result.diff[0].sequence_number == 1 + - (result.metadata | length) == 1 + - result.metadata[0].action == "query" + - result.metadata[0].check_mode == false + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "query" + - (result.response | length) == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "GET" + - result.response[0].RETURN_CODE == 200 + - (result.result | length) == 1 + - result.result[0].found == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true + ################################################################################ # OVERRIDDEN - CLEANUP - Delete image policies and verify ################################################################################ diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml index a15cce612..0298dc724 100644 --- a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_replaced.yaml @@ -3,8 +3,8 @@ ################################################################################ # # Recent run times (MM:SS.ms): -# 00:17.898 -# 00:17.676 +# 00:19.415 +# 00:19.667 # ################################################################################ # STEPS @@ -29,11 +29,14 @@ # # 5. Use replaced state to update image_policy_1 and verify that: # - image_policy_1 is updated +# +# 6. query image policies and verify results +# - image_policy_1 is updated # - image_policy_2 is untouched # # CLEANUP # -# 6. Delete the image policies created in the test +# 7. Delete the image policies created in the test # ################################################################################ # REQUIREMENTS @@ -329,6 +332,240 @@ - result.result[0].sequence_number == 1 - result.result[0].success == true +################################################################################ +# REPLACED - TEST - query image policies and verify results +################################################################################ +# Expected result +# ok: [172.22.150.244] => { +# "result": { +# "changed": false, +# "diff": [ +# { +# "agnostic": false, +# "epldImgName": "n9000-epld.10.2.5.M.img", +# "fabricPolicyName": null, +# "imageName": "nxos64-cs.10.2.5.M.bin", +# "imagePresent": "Present", +# "nxosVersion": "10.2.5_nxos64-cs_64bit", +# "packageName": "", +# "platform": "N9K/N3K", +# "platformPolicies": "", +# "policyDescr": "KR5M replaced", +# "policyName": "KR5M", +# "policyType": "PLATFORM", +# "ref_count": 0, +# "role": null, +# "rpmimages": "", +# "sequence_number": 1, +# "unInstall": false +# }, +# { +# "agnostic": false, +# "epldImgName": "n9000-epld.10.3.1.F.img", +# "fabricPolicyName": null, +# "imageName": "nxos64-cs.10.3.1.F.bin", +# "imagePresent": "Present", +# "nxosVersion": "10.3.1_nxos64-cs_64bit", +# "packageName": "", +# "platform": "N9K/N3K", +# "platformPolicies": "", +# "policyDescr": "NR1F", +# "policyName": "NR1F", +# "policyType": "PLATFORM", +# "ref_count": 0, +# "role": null, +# "rpmimages": null, +# "sequence_number": 2, +# "unInstall": false +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "query", +# "check_mode": false, +# "sequence_number": 1, +# "state": "query" +# }, +# { +# "action": "query", +# "check_mode": false, +# "sequence_number": 2, +# "state": "query" +# } +# ], +# "response": [ +# { +# "DATA": { +# "lastOperDataObject": [ +# { +# "agnostic": false, +# "epldImgName": "n9000-epld.10.2.5.M.img", +# "fabricPolicyName": null, +# "imageName": "nxos64-cs.10.2.5.M.bin", +# "imagePresent": "Present", +# "nxosVersion": "10.2.5_nxos64-cs_64bit", +# "packageName": "", +# "platform": "N9K/N3K", +# "platformPolicies": "", +# "policyDescr": "KR5M replaced", +# "policyName": "KR5M", +# "policyType": "PLATFORM", +# "ref_count": 0, +# "role": null, +# "rpmimages": "", +# "unInstall": false +# }, +# { +# "agnostic": false, +# "epldImgName": "n9000-epld.10.3.1.F.img", +# "fabricPolicyName": null, +# "imageName": "nxos64-cs.10.3.1.F.bin", +# "imagePresent": "Present", +# "nxosVersion": "10.3.1_nxos64-cs_64bit", +# "packageName": "", +# "platform": "N9K/N3K", +# "platformPolicies": "", +# "policyDescr": "NR1F", +# "policyName": "NR1F", +# "policyType": "PLATFORM", +# "ref_count": 0, +# "role": null, +# "rpmimages": null, +# "unInstall": false +# } +# ], +# "message": "", +# "status": "SUCCESS" +# }, +# "MESSAGE": "OK", +# "METHOD": "GET", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# }, +# { +# "DATA": { +# "lastOperDataObject": [ +# { +# "agnostic": false, +# "epldImgName": "n9000-epld.10.2.5.M.img", +# "fabricPolicyName": null, +# "imageName": "nxos64-cs.10.2.5.M.bin", +# "imagePresent": "Present", +# "nxosVersion": "10.2.5_nxos64-cs_64bit", +# "packageName": "", +# "platform": "N9K/N3K", +# "platformPolicies": "", +# "policyDescr": "KR5M replaced", +# "policyName": "KR5M", +# "policyType": "PLATFORM", +# "ref_count": 0, +# "role": null, +# "rpmimages": "", +# "unInstall": false +# }, +# { +# "agnostic": false, +# "epldImgName": "n9000-epld.10.3.1.F.img", +# "fabricPolicyName": null, +# "imageName": "nxos64-cs.10.3.1.F.bin", +# "imagePresent": "Present", +# "nxosVersion": "10.3.1_nxos64-cs_64bit", +# "packageName": "", +# "platform": "N9K/N3K", +# "platformPolicies": "", +# "policyDescr": "NR1F", +# "policyName": "NR1F", +# "policyType": "PLATFORM", +# "ref_count": 0, +# "role": null, +# "rpmimages": null, +# "unInstall": false +# } +# ], +# "message": "", +# "status": "SUCCESS" +# }, +# "MESSAGE": "OK", +# "METHOD": "GET", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", +# "RETURN_CODE": 200, +# "sequence_number": 2 +# } +# ], +# "result": [ +# { +# "found": true, +# "sequence_number": 1, +# "success": true +# }, +# { +# "found": true, +# "sequence_number": 2, +# "success": true +# } +# ] +# } +# } +################################################################################ + +- name: REPLACED - TEST - query image policies and verify results + cisco.dcnm.dcnm_image_policy: + state: query + config: + - name: "{{ image_policy_1 }}" + - name: "{{ image_policy_2 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == false + - result.failed == false + - (result.diff | length) == 2 + - result.diff[0].agnostic == false + - result.diff[1].agnostic == false + - result.diff[0].policyName == image_policy_1 + - result.diff[1].policyName == image_policy_2 + - result.diff[0].policyDescr == image_policy_1 + " replaced" + - result.diff[1].policyDescr == image_policy_2 + - result.diff[0].epldImgName == epld_image_1 + - result.diff[0].nxosVersion == nxos_release_1 + - result.diff[0].platform == "N9K/N3K" + - result.diff[1].platform == "N9K/N3K" + - result.diff[0].policyType == "PLATFORM" + - result.diff[1].policyType == "PLATFORM" + - result.diff[0].ref_count == 0 + - result.diff[1].ref_count == 0 + - result.diff[0].sequence_number == 1 + - result.diff[1].sequence_number == 2 + - (result.metadata | length) == 2 + - result.metadata[0].action == "query" + - result.metadata[0].check_mode == false + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "query" + - result.metadata[1].action == "query" + - result.metadata[1].check_mode == false + - result.metadata[1].sequence_number == 2 + - result.metadata[1].state == "query" + - (result.response | length) == 2 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "GET" + - result.response[0].RETURN_CODE == 200 + - result.response[1].MESSAGE == "OK" + - result.response[1].METHOD == "GET" + - result.response[1].RETURN_CODE == 200 + - (result.result | length) == 2 + - result.result[0].found == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true + - result.result[1].found == true + - result.result[1].sequence_number == 2 + - result.result[1].success == true + ################################################################################ # REPLACED - CLEANUP - Delete image policies and verify ################################################################################ @@ -374,6 +611,7 @@ # } # } ################################################################################ + - name: REPLACED - CLEANUP - Delete image policies and verify cisco.dcnm.dcnm_image_policy: state: deleted From ac98e7ba699cfeb3231f4a9373348049b821ccf6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 29 Jun 2024 11:01:11 -1000 Subject: [PATCH 211/230] ImagePolicyUpdate: Align with v2 classes. 1. EpPolicyEdit() Add endpoint class. 2. update.py - Use EpPolicyEdit() - Remove ApiEndpoints() - make some methods public. --- .../rest/policymgnt/policymgnt.py | 113 +++++++++++++----- plugins/module_utils/image_policy/update.py | 35 +++--- 2 files changed, 97 insertions(+), 51 deletions(-) diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py index 84e411db9..371125ad9 100644 --- a/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py @@ -133,32 +133,36 @@ def verb(self): return "GET" -class EpPolicyDelete(PolicyMgnt): + + +class EpPolicyAttach(PolicyMgnt): """ - ## V1 API - PolicyMgnt().EpPolicyDelete() + ## V1 API - PolicyMgnt().EpPolicyAttach() ### Description - Delete image policies. + Return endpoint information. ### Raises - None ### Path - - ``/rest/policymgnt/policy`` + - ``/rest/policymgnt/attach-policy`` ### Verb - - DELETE + - POST - ### Notes - Expects a JSON payload as shown below, where ``policyNames`` is a - comma-separated list of policy names. + ### Parameters + - path: retrieve the path for the endpoint + - verb: retrieve the verb for the endpoint - ```json - { - "policyNames": "policyA,policyB,etc" - } + ### Usage + ```python + instance = EpPolicyAttach() + path = instance.path + verb = instance.verb ``` """ + def __init__(self): super().__init__() self.class_name = self.__class__.__name__ @@ -169,16 +173,16 @@ def __init__(self): @property def path(self): - return f"{self.policymgnt}/policy" + return f"{self.policymgnt}/attach-policy" @property def verb(self): - return "DELETE" + return "POST" -class EpPolicyAttach(PolicyMgnt): +class EpPolicyCreate(PolicyMgnt): """ - ## V1 API - PolicyMgnt().EpPolicyAttach() + ## V1 API - PolicyMgnt().EpPolicyCreate() ### Description Return endpoint information. @@ -187,7 +191,7 @@ class EpPolicyAttach(PolicyMgnt): - None ### Path - - ``/rest/policymgnt/attach-policy`` + - ``/rest/policymgnt/platform-policy`` ### Verb - POST @@ -198,7 +202,7 @@ class EpPolicyAttach(PolicyMgnt): ### Usage ```python - instance = EpPolicyAttach() + instance = EpPolicyCreate() path = instance.path verb = instance.verb ``` @@ -214,16 +218,59 @@ def __init__(self): @property def path(self): - return f"{self.policymgnt}/attach-policy" + return f"{self.policymgnt}/platform-policy" @property def verb(self): return "POST" -class EpPolicyCreate(PolicyMgnt): +class EpPolicyDelete(PolicyMgnt): """ - ## V1 API - PolicyMgnt().EpPolicyCreate() + ## V1 API - PolicyMgnt().EpPolicyDelete() + + ### Description + Delete image policies. + + ### Raises + - None + + ### Path + - ``/rest/policymgnt/policy`` + + ### Verb + - DELETE + + ### Notes + Expects a JSON payload as shown below, where ``policyNames`` is a + comma-separated list of policy names. + + ```json + { + "policyNames": "policyA,policyB,etc" + } + ``` + """ + def __init__(self): + super().__init__() + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") + msg = "ENTERED api.v1.imagemanagement.rest." + msg += f"policymgnt.{self.class_name}" + self.log.debug(msg) + + @property + def path(self): + return f"{self.policymgnt}/policy" + + @property + def verb(self): + return "DELETE" + + +class EpPolicyDetach(PolicyMgnt): + """ + ## V1 API - PolicyMgnt().EpPolicyDetach() ### Description Return endpoint information. @@ -232,10 +279,10 @@ class EpPolicyCreate(PolicyMgnt): - None ### Path - - ``/rest/policymgnt/platform-policy`` + - ``/rest/policymgnt/detach-policy`` ### Verb - - POST + - DELETE ### Parameters - path: retrieve the path for the endpoint @@ -243,7 +290,7 @@ class EpPolicyCreate(PolicyMgnt): ### Usage ```python - instance = EpPolicyCreate() + instance = EpPolicyDetach() path = instance.path verb = instance.verb ``` @@ -259,16 +306,16 @@ def __init__(self): @property def path(self): - return f"{self.policymgnt}/platform-policy" + return f"{self.policymgnt}/detach-policy" @property def verb(self): - return "POST" + return "DELETE" -class EpPolicyDetach(PolicyMgnt): +class EpPolicyEdit(PolicyMgnt): """ - ## V1 API - PolicyMgnt().EpPolicyDetach() + ## V1 API - PolicyMgnt().EpPolicyEdit() ### Description Return endpoint information. @@ -277,10 +324,10 @@ class EpPolicyDetach(PolicyMgnt): - None ### Path - - ``/rest/policymgnt/detach-policy`` + - ``/rest/policymgnt/edit-policy`` ### Verb - - DELETE + - POST ### Parameters - path: retrieve the path for the endpoint @@ -288,7 +335,7 @@ class EpPolicyDetach(PolicyMgnt): ### Usage ```python - instance = EpPolicyDetach() + instance = EpPolicyEdit() path = instance.path verb = instance.verb ``` @@ -304,11 +351,11 @@ def __init__(self): @property def path(self): - return f"{self.policymgnt}/detach-policy" + return f"{self.policymgnt}/edit-policy" @property def verb(self): - return "DELETE" + return "POST" class EpPolicyInfo(PolicyMgnt): diff --git a/plugins/module_utils/image_policy/update.py b/plugins/module_utils/image_policy/update.py index ae5a1afaa..7918b12f7 100644 --- a/plugins/module_utils/image_policy/update.py +++ b/plugins/module_utils/image_policy/update.py @@ -22,14 +22,14 @@ import json import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.policymgnt.policymgnt import \ + EpPolicyEdit from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ Properties from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ ImagePolicies @@ -53,10 +53,9 @@ def __init__(self): self._image_policies = ImagePolicies() self._image_policies.results = Results() - self.endpoints = ApiEndpoints() - - self.path = self.endpoints.policy_edit.get("path") - self.verb = self.endpoints.policy_edit.get("verb") + self.endpoint = EpPolicyEdit() + self.path = self.endpoint.path + self.verb = self.endpoint.verb self._payloads_to_commit = [] @@ -75,7 +74,7 @@ def __init__(self): msg += f"action: {self.action}, " self.log.debug(msg) - def _verify_payload(self, payload): + def verify_payload(self, payload): """ Verify that the payload is a dict and contains all mandatory keys """ @@ -99,7 +98,7 @@ def _verify_payload(self, payload): msg += f"{sorted(missing_keys)}" raise ValueError(msg) - def _build_payloads_to_commit(self): + def build_payloads_to_commit(self): """ Build a list of payloads to commit. Skip any payloads that do not exist on the controller. @@ -138,7 +137,7 @@ def _build_payloads_to_commit(self): merge.commit() updated_payload = copy.deepcopy(merge.dict_merged) except (TypeError, ValueError) as error: - msg = f"{self.class_name}._build_payloads_to_commit: " + msg = f"{self.class_name}.build_payloads_to_commit: " msg += "Error merging payload and policy. " msg += f"Error detail: {error}." raise ValueError(msg) from error @@ -191,7 +190,7 @@ def _verify_image_policy_ref_count(self, instance, policy_names): msg += f"ref_count: {ref_count}. " raise ValueError(msg) - def _send_payloads(self): + def send_payloads(self): """ If check_mode is False, send the payloads to the controller If check_mode is True, do not send the payloads to the controller @@ -203,10 +202,10 @@ def _send_payloads(self): ) for payload in self._payloads_to_commit: - self._send_payload(payload) + self.send_payload(payload) # pylint: disable=no-member - def _send_payload(self, payload): + def send_payload(self, payload): """ ### Summary Send one image policy update payload @@ -268,7 +267,7 @@ def payloads(self, value): msg += f"value {value}" raise TypeError(msg) for item in value: - self._verify_payload(item) + self.verify_payload(item) self._payloads = value @@ -366,10 +365,10 @@ def commit(self): msg += f"rest_send must be set prior to calling {method_name}." raise ValueError(msg) - self._build_payloads_to_commit() + self.build_payloads_to_commit() if len(self._payloads_to_commit) == 0: return - self._send_payloads() + self.send_payloads() class ImagePolicyUpdate(ImagePolicyUpdateCommon): @@ -428,7 +427,7 @@ def payload(self): @payload.setter def payload(self, value): - self._verify_payload(value) + self.verify_payload(value) self._payload = value # ImagePolicyUpdateCommon expects a list of payloads self._payloads = [value] @@ -452,8 +451,8 @@ def commit(self): msg += f"rest_send must be set prior to calling {method_name}." raise ValueError(msg) - self._build_payloads_to_commit() + self.build_payloads_to_commit() if len(self._payloads_to_commit) == 0: return - self._send_payloads() + self.send_payloads() From 5e221bbfe8d384cd16eebf079d8fa8620e508f2e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sat, 29 Jun 2024 19:10:15 -1000 Subject: [PATCH 212/230] ImagePolicyUpdate: Update unit tests 1. ImagePolicyUpdate: Update unit tests to reflect changes in the last commit. 2. ImagePolicyUpdateBulk: Remove unit test test_image_policy_update_bulk_00040. This test was relevant only for replaced state and is covered there. 3. test_image_policy_create_bulk.py: Update docstrings. 4. Remove fixture data from all_policies_ImagePolicies.json that now lives in responses_EpPolicies.json. 5. update.py: Remove trailing space from error message. 6. image_policies.py: Fix access to unassigned vars in error messages. Run through linters. --- .../image_policy/image_policies.py | 16 +- plugins/module_utils/image_policy/update.py | 2 +- .../fixtures/all_policies_ImagePolicies.json | 123 ----- .../fixtures/payloads_ImagePolicyUpdate.json | 2 +- .../fixtures/responses_EpPolicies.json | 218 ++++++++ .../fixtures/responses_EpPolicyEdit.json | 25 + .../test_image_policy_create_bulk.py | 22 +- .../test_image_policy_update.py | 465 +++++++++++------- .../test_image_policy_update_bulk.py | 25 - .../modules/dcnm/dcnm_image_policy/utils.py | 27 +- 10 files changed, 568 insertions(+), 357 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json diff --git a/plugins/module_utils/image_policy/image_policies.py b/plugins/module_utils/image_policy/image_policies.py index 6bd51768f..bae0f7e17 100644 --- a/plugins/module_utils/image_policy/image_policies.py +++ b/plugins/module_utils/image_policy/image_policies.py @@ -100,12 +100,12 @@ def refresh(self): - ``rest_send`` is not set. - ``results`` is not set. - The controller response cannot be parsed. - + ### Notes - pylint: disable=no-member is needed because the rest_send, results, and params properties are dynamically created by the @Properties class decorators. - """ + """ method_name = inspect.stack()[0][3] if self.rest_send is None: @@ -134,7 +134,7 @@ def refresh(self): data = self.rest_send.response_current.get("DATA", {}).get("lastOperDataObject") if data is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Bad response when retrieving image policy " msg += "information from the controller." raise ControllerResponseError(msg) @@ -150,15 +150,13 @@ def refresh(self): for policy in data: policy_name = policy.get("policyName") if policy_name is None: - msg = f"{self.class_name}.{self.method_name}: " + msg = f"{self.class_name}.{method_name}: " msg += "Cannot parse policy information from the controller." raise ValueError(msg) self.data[policy_name] = policy self._response_data[policy_name] = policy - self._all_policies = copy.deepcopy( - self._response_data - ) + self._all_policies = copy.deepcopy(self._response_data) self.results.response_current = self.rest_send.response_current self.results.result_current = self.rest_send.result_current @@ -191,9 +189,7 @@ def _get(self, item): raise ValueError(msg) return self.conversion.make_boolean( - self.conversion.make_none( - self._response_data[self.policy_name][item] - ) + self.conversion.make_none(self._response_data[self.policy_name][item]) ) @property diff --git a/plugins/module_utils/image_policy/update.py b/plugins/module_utils/image_policy/update.py index 7918b12f7..4d2f28181 100644 --- a/plugins/module_utils/image_policy/update.py +++ b/plugins/module_utils/image_policy/update.py @@ -184,7 +184,7 @@ def _verify_image_policy_ref_count(self, instance, policy_names): msg = f"{self.class_name}.{method_name}: " msg += "One or more policies have devices attached. " msg += "Detach these policies from all devices first using " - msg += "the dcnm_image_upgrade module, with state == deleted. " + msg += "the dcnm_image_upgrade module with state == deleted." for policy_name, ref_count in _non_zero_ref_counts.items(): msg += f"policy_name: {policy_name}, " msg += f"ref_count: {ref_count}. " diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/all_policies_ImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/all_policies_ImagePolicies.json index 7bf1bdddc..83ff30d95 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/all_policies_ImagePolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/all_policies_ImagePolicies.json @@ -571,129 +571,6 @@ "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" } }, - "test_image_policy_update_00030a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_update_00031a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_update_00034a": {}, - "test_image_policy_update_00035a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_update_00036a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - }, - "test_image_policy_update_00050a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 2, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - }, "test_image_policy_update_bulk_00030a": { "KR5M": { "agnostic": false, diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdate.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdate.json index 8472be256..b4c33c794 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdate.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdate.json @@ -3,7 +3,7 @@ "Mocked payloads for ImagePolicyUpdateBulk unit tests.", "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py" ], - "test_image_policy_update_00020a":{ + "test_image_policy_update_00010a":{ "agnostic": false, "epldImgName": "n9000-epld.10.3.2.F.img", "nxosVersion": "10.3.1_nxos64-cs_64bit", diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json index b0b18e5e5..3bb4e6598 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json @@ -106,5 +106,223 @@ ], "message": "" } + }, + "test_image_policy_update_00030a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_update_00031a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_update_00034a": { + "TEST_NOTES": [ + "No image policies exist on the controller." + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "test_image_policy_update_00035a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_update_00036a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_update_00050a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 2, + "imagePresent": "Present" + } + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json new file mode 100644 index 000000000..d08e0d974 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json @@ -0,0 +1,25 @@ +{ + "TEST_NOTES": [ + "Mocked responses for endpoint EpPolicyEdit.", + "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py" + ], + "test_image_policy_update_00030a": { + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_update_00035a": { + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_update_00036a": { + "DATA": "Internal server error.", + "MESSAGE": "NOK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 500 + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py index 1a77217a7..7b055c0ba 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py @@ -259,18 +259,24 @@ def test_image_policy_create_bulk_00032(monkeypatch, image_policy_create_bulk) - - ImagePolicyCreateBulk - __init__() + ### Summary + Verify that instance.build_payloads_to_commit() adds a payload to the + payloads_to_commit list when the image policy in the payload does not + on the controller. + ### Setup - - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), - is mocked to indicate that two image policies (KR5M, NR3F) exist on the - controller. - - ImagePolicyCreateCommon().payloads is set to contain one payload containing - an image policy (FOO) that is not present in all_policies and one payload - containing an image policy (KR5M) that does exist on the controller. + - ImagePolicies().all_policies, called from + instance.build_payloads_to_commit(), is mocked to indicate that two + image policies (KR5M, NR3F) exist on the controller. + - ImagePolicyCreateCommon().payloads is set to contain one payload + containing an image policy (FOO) that is not present in all_policies + and one payload containing an image policy (KR5M) that does exist on + the controller. ### Test - _payloads_to_commit will contain one payload - - The policyName for this payload will be "FOO", which is the image policy that - does not exist on the controller + - The policyName for this payload will be "FOO", which is the image + policy that does not exist on the controller """ method_name = inspect.stack()[0][3] key = f"{method_name}a" diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py index bd2548b43..fa2563fe8 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py @@ -29,22 +29,28 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -from typing import Any, Dict +import copy +import inspect import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - MockImagePolicies, does_not_raise, image_policy_update_fixture, - payloads_image_policy_update, responses_image_policy_update, - rest_send_result_current, results_image_policy_update) + MockAnsibleModule, does_not_raise, image_policy_update_fixture, params, + payloads_image_policy_update, responses_ep_policies, + responses_ep_policy_edit, responses_image_policy_update, + rest_send_result_current) -def test_image_policy_update_00010(image_policy_update) -> None: +def test_image_policy_update_00000(image_policy_update) -> None: """ Classes and Methods - ImagePolicyUpdate @@ -55,17 +61,16 @@ def test_image_policy_update_00010(image_policy_update) -> None: Test - Class attributes initialized to expected values - - fail_json is not called + - Exceptions are not raised. """ with does_not_raise(): instance = image_policy_update assert instance.class_name == "ImagePolicyUpdate" assert instance.action == "update" - assert instance.state == "merged" - assert instance.check_mode is False - assert isinstance(instance.endpoints, ApiEndpoints) - assert instance.path == ApiEndpoints().policy_edit["path"] - assert instance.verb == ApiEndpoints().policy_edit["verb"] + assert instance.params.get("state") == "merged" + assert instance.params.get("check_mode") is False + assert instance.endpoint.class_name == "EpPolicyEdit" + assert instance.endpoint.verb == "POST" assert instance._mandatory_payload_keys == { "nxosVersion", "policyName", @@ -75,22 +80,23 @@ def test_image_policy_update_00010(image_policy_update) -> None: assert instance._payloads_to_commit == [] -def test_image_policy_update_00020(image_policy_update) -> None: +def test_image_policy_update_00010(image_policy_update) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdate - __init__ - payload setter - Summary + ### Summary Verify that the payload setter sets the payload attribute to the expected value. - Test + ### Test - payload is set to expected value - - fail_json is not called + - Exceptions are not raised. """ - key = "test_image_policy_update_00020a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_update @@ -100,29 +106,33 @@ def test_image_policy_update_00020(image_policy_update) -> None: def test_image_policy_update_00021(image_policy_update) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdate - __init__ - payload setter - Summary - Verify that the payload setter calls fail_json when payload is not a dict + ### Summary + Verify that the payloads setter raises TypeError when payloads is not + a dict. - Setup + ### Setup - payload is set to a list - Test - - fail_json is called because payload is not a dict + ### Test + - TypeError is raised because payloads is not a dict - instance.payload is not modified, hence it retains its initial value of None """ - key = "test_image_policy_update_00021a" - match = "ImagePolicyUpdate._verify_payload: " - match += "payload must be a dict. Got type list, value" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_update instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): + + match = r"ImagePolicyUpdate\.verify_payload:\s+" + match += r"payload must be a dict\. Got type list, value" + + with pytest.raises(TypeError, match=match): instance.payload = payloads_image_policy_update(key) assert instance.payload is None @@ -137,199 +147,262 @@ def test_image_policy_update_00021(image_policy_update) -> None: ) def test_image_policy_update_00022(image_policy_update, key, match) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdate - __init__ - payload setter - Summary + ### Summary Verify that the payload setter calls fail_json when a payload is missing a mandatory key - Test - - fail_json is called because payload is missing a mandatory key + ### Test + - ``ValueError`` is raised because payload is missing a mandatory key. - instance.payload is not modified, hence it retains its initial value of None """ with does_not_raise(): instance = image_policy_update instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): + with pytest.raises(ValueError, match=match): instance.payload = payloads_image_policy_update(key) assert instance.payload is None -def test_image_policy_update_00030(monkeypatch, image_policy_update) -> None: +def test_image_policy_update_00030(image_policy_update) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdate - __init__() - - _build_payloads_to_commit() + - build_payloads_to_commit() - _verify_image_policy_ref_count() - payload setter - Summary - Verify _build_payloads_to_commit() behavior when a request contains one + ### Summary + Verify build_payloads_to_commit() behavior when a request contains one image policy that exists on the controller and the caller has requested to update it. The update consists of changing the policyDescr. - Setup - - ImagePolicies().all_policies, is mocked to indicate that two image + ### Setup + - EpPolicies() endpoint response is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyUpdate().payload is set to contain a payload (KR5M) that is present on the controller. - Test + ### Test - payloads_to_commit will contain the payload for KR5M since it exists on the controller and the caller has requested to update it. - The policyName for this payload will be "KR5M" - The policyDescr for this payload will be "KR5M updated" - - fail_json is not called + - Exceptions are not raised. """ - key = "test_image_policy_update_00030a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_update(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_update instance.results = Results() - instance.payload = payloads_image_policy_update(key) + instance.rest_send = rest_send + instance.params = params + instance.payload = gen_payloads.next + instance.commit() - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) + # The controller adds fields to the payload that we need to + # account for when verifying diff_current, since diff_current + # will contains these extra fields. + payload_compare = copy.deepcopy(payloads_image_policy_update(key)) + payload_compare["fabricPolicyName"] = "" + payload_compare["imagePresent"] = "Present" + payload_compare["role"] = "" + payload_compare["unInstall"] = "false" - with does_not_raise(): - instance._build_payloads_to_commit() - - assert instance._payloads_to_commit == [payloads_image_policy_update(key)] + assert instance._payloads_to_commit == [payload_compare] assert instance._payloads_to_commit[0]["policyName"] == "KR5M" assert instance._payloads_to_commit[0]["policyDescr"] == "KR5M updated" -def test_image_policy_update_00031(monkeypatch, image_policy_update) -> None: +def test_image_policy_update_00031(image_policy_update) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdate - __init__() - - _build_payloads_to_commit() + - build_payloads_to_commit() - _verify_image_policy_ref_count() - payload setter - Summary - Verify that instance._build_payloads_to_commit() does not add a payload + ### Summary + Verify that instance.build_payloads_to_commit() does not add a payload to the payloads_to_commit list when a request is made to update an image policy that does not exist on the controller. - Setup - - ImagePolicies().all_policies, is mocked to indicate that two image policies - (KR5M, NR3F) exist on the controller. + ### Setup + - EpPolicies() endpoint response is mocked to indicate that two image + policies (KR5M, NR3F) exist on the controller. - ImagePolicyUpdate().payload is set to contain a payload containing an image policy (FOO) that does not exist on the controller. - Test - - fail_json is not called + ### Test + - Exceptions are not raised. - _payloads_to_commit will be an empty list since policy FOO does not exist on the controller. """ - key = "test_image_policy_update_00031a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_update(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_update instance.results = Results() - instance.payload = payloads_image_policy_update(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() + instance.rest_send = rest_send + instance.params = params + instance.payload = gen_payloads.next + instance.commit() assert instance._payloads_to_commit == [] def test_image_policy_update_00033(image_policy_update) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdate - commit() - - _build_payloads_to_commit - - fail_json + - build_payloads_to_commit - Summary - Verify that _build_payloads_to_commit() calls fail_json when + ### Summary + Verify that build_payloads_to_commit() raises ``ValueError`` when payload is not set. - Setup + ### Setup - ImagePolicyUpdate().payload is not set - Test - - fail_json is called because payload is None + ### Test + - ``ValueError`` is raised because payload is None. """ with does_not_raise(): instance = image_policy_update instance.results = Results() - match = "ImagePolicyUpdate.commit: payload must be set prior to calling commit." - with pytest.raises(AnsibleFailJson, match=match): + match = r"ImagePolicyUpdate.commit:\s+" + match += r"payload must be set prior to calling commit\." + with pytest.raises(ValueError, match=match): instance.commit() -def test_image_policy_update_00034(monkeypatch, image_policy_update) -> None: +def test_image_policy_update_00034(image_policy_update) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateCommon - - _build_payloads_to_commit() + - build_payloads_to_commit() - ImagePolicyUpdate - payload setter - commit() - Summary + ### Summary Verify that commit() returns without doing anything when payloads is set to a policy that does not exist on the controller. - Setup - - ImagePolicies().all_policies, is mocked to indicate that no policies - exist on the controller. + ### Setup + - EpPolicies() endpoint response is mocked to indicate that no + policies exist on the controller. - ImagePolicyUpdate().payload is set to a policy (FOO) that does not exist on the controller - Test + ### Test - ImagePolicyUpdate().commit returns without doing anything - ImagePolicyUpdate()._payloads_to_commit is an empty list - ImagePolicyUpdate().results.changed is empty - ImagePolicyUpdate().results.failed is empty """ - key = "test_image_policy_update_00034a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_update(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_update instance.results = Results() - instance.payload = payloads_image_policy_update(key) - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) + instance.rest_send = rest_send + instance.params = params with does_not_raise(): + instance.payload = gen_payloads.next instance.commit() assert instance._payloads_to_commit == [] assert len(instance.results.changed) == 0 assert len(instance.results.failed) == 0 -def test_image_policy_update_00035(monkeypatch, image_policy_update) -> None: +def test_image_policy_update_00035(image_policy_update) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdate - - _build_payloads_to_commit() - - _send_payloads() + - build_payloads_to_commit() + - send_payloads() - payload setter - commit() - Summary - Verify that ImagePolicyUpdate.commit() behaves as expected when the - controller responds to an image policy update request with a 200 response. + ### Summary + Verify ImagePolicyUpdate.commit() happy path. Controller responds + to an image create update with a 200 response. - Setup - - ImagePolicies().all_policies, is mocked to indicate that two policies + ### Setup + - EpPolicies() endpoint response is mocked to indicate that two policies (KR5M, NR3F) exist on the controller. - ImagePolicyUpdate().payload is set to contain a payload for KR5M in which policyDescr is different from the existing policyDescr. - - dcnm_send is mocked to return a successful (200) response. + - EpPolicyEdit() endpoint response is mocked to return a successful + (200) response. - Test - Test - - commit calls _build_payloads_to_commit which returns one payload. - - commit calls _send_payloads, which calls rest_send, which populates + ### Test + - commit calls build_payloads_to_commit which returns one payload. + - commit calls send_payloads, which calls rest_send, which populates diff_current with the payload due to result_current indicating success. - results.result_current is set to the expected value @@ -337,24 +410,35 @@ def test_image_policy_update_00035(monkeypatch, image_policy_update) -> None: - results.response_current is set to the expected value - results.action is set to "update" """ - key = "test_image_policy_update_00035a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_update(key) + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + gen_responses = ResponseGenerator(responses()) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + def payloads(): + yield payloads_image_policy_update(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_update instance.results = Results() - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) + instance.rest_send = rest_send + instance.params = params with does_not_raise(): - instance.payload = payloads_image_policy_update(key) + instance.payload = gen_payloads.next instance.commit() response_current = responses_image_policy_update(key) @@ -363,14 +447,23 @@ def mock_dcnm_send(*args, **kwargs): result_current = rest_send_result_current(key) result_current["sequence_number"] = 1 - payload = payloads_image_policy_update(key) - payload["sequence_number"] = 1 + # The controller adds fields to the payload that we need to + # account for when verifying diff_current, since diff_current + # will contains these extra fields. + # Also WE add sequence_number to the diff, so this is added here + # as well. + diff_compare = copy.deepcopy(payloads_image_policy_update(key)) + diff_compare["sequence_number"] = 1 + diff_compare["fabricPolicyName"] = "" + diff_compare["imagePresent"] = "Present" + diff_compare["role"] = "" + diff_compare["unInstall"] = "false" assert instance.results.action == "update" assert instance.rest_send.result_current == rest_send_result_current(key) assert instance.results.result_current == result_current assert instance.results.response_current == response_current - assert instance.results.diff_current == payload + assert instance.results.diff_current == diff_compare assert False in instance.results.failed assert True not in instance.results.failed assert False not in instance.results.changed @@ -381,53 +474,66 @@ def mock_dcnm_send(*args, **kwargs): assert instance.results.metadata[0]["sequence_number"] == 1 -def test_image_policy_update_00036(monkeypatch, image_policy_update) -> None: +def test_image_policy_update_00036(image_policy_update) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdate - - _build_payloads_to_commit() - - _send_payloads() + - build_payloads_to_commit() + - send_payloads() - payload setter - commit() - Summary + ### Summary Verify that ImagePolicyUpdate.commit() behaves as expected when the controller responds to an image policy update request with a 500 response. - Setup - - ImagePolicies().all_policies, is mocked to indicate that one policy + ### Setup + - EpPolicies() endpoint response is mocked to indicate that one policy (KR5M) exists on the controller. - ImagePolicyUpdate().payloads is set to contain the payload for image policy KR5M with policyDescr changed. - - dcnm_send is mocked to return a failure (500) response. + - EpPolicyEdit() endpoint response is mocked to return an internal + server error (500) response. - Test - - commit calls _build_payloads_to_commit which returns one payload - - commit calls _send_payloads, which populates response_ok, result_ok, + ### Test + - commit calls build_payloads_to_commit which returns one payload + - commit calls send_payloads, which populates response_ok, result_ok, diff_ok, response_nok, result_nok, and diff_nok based on the payload - returned from _build_payloads_to_commit and the failure response + returned from build_payloads_to_commit and the failure response - response_ok, result_ok, and diff_ok are set to empty lists - response_nok, result_nok, and diff_nok are set to expected values """ - key = "test_image_policy_update_00036a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) + + gen_responses = ResponseGenerator(responses()) - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_update(key) + def payloads(): + yield payloads_image_policy_update(key) - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + gen_payloads = ResponseGenerator(payloads()) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.timeout = 1 + rest_send.unit_test = True with does_not_raise(): instance = image_policy_update - instance.rest_send.unit_test = True instance.results = Results() - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) + instance.rest_send = rest_send + instance.params = params with does_not_raise(): - instance.payload = payloads_image_policy_update(key) + instance.payload = gen_payloads.next instance.commit() response_current = responses_image_policy_update(key) @@ -451,66 +557,63 @@ def mock_dcnm_send(*args, **kwargs): assert instance.results.metadata[0]["sequence_number"] == 1 -def test_image_policy_update_00040(image_policy_update) -> None: - """ - Classes and Methods - - ImagePolicyUpdate - - __init__ - - _default_policy - - Summary - Verify that instance._default_policy setter calls fail_json when - passed a policy_name that is not a string. - - Test - - fail_json is called because policy_name is a list - """ - match = "ImagePolicyUpdate._default_policy: " - match += "policy_name must be a string. " - match += r"Got type list for value \[\]" - - with does_not_raise(): - instance = image_policy_update - instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): - instance._default_policy([]) - - -def test_image_policy_update_00050(monkeypatch, image_policy_update) -> None: +def test_image_policy_update_00050(image_policy_update) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdate - - _build_payloads_to_commit() - - _send_payloads() + - build_payloads_to_commit() + - send_payloads() - payload setter - commit() - Summary - Verify that fail_json is called when an image policy update request is made + ### Summary + Verify that ValueError is raised when an image policy update request is made for an image policy which has a ref_count != 0 on the controller, i.e. switches are attached to the image policy. - Setup - - ImagePolicies().all_policies, is mocked to indicate that one policy + ### Setup + - EpPolicies() endpoint response is mocked to indicate that one policy (KR5M) exists on the controller with ref_count == 2. - ImagePolicyUpdate().payloads is set to contain the payload for image policy KR5M with policyDescr changed. - Test - - commit calls _build_payloads_to_commit - - _build_payloads_to_commit calls _verify_image_policy_ref_count - - _verify_image_policy_ref_count calls fail_json with the expected message + ### Test + - commit calls ``build_payloads_to_commit`` + - ``build_payloads_to_commit`` calls ``_verify_image_policy_ref_count`` + - ``_verify_image_policy_ref_count`` raises ``ValueError`` with the + expected message. """ - key = "test_image_policy_update_00050a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_update(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_update instance.results = Results() - instance.payload = payloads_image_policy_update(key) - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - - match = "ImagePolicyUpdate._verify_image_policy_ref_count: " - match += "One or more policies have devices attached." - with pytest.raises(AnsibleFailJson, match=match): + instance.rest_send = rest_send + instance.params = params + instance.payload = gen_payloads.next + + match = r"ImagePolicyUpdate\._verify_image_policy_ref_count:\s+" + match += r"One or more policies have devices attached\.\s+" + match += r"Detach these policies from all devices first using\s+" + match += r"the dcnm_image_upgrade module with state == deleted\." + with pytest.raises(ValueError, match=match): instance.commit() diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py index aed364f06..898b1c4ee 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py @@ -577,31 +577,6 @@ def mock_dcnm_send(*args, **kwargs): assert True in instance.results.failed -def test_image_policy_update_bulk_00040(image_policy_update_bulk) -> None: - """ - Classes and Methods - - ImagePolicyUpdateBulk - - __init__ - - _default_policy - - Summary - Verify that instance._default_policy setter calls fail_json when - passed a policy_name that is not a string. - - Test - - fail_json is called because policy_name is a list - """ - match = "ImagePolicyUpdateBulk._default_policy: " - match += "policy_name must be a string. " - match += r"Got type list for value \[\]" - - with does_not_raise(): - instance = image_policy_update_bulk - instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): - instance._default_policy([]) - - def test_image_policy_update_bulk_00050(monkeypatch, image_policy_update_bulk) -> None: """ Classes and Methods diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py index 461d61f29..00db97e5e 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py @@ -276,21 +276,22 @@ def image_policy_replace_bulk_fixture(): @pytest.fixture(name="image_policy_update") def image_policy_update_fixture(): """ - mock ImagePolicyUpdate + Return ImagePolicyUpdate with params set. """ - instance = MockAnsibleModule() - instance.state = "merged" - return ImagePolicyUpdate(instance) + instance = ImagePolicyUpdate() + instance.params = params + return instance @pytest.fixture(name="image_policy_update_bulk") def image_policy_update_bulk_fixture(): """ - mock ImagePolicyUpdateBulk + Return ImagePolicyUpdateBulk with params set. """ - instance = MockAnsibleModule() - instance.state = "merged" - return ImagePolicyUpdateBulk(instance) + instance = ImagePolicyUpdateBulk() + instance.params = params + return instance + @pytest.fixture(name="config2payload") @@ -403,6 +404,16 @@ def responses_ep_policy_create(key: str) -> Dict[str, str]: return data +def responses_ep_policy_edit(key: str) -> Dict[str, str]: + """ + Return responses for EpPolicyEdit() endpoint + """ + data_file = "responses_EpPolicyEdit" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + def responses_image_policies(key: str) -> Dict[str, str]: """ Return responses for ImagePolicies From 1c48c905db0bc3308ca92bb7c9bbc017b4f73d3f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 30 Jun 2024 10:21:13 -1000 Subject: [PATCH 213/230] ImagePolicyUpdateBulk: Align with v2 classes. ImagePolicyUpdateBulk: update unit tests to reflect use of v2 classes. utils.py: ImagePolicy* fixtures: update params with correct state using get_state() test_image_policy_update.py: docstring updates and other very minor updates. test_image_policy_update_bulk.py: update unit tests to reflect use of v2 classes. dcnm_image_policy.py: Remove commented code. Run through black/isort. --- plugins/modules/dcnm_image_policy.py | 6 +- .../payloads_ImagePolicyUpdateBulk.json | 41 +- .../fixtures/responses_EpPolicies.json | 314 +++++++++ .../fixtures/responses_EpPolicyEdit.json | 45 ++ .../fixtures/result_current_RestSend.json | 4 + .../test_image_policy_update.py | 39 +- .../test_image_policy_update_bulk.py | 622 +++++++++++------- .../modules/dcnm/dcnm_image_policy/utils.py | 45 +- 8 files changed, 827 insertions(+), 289 deletions(-) diff --git a/plugins/modules/dcnm_image_policy.py b/plugins/modules/dcnm_image_policy.py index 72e330df6..80bff4664 100644 --- a/plugins/modules/dcnm_image_policy.py +++ b/plugins/modules/dcnm_image_policy.py @@ -642,11 +642,9 @@ def commit(self) -> None: self.log.debug(msg) self._delete_policies_not_in_want() - #task = Merged(self.params) # pylint: disable=attribute-defined-outside-init - self.merged.rest_send = ( - self.rest_send - ) + self.merged.rest_send = self.rest_send + # pylint: enable=attribute-defined-outside-init self.merged.results = self.results self.merged.commit() diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdateBulk.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdateBulk.json index ddda1c2dd..ca0382263 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdateBulk.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdateBulk.json @@ -3,7 +3,7 @@ "Mocked payloads for ImagePolicyUpdate unit tests.", "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py" ], - "test_image_policy_update_bulk_00020a": [ + "test_image_policy_update_bulk_00010a": [ { "agnostic": false, "epldImgName": "n9000-epld.10.3.2.F.img", @@ -115,6 +115,19 @@ "rpmimages": "" } ], + "test_image_policy_update_bulk_00034a": [ + { + "agnostic": false, + "epldImgName": "n9000-epld.10.3.2.F.img", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", + "platform": "N9K", + "policyDescr": "image policy of 10.3(3)F", + "policyName": "FOO", + "policyType": "PLATFORM", + "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" + } + ], "test_image_policy_update_bulk_00035a": [ { "agnostic": false, @@ -152,32 +165,6 @@ "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" } ], - "test_image_policy_update_bulk_00037a": [ - { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.2.F.img", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K", - "policyDescr": "image policy of 10.3(3)F", - "policyName": "FOO", - "policyType": "PLATFORM", - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - ], - "test_image_policy_update_bulk_00037b": [ - { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.2.F.img", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K", - "policyDescr": "image policy of 10.3(3)F", - "policyName": "BAR", - "policyType": "PLATFORM", - "rpmimages": "" - } - ], "test_image_policy_update_bulk_00037d": [ { "agnostic": false, diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json index 3bb4e6598..1535b4a6c 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json @@ -324,5 +324,319 @@ ], "message": "" } + }, + "test_image_policy_update_bulk_00030a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_update_bulk_00031a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_update_bulk_00032a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_update_bulk_00034a": { + "TEST_NOTES": [ + "No image policies exist on the controller." + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "test_image_policy_update_bulk_00035a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_update_bulk_00036a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_update_bulk_00037a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "FOO", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "FOO", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "BAR", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "BAR", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_update_bulk_00050a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 2, + "imagePresent": "Present" + } + ], + "message": "" + } } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json index d08e0d974..c153137b0 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json @@ -21,5 +21,50 @@ "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", "RETURN_CODE": 500 + }, + "test_image_policy_update_bulk_00030a": { + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_update_bulk_00031a": { + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_update_bulk_00032a": { + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_update_bulk_00035a": { + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_update_bulk_00036a": { + "DATA": "Internal server error.", + "MESSAGE": "NOK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 500 + }, + "test_image_policy_update_bulk_00037a": { + "DATA": "Policy edited successfully.", + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_update_bulk_00037b": { + "DATA": "Internal server error.", + "MESSAGE": "NOK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 500 } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/result_current_RestSend.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/result_current_RestSend.json index 1de070597..90bad89ba 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/result_current_RestSend.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/result_current_RestSend.json @@ -26,5 +26,9 @@ "test_image_policy_update_bulk_00035a": { "changed": true, "success": true + }, + "test_image_policy_update_bulk_00036a": { + "changed": false, + "success": false } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py index fa2563fe8..3b3ee3125 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py @@ -119,8 +119,8 @@ def test_image_policy_update_00021(image_policy_update) -> None: - payload is set to a list ### Test - - TypeError is raised because payloads is not a dict - - instance.payload is not modified, hence it retains its initial value of None + - ``TypeError`` is raised because payload is not a dict. + - ``instance.payload`` is not modified. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -153,12 +153,12 @@ def test_image_policy_update_00022(image_policy_update, key, match) -> None: - payload setter ### Summary - Verify that the payload setter calls fail_json when a payload is missing - a mandatory key + Verify that ``payload.setter`` raises ``ValueError when a payload is + missing a mandatory key ### Test - - ``ValueError`` is raised because payload is missing a mandatory key. - - instance.payload is not modified, hence it retains its initial value of None + - ``ValueError`` is raised because payload is missing a mandatory key. + - ``instance.payload`` is not modified. """ with does_not_raise(): instance = image_policy_update @@ -220,7 +220,6 @@ def payloads(): instance = image_policy_update instance.results = Results() instance.rest_send = rest_send - instance.params = params instance.payload = gen_payloads.next instance.commit() @@ -248,9 +247,13 @@ def test_image_policy_update_00031(image_policy_update) -> None: - payload setter ### Summary - Verify that instance.build_payloads_to_commit() does not add a payload - to the payloads_to_commit list when a request is made to update an - image policy that does not exist on the controller. + Verify behavior when a request is sent to update a policy that does + not exist on the controller + + ### Expected behavior + ``instance.build_payloads_to_commit()`` does not add a payload + to the ``payloads_to_commit`` list if the associated policy + does not exist on the controller. ### Setup - EpPolicies() endpoint response is mocked to indicate that two image @@ -287,7 +290,6 @@ def payloads(): instance = image_policy_update instance.results = Results() instance.rest_send = rest_send - instance.params = params instance.payload = gen_payloads.next instance.commit() assert instance._payloads_to_commit == [] @@ -369,9 +371,6 @@ def payloads(): instance = image_policy_update instance.results = Results() instance.rest_send = rest_send - instance.params = params - - with does_not_raise(): instance.payload = gen_payloads.next instance.commit() assert instance._payloads_to_commit == [] @@ -389,8 +388,8 @@ def test_image_policy_update_00035(image_policy_update) -> None: - commit() ### Summary - Verify ImagePolicyUpdate.commit() happy path. Controller responds - to an image create update with a 200 response. + Verify ImagePolicyUpdate.commit() happy path. Controller returns + a 200 response to an image policy update request. ### Setup - EpPolicies() endpoint response is mocked to indicate that two policies @@ -435,9 +434,6 @@ def payloads(): instance = image_policy_update instance.results = Results() instance.rest_send = rest_send - instance.params = params - - with does_not_raise(): instance.payload = gen_payloads.next instance.commit() @@ -530,9 +526,6 @@ def payloads(): instance = image_policy_update instance.results = Results() instance.rest_send = rest_send - instance.params = params - - with does_not_raise(): instance.payload = gen_payloads.next instance.commit() @@ -588,7 +581,6 @@ def test_image_policy_update_00050(image_policy_update) -> None: def responses(): yield responses_ep_policies(key) - yield responses_ep_policy_edit(key) gen_responses = ResponseGenerator(responses()) @@ -608,7 +600,6 @@ def payloads(): instance = image_policy_update instance.results = Results() instance.rest_send = rest_send - instance.params = params instance.payload = gen_payloads.next match = r"ImagePolicyUpdate\._verify_image_policy_ref_count:\s+" diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py index 898b1c4ee..b4cc0a468 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py @@ -29,32 +29,38 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -from typing import Any, Dict +import copy +import inspect import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - GenerateResponses, MockImagePolicies, does_not_raise, - image_policy_update_bulk_fixture, payloads_image_policy_update_bulk, - responses_image_policy_update_bulk, rest_send_result_current, - results_image_policy_update_bulk) + GenerateResponses, MockAnsibleModule, does_not_raise, + image_policy_update_bulk_fixture, params, + payloads_image_policy_update_bulk, responses_ep_policies, + responses_ep_policy_edit, responses_image_policy_update_bulk, + rest_send_result_current) -def test_image_policy_update_bulk_00010(image_policy_update_bulk) -> None: +def test_image_policy_update_bulk_00000(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateBulk - __init__ - Summary + ### Summary Verify that __init__() sets class attributes to the expected values. - Test + ### Test - Class attributes initialized to expected values - fail_json is not called """ @@ -62,11 +68,10 @@ def test_image_policy_update_bulk_00010(image_policy_update_bulk) -> None: instance = image_policy_update_bulk assert instance.class_name == "ImagePolicyUpdateBulk" assert instance.action == "update" - assert instance.state == "merged" - assert instance.check_mode is False - assert isinstance(instance.endpoints, ApiEndpoints) - assert instance.path == ApiEndpoints().policy_edit["path"] - assert instance.verb == ApiEndpoints().policy_edit["verb"] + assert instance.params.get("state") == "merged" + assert instance.params.get("check_mode") is False + assert instance.endpoint.class_name == "EpPolicyEdit" + assert instance.endpoint.verb == "POST" assert instance._mandatory_payload_keys == { "nxosVersion", "policyName", @@ -76,24 +81,25 @@ def test_image_policy_update_bulk_00010(image_policy_update_bulk) -> None: assert instance._payloads_to_commit == [] -def test_image_policy_update_bulk_00020(image_policy_update_bulk) -> None: +def test_image_policy_update_bulk_00010(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateCommon - __init__ - payloads setter - ImagePolicyUpdateBulk - __init__() - Summary + ### Summary Verify that the payloads setter sets the payloads attribute to the expected value. - Test + ### Test - payloads is set to expected value - fail_json is not called """ - key = "test_image_policy_update_bulk_00020a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_update_bulk @@ -103,28 +109,31 @@ def test_image_policy_update_bulk_00020(image_policy_update_bulk) -> None: def test_image_policy_update_bulk_00021(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateCommon - __init__() - payloads setter - ImagePolicyUpdateBulk - __init__() - Summary - Verify that the payloads setter calls fail_json when payloads is not a list of dict + ### Summary + Verify that the payloads setter raises ``TypeError`` when payloads is not + a list of dict. - Test - - fail_json is called because payloads is not a list - - instance.payloads is not modified, hence it retains its initial value of None + ### Test + - ``TypeError`` is raised because payloads is not a list + - instance.payloads is not modified, hence it retains its initial value of None """ - key = "test_image_policy_update_bulk_00021a" - match = "ImagePolicyUpdateBulk.payloads: " - match += "payloads must be a list of dict. got dict for value" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_update_bulk instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): + + match = r"ImagePolicyUpdateBulk\.payloads:\s+" + match += r"payloads must be a list of dict\. got dict for value.*" + with pytest.raises(TypeError, match=match): instance.payloads = payloads_image_policy_update_bulk(key) assert instance.payloads is None @@ -139,167 +148,245 @@ def test_image_policy_update_bulk_00021(image_policy_update_bulk) -> None: ) def test_image_policy_update_bulk_00022(image_policy_update_bulk, key, match) -> None: """ - Classes and Methods - - ImagePolicyCreateCommon + ### Classes and Methods + - ImagePolicyUpdateCommon - __init__() - - payloads setter + - payloads.setter - ImagePolicyUpdateBulk - __init__() - Summary - Verify that the payloads setter calls fail_json when a payload in the payloads list - is missing a mandatory key + ### Summary + Verify that ``payloads.setter`` raises ``ValueError when a payload is + missing a mandatory key - Test - - fail_json is called because a payload in the payloads list is missing a mandatory key - - instance.payloads is not modified, hence it retains its initial value of None + ### Test + - ``ValueError`` is raised because payload is missing a mandatory key. + - ``instance.payload`` is not modified. """ with does_not_raise(): instance = image_policy_update_bulk instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): + with pytest.raises(ValueError, match=match): instance.payloads = payloads_image_policy_update_bulk(key) assert instance.payloads is None def test_image_policy_update_bulk_00023(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods + - ImagePolicyUpdateCommon + - __init__ + - payloads.setter - ImagePolicyUpdateBulk - __init__ - - payload setter - Summary - Verify that the payloads setter calls fail_json when payloads is a list - but contains an element that is not a dict. + ### Summary + Verify that ``payloads.setter` raises ``TypeError`` when payloads is + a list but contains an element that is not a dict. - Test - - fail_json is called because payloads is a list, but contains a non-dict element - - instance.payloads is not modified, hence it retains its initial value of None + ### Test + - ``TypeError`` is raised because payloads is a list, but contains a + non-dict element. + - ``instance.payloads`` is not modified. """ - key = "test_image_policy_update_bulk_00023a" - match = "ImagePolicyUpdateBulk._verify_payload: " - match += "payload must be a dict. Got type str, value " - match += "IM_A_STRING_BUT_SHOULD_BE_A_DICT" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_update_bulk instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): + + match = r"ImagePolicyUpdateBulk\.verify_payload:\s+" + match += r"payload must be a dict\. Got type str, value\s+" + match += r"IM_A_STRING_BUT_SHOULD_BE_A_DICT" + with pytest.raises(TypeError, match=match): instance.payloads = payloads_image_policy_update_bulk(key) assert instance.payloads is None -def test_image_policy_update_bulk_00030(monkeypatch, image_policy_update_bulk) -> None: +def test_image_policy_update_bulk_00030(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateBulk - __init__() - - _build_payloads_to_commit() + - build_payloads_to_commit() - _verify_image_policy_ref_count() - payloads setter - Summary - Verify _build_payloads_to_commit() behavior when a request contains one + ### Summary + Verify build_payloads_to_commit() behavior when a request contains one image policy that exists on the controller and the caller has requested to update it. The update consists of changing the policyDescr. - Setup - - ImagePolicies().all_policies, is mocked to indicate that two image + ### Setup + - EpPolicies() endpoint response is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyUpdateBulk().payloads is set to contain one payload (KR5M) that is present on the controller. - Test + ### Test - payloads_to_commit will contain payload for KR5M since it exists on the controller and the caller has requested to update it. """ - key = "test_image_policy_update_bulk_00030a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_update_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_update_bulk instance.results = Results() - instance.payloads = payloads_image_policy_update_bulk(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() - assert instance._payloads_to_commit == payloads_image_policy_update_bulk(key) + instance.rest_send = rest_send + instance.payloads = gen_payloads.next + instance.commit() + + # The controller adds fields to the payload that we need to + # account for when verifying diff_current, since _payloads_to_commit + # will contains these extra fields. + payload_compare = copy.deepcopy(payloads_image_policy_update_bulk(key)) + for payload in payload_compare: + payload.update({"fabricPolicyName": ""}) + payload.update({"imagePresent": "Present"}) + payload.update({"role": ""}) + payload.update({"unInstall": "false"}) + + assert instance._payloads_to_commit == payload_compare assert instance._payloads_to_commit[0]["policyName"] == "KR5M" assert instance._payloads_to_commit[0]["policyDescr"] == "KR5M updated" -def test_image_policy_update_bulk_00031(monkeypatch, image_policy_update_bulk) -> None: +def test_image_policy_update_bulk_00031(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateBulk - __init__() - - _build_payloads_to_commit() + - build_payloads_to_commit() - _verify_image_policy_ref_count() - payloads setter - Summary + ### Summary Verify behavior when a request is sent to update a policy that does not exist on the controller - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), - is mocked to indicate that two image policies (KR5M, NR3F) exist on the - controller. + ### Expected behavior + ``instance.build_payloads_to_commit()`` does not add a payload + to the ``payloads_to_commit`` list if the associated policy + does not exist on the controller. + + ### Setup + - EpPolicies() endpoint response is mocked to indicate that two image + policies (KR5M, NR3F) exist on the controller. - ImagePolicyUpdateBulk().payloads is set to contain one payload containing an image policy (FOO) that is not present on the controller. - Test - - fail_json is not called + ### Test + - Exceptions are not raised. - _payloads_to_commit will be an empty list since policy FOO does not exist on the controller. """ - key = "test_image_policy_update_bulk_00031a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_update_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_update_bulk instance.results = Results() - instance.payloads = payloads_image_policy_update_bulk(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next + instance.commit() + assert instance._payloads_to_commit == [] assert len(instance.results.failed) == 0 assert len(instance.results.changed) == 0 -def test_image_policy_update_bulk_00032(monkeypatch, image_policy_update_bulk) -> None: +def test_image_policy_update_bulk_00032(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateBulk - __init__() - - _build_payloads_to_commit() + - build_payloads_to_commit() - _verify_image_policy_ref_count() - payloads setter - Summary - Verify _build_payloads_to_commit() behavior when a request contains one + ### Summary + Verify build_payloads_to_commit() behavior when a request contains one image policy that does not exist on the controller and one image policy that exists on the controller. - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), - is mocked to indicate that two image policies (KR5M, NR3F) exist on the - controller. + ### Setup + - EpPolicies() endpoint response is mocked to indicate that two image + policies (KR5M, NR3F) exist on the controller. - ImagePolicyUpdateBulk().payloads is set to contain one payload containing an image policy (FOO) that does not exist on the controller and one payload containing an image policy (KR5M) that exists on the controller. - Test + ### Test - _payloads_to_commit will contain one payload - The policyName for this payload will be "KR5M", which is the image policy that - exists on the controller + exists on the controllers. """ - key = "test_image_policy_update_bulk_00032a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_update_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_update_bulk instance.results = Results() - instance.payloads = payloads_image_policy_update_bulk(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next + instance.commit() + assert len(instance._payloads_to_commit) == 1 assert instance._payloads_to_commit[0]["policyName"] == "KR5M" assert instance._payloads_to_commit[0]["policyDescr"] == "KR5M updated" @@ -307,117 +394,157 @@ def test_image_policy_update_bulk_00032(monkeypatch, image_policy_update_bulk) - def test_image_policy_update_bulk_00033(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateBulk - commit() - - _build_payloads_to_commit - - fail_json + - build_payloads_to_commit - Summary - Verify that _build_payloads_to_commit() calls fail_json when + ### Summary + Verify that build_payloads_to_commit() raises ``ValueError`` when payloads is not set. - Setup + ### Setup - ImagePolicyUpdateBulk().payloads is not set - Test - - fail_json is called because payloads is None + ### Test + - ``ValueError`` is raised because payloads is None. """ with does_not_raise(): instance = image_policy_update_bulk instance.results = Results() - match = ( - "ImagePolicyUpdateBulk.commit: payloads must be set prior to calling commit." - ) - with pytest.raises(AnsibleFailJson, match=match): + match = r"ImagePolicyUpdateBulk\.commit:\s+" + match += r"payloads must be set prior to calling commit\." + with pytest.raises(ValueError, match=match): instance.commit() -def test_image_policy_update_bulk_00034(monkeypatch, image_policy_update_bulk) -> None: +def test_image_policy_update_bulk_00034(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateBulk - payloads setter - commit() - - _build_payloads_to_commit() + - build_payloads_to_commit() - Summary + ### Summary Verify that commit() returns without doing anything when payloads - is set to an empty list. + is set to a policy that does not exist on the controller. - Setup - - ImagePolicyUpdateBulk().payloads is set to an empty list + ### Setup + - EpPolicies() endpoint response is mocked to indicate that no + policies exist on the controller. + - ImagePolicyUpdate().payload is set to a policy (FOO) that does not + exist on the controller - Test + ### Test - ImagePolicyUpdateBulk().commit returns without doing anything """ - key = "test_image_policy_update_bulk_00034a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" - with does_not_raise(): - instance = image_policy_update_bulk - instance.results = Results() - instance.payloads = [] + def responses(): + yield responses_ep_policies(key) + + gen_responses = ResponseGenerator(responses()) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) + def payloads(): + yield payloads_image_policy_update_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): + instance = image_policy_update_bulk + instance.results = Results() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next instance.commit() + assert instance._payloads_to_commit == [] + assert len(instance.results.changed) == 0 + assert len(instance.results.failed) == 0 -def test_image_policy_update_bulk_00035(monkeypatch, image_policy_update_bulk) -> None: +def test_image_policy_update_bulk_00035(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateBulk - - _build_payloads_to_commit() + - build_payloads_to_commit() - _send_payloads() - payloads setter - commit() - Summary - Verify behavior when a request is made to update two image policies - that exist on the controller. + ### Summary + Verify ImagePolicyUpdateBulk.commit() happy path. Controller returns + a 200 response to an image policy update request. - Setup - - ImagePolicies().all_policies, is mocked to indicate that two policies + ### Setup + - EpPolicies() endpoint response is mocked to indicate that two policies (KR5M, NR3F) exist on the controller. - - ImagePolicyUpdateBulk().payloads is set to contain payloads for KR5M and NR3F + - ImagePolicyUpdate().payload is set to contain payloads for KR5M and NR3F in which policyDescr is different from the existing policyDescr. - - dcnm_send is mocked to return a successful (200) response. + - EpPolicyEdit() endpoint response is mocked to return a successful + (200) response. - Test - - commit calls _build_payloads_to_commit which returns two payloads + ### Test + - commit calls build_payloads_to_commit which returns two payloads. - commit calls _send_payloads, which calls results.register_task_result() to update the results. - - results.* are set to the expected values + - results.* are set to the expected values. """ - key = "test_image_policy_update_bulk_00035a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_update_bulk(key) + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) + yield responses_ep_policy_edit(key) - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + gen_responses = ResponseGenerator(responses()) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + def payloads(): + yield payloads_image_policy_update_bulk(key) - with does_not_raise(): - instance = image_policy_update_bulk - instance.results = Results() + gen_payloads = ResponseGenerator(payloads()) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): - instance.payloads = payloads_image_policy_update_bulk(key) + instance = image_policy_update_bulk + instance.results = Results() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next instance.commit() - payload_0 = payloads_image_policy_update_bulk(key)[0] - # sequence_number is added by the Results class - payload_0["sequence_number"] = 1 + response_current = responses_image_policy_update_bulk(key) + response_current["sequence_number"] = 1 - payload_1 = payloads_image_policy_update_bulk(key)[1] - payload_1["sequence_number"] = 2 + result_current = rest_send_result_current(key) + result_current["sequence_number"] = 1 + + # The controller adds fields to the payload that we need to + # account for when verifying diff_current, since _payloads_to_commit + # will contains these extra fields. + diff_compare = copy.deepcopy(payloads_image_policy_update_bulk(key)) + sequence_number = 1 + for item in diff_compare: + item.update({"fabricPolicyName": ""}) + item.update({"imagePresent": "Present"}) + item.update({"role": ""}) + item.update({"unInstall": "false"}) + item.update({"sequence_number": sequence_number}) + sequence_number += 1 assert instance.results.action == "update" assert instance.rest_send.result_current == rest_send_result_current(key) @@ -426,8 +553,8 @@ def mock_dcnm_send(*args, **kwargs): assert len(instance.results.response) == 2 assert instance.results.result[0].get("sequence_number") == 1 assert instance.results.result[1].get("sequence_number") == 2 - assert instance.results.diff[0] == payload_0 - assert instance.results.diff[1] == payload_1 + assert instance.results.diff[0] == diff_compare[0] + assert instance.results.diff[1] == diff_compare[1] assert instance.results.diff[0].get("policyDescr") == "KR5M updated" assert instance.results.diff[1].get("policyDescr") == "NR3F updated" assert False in instance.results.failed @@ -443,28 +570,29 @@ def mock_dcnm_send(*args, **kwargs): assert instance.results.metadata[1]["sequence_number"] == 2 -def test_image_policy_update_bulk_00036(monkeypatch, image_policy_update_bulk) -> None: +def test_image_policy_update_bulk_00036(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateCommon - payloads setter - - _build_payloads_to_commit() + - build_payloads_to_commit() - _send_payloads() - ImagePolicyUpdateBulk - commit() - Summary - Verify behavior when the controller returns a 500 response to an - image policy update request + ### Summary + Verify ImagePolicyUpdateBulk.commit() happy path. Controller returns + a 500 response to an image policy update request. - Setup - - ImagePolicies().all_policies, is mocked to indicate that one policy + ### Setup + - EpPolicies() endpoint response is mocked to indicate that one policy (KR5M) exists on the controller. - ImagePolicyUpdateBulk().payloads is set to contain the payload for image policy KR5M with policyDescr changed. - - dcnm_send is mocked to return a failure (500) response. + - EpPolicyEdit() endpoint response is mocked to return a failed + (500) response. - Test + ### Test - A sequence_number key is added to instance.results.response_current - instance.results.diff_current is set to a dict with only the key "sequence_number", since no changes were made @@ -475,25 +603,54 @@ def test_image_policy_update_bulk_00036(monkeypatch, image_policy_update_bulk) - - The value of instance.results.metadata "state" is "merged" - The value of instance.results.metadata "sequence_number" is 1 """ - key = "test_image_policy_update_bulk_00036a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_update_bulk(key) - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_update_bulk(key) + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.timeout = 1 + rest_send.unit_test = True with does_not_raise(): instance = image_policy_update_bulk - instance.rest_send.unit_test = True instance.results = Results() - instance.payloads = payloads_image_policy_update_bulk(key) + instance.rest_send = rest_send + instance.payloads = gen_payloads.next + instance.commit() - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + response_current = responses_image_policy_update_bulk(key) + response_current["sequence_number"] = 1 - with does_not_raise(): - instance.commit() + result_current = rest_send_result_current(key) + result_current["sequence_number"] = 1 + + # The controller adds fields to the payload that we need to + # account for when verifying diff_current, since _payloads_to_commit + # will contains these extra fields. + diff_compare = copy.deepcopy(payloads_image_policy_update_bulk(key)) + sequence_number = 1 + for item in diff_compare: + item.update({"fabricPolicyName": ""}) + item.update({"imagePresent": "Present"}) + item.update({"role": ""}) + item.update({"unInstall": "false"}) + item.update({"sequence_number": sequence_number}) + sequence_number += 1 response_current = responses_image_policy_update_bulk(key) response_current["sequence_number"] = 1 @@ -511,23 +668,23 @@ def mock_dcnm_send(*args, **kwargs): assert instance.results.metadata[0]["sequence_number"] == 1 -def test_image_policy_update_bulk_00037(monkeypatch, image_policy_update_bulk) -> None: +def test_image_policy_update_bulk_00037(image_policy_update_bulk) -> None: """ - Classes and Methods - - ImagePolicyCreateCommon + ### Classes and Methods + - ImagePolicyUpdateCommon - _process_responses() - ImagePolicyCreateBulk - __init__() - Summary + ### Summary Verify behavior when the controller returns a 200 response to an image policy - create request, followed by a 500 response to a subsequent image policy create + update request, followed by a 500 response to a subsequent image policy update request. - Setup + ### Setup - instance.payloads is set to contain two payloads - Test + ### Test - Both successful and bad responses are recorded with separate sequence_numbers. - instance.results.failed will be a set() containing both True and False - instance.results.changed will be a set() containing both True and False @@ -535,34 +692,37 @@ def test_image_policy_update_bulk_00037(monkeypatch, image_policy_update_bulk) - - instance.results.result contains two results - instance.results.diff contains two diffs """ - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" - key_policies = "test_image_policy_update_bulk_00037a" - key_ok = "test_image_policy_update_bulk_00037b" - key_nok = "test_image_policy_update_bulk_00037c" + key_ok = "test_image_policy_update_bulk_00037a" + key_nok = "test_image_policy_update_bulk_00037b" key_payloads = "test_image_policy_update_bulk_00037d" def responses(): - yield responses_image_policy_update_bulk(key_policies) - yield responses_image_policy_update_bulk(key_ok) - yield responses_image_policy_update_bulk(key_nok) + yield responses_ep_policies(key_policies) + yield responses_ep_policy_edit(key_ok) + yield responses_ep_policy_edit(key_nok) - gen = GenerateResponses(responses()) + gen_responses = ResponseGenerator(responses()) - def mock_dcnm_send(*args, **kwargs): - item = gen.next - return item + def payloads(): + yield payloads_image_policy_update_bulk(key_payloads) - with does_not_raise(): - instance = image_policy_update_bulk - instance.rest_send.unit_test = True - instance.results = Results() - instance.payloads = payloads_image_policy_update_bulk(key_payloads) + gen_payloads = ResponseGenerator(payloads()) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.timeout = 1 + rest_send.unit_test = True with does_not_raise(): + instance = image_policy_update_bulk + instance.results = Results() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next instance.commit() assert len(instance.results.diff) == 2 @@ -577,41 +737,61 @@ def mock_dcnm_send(*args, **kwargs): assert True in instance.results.failed -def test_image_policy_update_bulk_00050(monkeypatch, image_policy_update_bulk) -> None: +def test_image_policy_update_bulk_00050(image_policy_update_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyUpdateCommon - - _build_payloads_to_commit() + - build_payloads_to_commit() - ImagePolicyUpdateBulk - payloads setter - commit() - Summary - Verify that fail_json is called when an image policy update request is made + ### Summary + Verify that ValueError is raised when an image policy update request is made for an image policy which has a ref_count != 0 on the controller, i.e. switches are attached to the image policy. - Setup - - ImagePolicies().all_policies, is mocked to indicate that one policy + ### Setup + - EpPolicies() endpoint response is mocked to indicate that one policy (KR5M) exists on the controller with ref_count == 2. - ImagePolicyUpdateBulk().payloads is set to contain a payload for image policy KR5M with policyDescr changed. - Test - - commit calls _build_payloads_to_commit - - _build_payloads_to_commit calls _verify_image_policy_ref_count - - _verify_image_policy_ref_count calls fail_json with the expected message + ### Test + - commit calls ``build_payloads_to_commit`` + - ``build_payloads_to_commit`` calls ``_verify_image_policy_ref_count`` + - ``_verify_image_policy_ref_count`` raises ``ValueError`` with the + expected message. """ - key = "test_image_policy_update_bulk_00050a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_update_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_update_bulk instance.results = Results() - instance.payloads = payloads_image_policy_update_bulk(key) - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - - match = "ImagePolicyUpdateBulk._verify_image_policy_ref_count: " - match += "One or more policies have devices attached." - with pytest.raises(AnsibleFailJson, match=match): + instance.rest_send = rest_send + instance.payloads = gen_payloads.next + + match = r"ImagePolicyUpdateBulk\._verify_image_policy_ref_count:\s+" + match += r"One or more policies have devices attached\.\s+" + match += r"Detach these policies from all devices first using\s+" + match += r"the dcnm_image_upgrade module with state == deleted\." + with pytest.raises(ValueError, match=match): instance.commit() diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py index 00db97e5e..cee50d768 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py @@ -38,6 +38,19 @@ from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.fixture import \ load_fixture +def get_state(action): + if action in ["create", "update"]: + state = "merged" + elif action == "delete": + state = "deleted" + elif action == "query": + state = "query" + elif action == "replace": + state = "replaced" + else: + state = "merged" + return state + params = { "state": "merged", "check_mode": False, @@ -230,6 +243,7 @@ def image_policy_create_fixture(): """ instance = ImagePolicyCreate() instance.params = params + params.update({"state": get_state(instance.action)}) return instance @@ -240,37 +254,41 @@ def image_policy_create_bulk_fixture(): """ instance = ImagePolicyCreateBulk() instance.params = params + params.update({"state": get_state(instance.action)}) return instance @pytest.fixture(name="image_policy_delete") def image_policy_delete_fixture(): """ - mock ImagePolicyDelete + Return ImagePolicyDelete with params set. """ - instance = MockAnsibleModule() - instance.state = "deleted" - return ImagePolicyDelete(instance) + instance = ImagePolicyDelete() + instance.params = params + params.update({"state": get_state(instance.action)}) + return instance @pytest.fixture(name="image_policy_query") def image_policy_query_fixture(): """ - mock ImagePolicyQuery + Return ImagePolicyQuery with params set. """ - instance = MockAnsibleModule() - instance.state = "query" - return ImagePolicyQuery(instance) + instance = ImagePolicyQuery() + instance.params = params + params.update({"state": get_state(instance.action)}) + return instance @pytest.fixture(name="image_policy_replace_bulk") def image_policy_replace_bulk_fixture(): """ - mock ImagePolicyReplaceBulk + Return ImagePolicyReplaceBulk with params set. """ - instance = MockAnsibleModule() - instance.state = "replaced" - return ImagePolicyReplaceBulk(instance) + instance = ImagePolicyReplaceBulk() + instance.params = params + params.update({"state": get_state(instance.action)}) + return instance @pytest.fixture(name="image_policy_update") @@ -280,6 +298,7 @@ def image_policy_update_fixture(): """ instance = ImagePolicyUpdate() instance.params = params + params.update({"state": get_state(instance.action)}) return instance @@ -290,10 +309,10 @@ def image_policy_update_bulk_fixture(): """ instance = ImagePolicyUpdateBulk() instance.params = params + params.update({"state": get_state(instance.action)}) return instance - @pytest.fixture(name="config2payload") def config2payload_fixture(): """ From fa3ded1ef4d6d6b0a1ab3ca0856cf5a0b6305296 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 30 Jun 2024 10:25:48 -1000 Subject: [PATCH 214/230] test_image_policy_update_bulk.py: Remove unused import. Remove GenerateResponses() which is superceded by ResponseGenerator() --- .../dcnm/dcnm_image_policy/test_image_policy_update_bulk.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py index b4cc0a468..3af6dfd4d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py @@ -44,9 +44,8 @@ from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - GenerateResponses, MockAnsibleModule, does_not_raise, - image_policy_update_bulk_fixture, params, - payloads_image_policy_update_bulk, responses_ep_policies, + MockAnsibleModule, does_not_raise, image_policy_update_bulk_fixture, + params, payloads_image_policy_update_bulk, responses_ep_policies, responses_ep_policy_edit, responses_image_policy_update_bulk, rest_send_result_current) From cf459f08e623d1153265dd9bac6306787b7f2fd5 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 30 Jun 2024 14:39:24 -1000 Subject: [PATCH 215/230] ImagePolicyReplaceBulk: Align with v2 classes. 1. utils.py: run through black/isort. 2. test_image_policy_update.py: rename test case for consistency with other states. 3. test_image_policy_update_bulk.py: - rename test case for consistency with other states. - update docstrings - rename fixture keys 4. test_image_policy_replace_bulk.py - align test cases with v2 classes. 5. update.py - update docstrings - wrap build_payloads_to_commit() in try-except block. 6. replace.py - Use EpPolicyEdit() endpoint class and remove ApiEndpoints() import. - make some private methods public. - commit(): Verify mandatory properties are set. - update docstrings. --- plugins/module_utils/image_policy/replace.py | 78 ++- plugins/module_utils/image_policy/update.py | 19 +- .../payloads_ImagePolicyReplaceBulk.json | 41 +- .../fixtures/payloads_ImagePolicyUpdate.json | 2 +- .../payloads_ImagePolicyUpdateBulk.json | 4 +- .../fixtures/responses_EpPolicies.json | 284 ++++++++ .../fixtures/responses_EpPolicyEdit.json | 43 +- .../fixtures/result_current_RestSend.json | 4 + .../test_image_policy_replace_bulk.py | 618 +++++++++++------- .../test_image_policy_update.py | 4 +- .../test_image_policy_update_bulk.py | 56 +- .../modules/dcnm/dcnm_image_policy/utils.py | 9 +- 12 files changed, 816 insertions(+), 346 deletions(-) diff --git a/plugins/module_utils/image_policy/replace.py b/plugins/module_utils/image_policy/replace.py index 5daf2ded4..8862ab254 100644 --- a/plugins/module_utils/image_policy/replace.py +++ b/plugins/module_utils/image_policy/replace.py @@ -22,14 +22,14 @@ import json import logging +from ansible_collections.cisco.dcnm.plugins.module_utils.common.api.v1.imagemanagement.rest.policymgnt.policymgnt import \ + EpPolicyEdit from ansible_collections.cisco.dcnm.plugins.module_utils.common.merge_dicts_v2 import \ MergeDicts from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ Properties from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ ImagePolicies @@ -101,14 +101,14 @@ def __init__(self): msg += f"action: {self.action}, " self.log.debug(msg) - self.endpoints = ApiEndpoints() self._image_policies = ImagePolicies() self._image_policies.results = Results() - self._payloads_to_commit = [] + self.endpoint = EpPolicyEdit() + self.path = self.endpoint.path + self.verb = self.endpoint.verb - self.path = self.endpoints.policy_edit.get("path") - self.verb = self.endpoints.policy_edit.get("verb") + self._payloads_to_commit = [] self._mandatory_payload_keys = set() self._mandatory_payload_keys.add("nxosVersion") @@ -120,7 +120,7 @@ def __init__(self): self._rest_send = None self._results = None - def _verify_payload(self, payload): + def verify_payload(self, payload): """ ### Summary Verify that the payload is a dict and contains all mandatory keys. @@ -186,7 +186,7 @@ def _verify_image_policy_ref_count(self, instance, policy_names): msg += f"ref_count: {ref_count}. " raise ValueError(msg) - def _default_policy(self, policy_name): + def default_policy(self, policy_name): """ ### Summary Return a default policy payload for policy name. @@ -216,7 +216,7 @@ def _default_policy(self, policy_name): } return policy - def _build_payloads_to_commit(self): + def build_payloads_to_commit(self): """ ### Summary Build the payloads to commit to the controller. @@ -228,16 +228,16 @@ def _build_payloads_to_commit(self): - ref_count for any policy is not 0. """ method_name = inspect.stack()[0][3] - if self.payloads is None: - msg = f"{self.class_name}.{method_name}: " - msg += "payloads must be set prior to calling commit." - raise ValueError(msg) - self._image_policies.rest_send = self.rest_send # pylint: disable=no-member + # pylint: disable=no-member + self._image_policies.rest_send = self.rest_send + # pylint: enable=no-member self._image_policies.refresh() - msg = f"self.payloads: {json.dumps(self.payloads, indent=4, sort_keys=True)}" + msg = "self.payloads: " + msg += f"{json.dumps(self.payloads, indent=4, sort_keys=True)}" self.log.debug(msg) + # Populate a list of policies on the contoller that match our payloads controller_policies = [] policy_names = [] @@ -247,11 +247,12 @@ def _build_payloads_to_commit(self): controller_policies.append(payload) policy_names.append(payload["policyName"]) - msg = f"controller_policies: {json.dumps(controller_policies, indent=4, sort_keys=True)}" + msg = "controller_policies: " + msg += f"{json.dumps(controller_policies, indent=4, sort_keys=True)}" self.log.debug(msg) - # raise ValueError if the ref_count for any policy is not 0 (i.e. the policy is - # in use and cannot be replaced) + # raise ValueError if the ref_count for any policy is not 0 (i.e. the + # policy is in use and cannot be replaced) try: self._verify_image_policy_ref_count(self._image_policies, policy_names) except ValueError as error: @@ -266,7 +267,7 @@ def _build_payloads_to_commit(self): self._payloads_to_commit = [] for payload in controller_policies: merge = MergeDicts() - merge.dict1 = copy.deepcopy(self._default_policy(payload["policyName"])) + merge.dict1 = copy.deepcopy(self.default_policy(payload["policyName"])) merge.dict2 = payload msg = f"merge.dict1: {json.dumps(merge.dict1, indent=4, sort_keys=True)}" self.log.debug(msg) @@ -278,7 +279,7 @@ def _build_payloads_to_commit(self): msg += f"{json.dumps(self._payloads_to_commit, indent=4, sort_keys=True)}" self.log.debug(msg) - def _send_payloads(self): + def send_payloads(self): """ ### Summary Send the payloads in self._payloads_to_commit to the controller @@ -293,7 +294,7 @@ def _send_payloads(self): for payload in self._payloads_to_commit: try: - self._send_payload(payload) + self.send_payload(payload) except ValueError as error: msg = f"{self.class_name}.{method_name}: " msg += "Error while sending payloads. " @@ -301,7 +302,7 @@ def _send_payloads(self): raise ValueError(msg) from error # pylint: disable=no-member - def _send_payload(self, payload): + def send_payload(self, payload): """ ### Summary Send one payload to the controller @@ -349,7 +350,6 @@ def _send_payload(self, payload): else: self.results.diff_current = copy.deepcopy(payload) - # self.send_payload_result[payload["FABRIC_NAME"]] = self.rest_send.result_current["success"] self.results.action = self.action self.results.check_mode = self.params.get("check_mode") self.results.state = self.params.get("state") @@ -359,15 +359,37 @@ def _send_payload(self, payload): def commit(self): """ - Commit the payloads to the controller + ### Summary + Commit the payloads to the controller. + ### Raises + - ``ValueError`` if payloads, results, or rest_send are not set prior + to calling commit. """ - self._build_payloads_to_commit() - self._send_payloads() + method_name = inspect.stack()[0][3] + if self.payloads is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"payloads must be set prior to calling {method_name}." + raise ValueError(msg) + if self.results is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"results must be set prior to calling {method_name}." + raise ValueError(msg) + if self.rest_send is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"rest_send must be set prior to calling {method_name}." + raise ValueError(msg) + + self.build_payloads_to_commit() + self.send_payloads() @property def payloads(self): """ - return the policy payloads + ### Summary + Return the policy payloads + + ### Raises + - ``TypeError`` if payloads is not a list. """ return self._payloads @@ -381,5 +403,5 @@ def payloads(self, value): msg += f"value {value}" raise TypeError(msg) for item in value: - self._verify_payload(item) + self.verify_payload(item) self._payloads = value diff --git a/plugins/module_utils/image_policy/update.py b/plugins/module_utils/image_policy/update.py index 4d2f28181..3208911ba 100644 --- a/plugins/module_utils/image_policy/update.py +++ b/plugins/module_utils/image_policy/update.py @@ -100,8 +100,9 @@ def verify_payload(self, payload): def build_payloads_to_commit(self): """ - Build a list of payloads to commit. Skip any payloads that - do not exist on the controller. + ### Summary + Build a list of payloads to commit. Skip any payloads that do not + exist on the controller. Expects self.payloads to be a list of dict, with each dict being a payload for the image policy edit API endpoint. @@ -110,6 +111,11 @@ def build_payloads_to_commit(self): to commit. """ method_name = inspect.stack()[0][3] + if self.payloads is None: + msg = f"{self.class_name}.{method_name}: " + msg += "payloads must be set prior to calling commit." + raise ValueError(msg) + self._image_policies.rest_send = self.rest_send # pylint: disable=no-member self._image_policies.refresh() @@ -366,6 +372,7 @@ def commit(self): raise ValueError(msg) self.build_payloads_to_commit() + if len(self._payloads_to_commit) == 0: return self.send_payloads() @@ -451,7 +458,13 @@ def commit(self): msg += f"rest_send must be set prior to calling {method_name}." raise ValueError(msg) - self.build_payloads_to_commit() + try: + self.build_payloads_to_commit() + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Error building payloads to commit. " + msg += f"Error detail: {error}." + raise ValueError(msg) from error if len(self._payloads_to_commit) == 0: return diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyReplaceBulk.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyReplaceBulk.json index 667b810ac..ab0fb1c58 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyReplaceBulk.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyReplaceBulk.json @@ -109,12 +109,25 @@ "nxosVersion": "10.2.5_nxos64-cs_64bit", "packageName": "", "platform": "N9K", - "policyDescr": "KR5M", + "policyDescr": "KR5M replaced", "policyName": "KR5M", "policyType": "PLATFORM", "rpmimages": "" } ], + "test_image_policy_replace_bulk_00034a": [ + { + "agnostic": false, + "epldImgName": "n9000-epld.10.3.2.F.img", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", + "platform": "N9K", + "policyDescr": "image policy of 10.3(3)F", + "policyName": "FOO", + "policyType": "PLATFORM", + "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" + } + ], "test_image_policy_replace_bulk_00035a": [ { "agnostic": false, @@ -157,32 +170,6 @@ } ], "test_image_policy_replace_bulk_00037a": [ - { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.2.F.img", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K", - "policyDescr": "image policy of 10.3(3)F", - "policyName": "FOO", - "policyType": "PLATFORM", - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - ], - "test_image_policy_replace_bulk_00037b": [ - { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.2.F.img", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K", - "policyDescr": "image policy of 10.3(3)F", - "policyName": "BAR", - "policyType": "PLATFORM", - "rpmimages": "" - } - ], - "test_image_policy_replace_bulk_00037d": [ { "agnostic": false, "epldImgName": "n9000-epld.10.3.2.F.img", diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdate.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdate.json index b4c33c794..8472be256 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdate.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdate.json @@ -3,7 +3,7 @@ "Mocked payloads for ImagePolicyUpdateBulk unit tests.", "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py" ], - "test_image_policy_update_00010a":{ + "test_image_policy_update_00020a":{ "agnostic": false, "epldImgName": "n9000-epld.10.3.2.F.img", "nxosVersion": "10.3.1_nxos64-cs_64bit", diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdateBulk.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdateBulk.json index ca0382263..2e4f07515 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdateBulk.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_ImagePolicyUpdateBulk.json @@ -3,7 +3,7 @@ "Mocked payloads for ImagePolicyUpdate unit tests.", "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py" ], - "test_image_policy_update_bulk_00010a": [ + "test_image_policy_update_bulk_00020a": [ { "agnostic": false, "epldImgName": "n9000-epld.10.3.2.F.img", @@ -165,7 +165,7 @@ "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" } ], - "test_image_policy_update_bulk_00037d": [ + "test_image_policy_update_bulk_00037a": [ { "agnostic": false, "epldImgName": "n9000-epld.10.3.2.F.img", diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json index 1535b4a6c..3012c5c0a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json @@ -107,6 +107,290 @@ "message": "" } }, + "test_image_policy_replace_bulk_00030a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_replace_bulk_00031a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_replace_bulk_00032a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_replace_bulk_00034a": { + "TEST_NOTES": [ + "No image policies exist on the controller." + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "test_image_policy_replace_bulk_00035a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_replace_bulk_00036a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_replace_bulk_00037a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "FOO", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "FOO", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "BAR", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "BAR", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, "test_image_policy_update_00030a": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json index c153137b0..1238a47dc 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyEdit.json @@ -3,32 +3,65 @@ "Mocked responses for endpoint EpPolicyEdit.", "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py" ], - "test_image_policy_update_00030a": { + "test_image_policy_replace_bulk_00030a": { "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", "RETURN_CODE": 200 }, - "test_image_policy_update_00035a": { + "test_image_policy_replace_bulk_00032a": { "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", "RETURN_CODE": 200 }, - "test_image_policy_update_00036a": { + "test_image_policy_replace_bulk_00035a": { + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_replace_bulk_00036a": { "DATA": "Internal server error.", "MESSAGE": "NOK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", "RETURN_CODE": 500 }, - "test_image_policy_update_bulk_00030a": { + "test_image_policy_replace_bulk_00037a": { + "DATA": "Policy edited successfully.", + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_replace_bulk_00037b": { + "DATA": "Internal server error.", + "MESSAGE": "NOK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 500 + }, + "test_image_policy_update_00030a": { + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_update_00035a": { "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", "RETURN_CODE": 200 }, - "test_image_policy_update_bulk_00031a": { + "test_image_policy_update_00036a": { + "DATA": "Internal server error.", + "MESSAGE": "NOK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", + "RETURN_CODE": 500 + }, + "test_image_policy_update_bulk_00030a": { "MESSAGE": "OK", "METHOD": "POST", "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/edit-policy", diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/result_current_RestSend.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/result_current_RestSend.json index 90bad89ba..558472702 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/result_current_RestSend.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/result_current_RestSend.json @@ -15,6 +15,10 @@ "changed": true, "success": true }, + "test_image_policy_replace_bulk_00036a": { + "changed": false, + "success": false + }, "test_image_policy_update_00035a": { "changed": true, "success": true diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py index be7a3e90b..52b168cf9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py @@ -29,46 +29,49 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" -from typing import Any, Dict +import copy +import inspect import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ - ImagePolicies +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - GenerateResponses, MockImagePolicies, does_not_raise, - image_policy_replace_bulk_fixture, payloads_image_policy_replace_bulk, - responses_image_policy_replace_bulk, rest_send_result_current, - results_image_policy_replace_bulk) + MockAnsibleModule, MockImagePolicies, does_not_raise, + image_policy_replace_bulk_fixture, params, + payloads_image_policy_replace_bulk, responses_ep_policies, + responses_ep_policy_edit, responses_image_policy_replace_bulk, + rest_send_result_current, results_image_policy_replace_bulk) -def test_image_policy_replace_bulk_00010(image_policy_replace_bulk) -> None: +def test_image_policy_replace_bulk_00000(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - __init__ - Summary + ### Summary Verify that __init__() sets class attributes to the expected values. - Test + ### Test - Class attributes initialized to expected values - - fail_json is not called + - Exceptions are not raised. """ with does_not_raise(): instance = image_policy_replace_bulk assert instance.class_name == "ImagePolicyReplaceBulk" assert instance.action == "replace" - assert instance.state == "replaced" - assert instance.check_mode is False - assert isinstance(instance.endpoints, ApiEndpoints) - assert instance.path == ApiEndpoints().policy_edit["path"] - assert instance.verb == ApiEndpoints().policy_edit["verb"] + assert instance.params.get("state") == "replaced" + assert instance.params.get("check_mode") is False + assert instance.endpoint.class_name == "EpPolicyEdit" + assert instance.endpoint.verb == "POST" assert instance._mandatory_payload_keys == { "nxosVersion", "policyName", @@ -76,25 +79,27 @@ def test_image_policy_replace_bulk_00010(image_policy_replace_bulk) -> None: } assert instance.payloads is None assert instance._payloads_to_commit == [] - assert isinstance(instance._image_policies, ImagePolicies) + assert instance._image_policies.class_name == "ImagePolicies" + assert instance._image_policies.results.class_name == "Results" def test_image_policy_replace_bulk_00020(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - __init__ - payloads setter - Summary + ### Summary Verify that the payloads setter sets the payloads attribute to the expected value. - Test + ### Test - payloads is set to expected value - - fail_json is not called + - Exceptions are not raised. """ - key = "test_image_policy_replace_bulk_00020a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_replace_bulk @@ -104,25 +109,28 @@ def test_image_policy_replace_bulk_00020(image_policy_replace_bulk) -> None: def test_image_policy_replace_bulk_00021(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - __init__ - payload setter - Summary - Verify that the payloads setter calls fail_json when payloads is not a list of dict + ### Summary + Verify that the payloads setter raises ``TypeError`` when payloads is not + a list of dict. - Test - - fail_json is called because payloads is not a list - - instance.payloads is not modified, hence it retains its initial value of None + ### Test + - ``TypeError`` is raised because payloads is not a list. + - ``instance.payloads`` is not modified. """ - key = "test_image_policy_replace_bulk_00021a" - match = "ImagePolicyReplaceBulk.payloads: " - match += "payloads must be a list of dict. got dict for value" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_replace_bulk - with pytest.raises(AnsibleFailJson, match=match): + + match = r"ImagePolicyReplaceBulk.payloads:\s+" + match += r"payloads must be a list of dict\. got dict for value.*" + with pytest.raises(TypeError, match=match): instance.payloads = payloads_image_policy_replace_bulk(key) assert instance.payloads is None @@ -137,288 +145,386 @@ def test_image_policy_replace_bulk_00021(image_policy_replace_bulk) -> None: ) def test_image_policy_replace_bulk_00022(image_policy_replace_bulk, key, match) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - __init__ - payloads setter - Test - - fail_json is called because a payload in the payloads list is missing a mandatory key - - instance.payloads is not modified, hence it retains its initial value of None + ### Summary + Verify that ``payloads.setter`` raises ``ValueError when a payload is + missing a mandatory key + + ### Test + - ``ValueError`` is raised because payload is missing a mandatory key. + - ``instance.payload`` is not modified. """ with does_not_raise(): instance = image_policy_replace_bulk instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): + with pytest.raises(ValueError, match=match): instance.payloads = payloads_image_policy_replace_bulk(key) assert instance.payloads is None def test_image_policy_replace_bulk_00023(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - __init__ - payload setter - Summary - Verify that the payloads setter calls fail_json when payloads is a list - but contains an element that is not a dict. + ### Summary + Verify that ``payloads.setter` raises ``TypeError`` when payloads is + a list but contains an element that is not a dict. - Test - - fail_json is called because payloads is a list, but contains a non-dict element - - instance.payloads is not modified, hence it retains its initial value of None + ### Test + - ``TypeError`` is raised because payloads is a list, but contains a + non-dict element. + - ``instance.payloads`` is not modified. """ - key = "test_image_policy_replace_bulk_00023a" - match = "ImagePolicyReplaceBulk._verify_payload: " - match += "payload must be a dict. Got type str, value " - match += "IM_A_STRING_BUT_SHOULD_BE_A_DICT" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" with does_not_raise(): instance = image_policy_replace_bulk instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): + + match = r"ImagePolicyReplaceBulk\.verify_payload:\s+" + match += r"payload must be a dict\. Got type str, value\s+" + match += r"IM_A_STRING_BUT_SHOULD_BE_A_DICT" + with pytest.raises(TypeError, match=match): instance.payloads = payloads_image_policy_replace_bulk(key) assert instance.payloads is None -def test_image_policy_replace_bulk_00030( - monkeypatch, image_policy_replace_bulk -) -> None: +def test_image_policy_replace_bulk_00030(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - __init__() - - _build_payloads_to_commit() + - build_payloads_to_commit() - _verify_image_policy_ref_count() - payloads setter - Summary - Verify _build_payloads_to_commit() behavior when a request contains one + ### Summary + Verify build_payloads_to_commit() behavior when a request contains one image policy that exists on the controller and the caller has requested to replace it. The replaced image policy contains a different policyDescr. - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), - is mocked to indicate that two image policies (KR5M, NR3F) exist on the - controller. + ### Setup + - EpPolicies() endpoint response is mocked to indicate that two image + policies (KR5M, NR3F) exist on the controller. - ImagePolicyReplaceBulk().payloads is set to contain one payload (KR5M) - that is present in all_policies. + that is present on the controller. - Test - - payloads_to_commit will contain payload for KR5M since it exists on the controller - and the caller has requested to replace it. - - Since this is a full payload, MergeDicts doesn't apply any defaults to it. + ### Test + - payloads_to_commit will contain payload for KR5M since it exists on + the controller and the caller has requested to update it. + - Since this is a full payload, MergeDicts doesn't apply any defaults + to it. """ - key = "test_image_policy_replace_bulk_00030a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_replace_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_replace_bulk instance.results = Results() - instance.payloads = payloads_image_policy_replace_bulk(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next + instance.commit() + assert instance._payloads_to_commit == payloads_image_policy_replace_bulk(key) assert instance._payloads_to_commit[0]["policyName"] == "KR5M" assert instance._payloads_to_commit[0]["policyDescr"] == "KR5M Replaced" -def test_image_policy_replace_bulk_00031( - monkeypatch, image_policy_replace_bulk -) -> None: +def test_image_policy_replace_bulk_00031(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - __init__() - - _build_payloads_to_commit() + - build_payloads_to_commit() - _verify_image_policy_ref_count() - payloads setter - Summary - Verify behavior when a request to replace an image policy is sent for - an image policy that does not exist on the controller + ### Summary + Verify behavior when a request is sent to replace an image policy + that does not exist on the controller - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), - is mocked to indicate that two image policies (KR5M, NR3F) exist on the - controller. + ### Expected behavior + ``instance.build_payloads_to_commit()`` does not add a payload + to the ``payloads_to_commit`` list if the associated policy + does not exist on the controller. + + ### Setup + - EpPolicies() endpoint response is mocked to indicate that two image + policies (KR5M, NR3F) exist on the controller. - ImagePolicyReplaceBulk().payloads is set to contain one payload containing an image policy (FOO) that is not present on the controller. - Test - - fail_json is not called - - _payloads_to_commit be an empty list since policy FOO does not + ### Test + - Exceptions are not raised. + - _payloads_to_commit is an empty list since policy FOO does not exist on the controller. """ - key = "test_image_policy_replace_bulk_00031a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_replace_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_replace_bulk instance.results = Results() - instance.payloads = payloads_image_policy_replace_bulk(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next + instance.commit() + assert instance._payloads_to_commit == [] + assert len(instance.results.failed) == 0 + assert len(instance.results.changed) == 0 -def test_image_policy_replace_bulk_00032( - monkeypatch, image_policy_replace_bulk -) -> None: +def test_image_policy_replace_bulk_00032(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - __init__() - - _build_payloads_to_commit() + - build_payloads_to_commit() - _verify_image_policy_ref_count() - payloads setter - Summary - Verify _build_payloads_to_commit() behavior when a request contains one + ### Summary + Verify build_payloads_to_commit() behavior when a request contains one image policy that does not exist on the controller and one image policy that exists on the controller. - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), - is mocked to indicate that two image policies (KR5M, NR3F) exist on the - controller. + ### Setup + - EpPolicies() endpoint response is mocked to indicate that two image + policies (KR5M, NR3F) exist on the controller. - ImagePolicyReplaceBulk().payloads is set to contain one payload containing an image policy (FOO) that does not exist on the controller and one payload containing an image policy (KR5M) that exists on the controller. - Test - - _payloads_to_commit will contain one payload - - The policyName for this payload will be "KR5M", which is the image policy that - exists on the controller + ### Test + - _payloads_to_commit contains one payload. + - The policyName for this payload is "KR5M", which is the image policy + that exists on the controller. + - The policyDesc for this payload is "KR5M replaced", which is the new + image policy description sent to the controller for the replaced state + update. """ - key = "test_image_policy_replace_bulk_00032a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_replace_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_replace_bulk instance.results = Results() - instance.payloads = payloads_image_policy_replace_bulk(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._build_payloads_to_commit() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next + instance.commit() + assert len(instance._payloads_to_commit) == 1 assert instance._payloads_to_commit[0]["policyName"] == "KR5M" + assert instance._payloads_to_commit[0]["policyDescr"] == "KR5M replaced" def test_image_policy_replace_bulk_00033(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - commit() - - _build_payloads_to_commit - - fail_json - Summary - Verify that _build_payloads_to_commit() calls fail_json when - payloads is not set. + ### Summary + Verify that commit() raises ``ValueError`` when payloads is not set. - Setup - - ImagePolicyReplaceBulk().payloads is not set + ### Setup + - ImagePolicyReplaceBulk().payloads is not set. - Test - - fail_json is called because payloads is None + ### Test + - ``ValueError`` is raised because payloads is None. """ with does_not_raise(): instance = image_policy_replace_bulk - match = ( - "ImagePolicyReplaceBulk._build_payloads_to_commit: payloads must be " - "set prior to calling commit." - ) - with pytest.raises(AnsibleFailJson, match=match): + match = r"ImagePolicyReplaceBulk\.commit:\s+" + match += r"payloads must be set prior to calling commit\." + with pytest.raises(ValueError, match=match): instance.commit() -def test_image_policy_replace_bulk_00034( - monkeypatch, image_policy_replace_bulk -) -> None: +def test_image_policy_replace_bulk_00034(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - payloads setter - commit() - - _build_payloads_to_commit() + - build_payloads_to_commit() - Summary + ### Summary Verify that commit() returns without doing anything when payloads - is set to an empty list. + is set to a policy that does not exist on the controller. - Setup - - ImagePolicyReplaceBulk().payloads is set to an empty list + ### Setup + ### Setup + - EpPolicies() endpoint response is mocked to indicate that no + policies exist on the controller. + - ImagePolicyReplaceBulk().payload is set to a policy (FOO) that does not + exist on the controller - Test - - ImagePolicyReplaceBulk().commit returns without doing anything + ### Test + - ImagePolicyReplaceBulk().commit returns without doing anything. """ - key = "test_image_policy_replace_bulk_00034a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" - with does_not_raise(): - instance = image_policy_replace_bulk - instance.results = Results() - instance.payloads = [] + def responses(): + yield responses_ep_policies(key) + + gen_responses = ResponseGenerator(responses()) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) + def payloads(): + yield payloads_image_policy_replace_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): + instance = image_policy_replace_bulk + instance.results = Results() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next instance.commit() + assert instance._payloads_to_commit == [] + assert len(instance.results.changed) == 0 + assert len(instance.results.failed) == 0 -def test_image_policy_replace_bulk_00035( - monkeypatch, image_policy_replace_bulk -) -> None: +def test_image_policy_replace_bulk_00035(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - - _build_payloads_to_commit() - - _send_payloads() - - payloads setter + - build_payloads_to_commit() + - send_payloads() + - payloads.setter - commit() - Summary - Verify behavior when a request is made to replace two image policies - that exist on the controller. + ### Summary + Verify ImagePolicyUpdateBulk.commit() happy path. Controller returns + a 200 response to an image policy update request. - Setup - - ImagePolicies().all_policies, called from instance._build_payloads_to_commit(), - is mocked to indicate that two policies (KR5M, NR3F) exist on the controller. + ### Setup + - EpPolicies() endpoint response is mocked to indicate that two policies + (KR5M, NR3F) exist on the controller. - ImagePolicyReplaceBulk().payloads is set to contain payloads for KR5M and NR3F in which policyDescr is different from the existing policyDescr. - - dcnm_send is mocked to return a successful (200) response. + - EpPolicyEdit() endpoint response is mocked to return a successful + (200) response. - Test - - commit calls _build_payloads_to_commit which returns two payloads - - commit calls _send_payloads, which calls results.register_task_result() - to update the results. + ### Test + - commit calls build_payloads_to_commit which returns two payloads. + - commit calls ``send_payloads``, which calls + ``results.register_task_result()`` to update the results. - results.* are set to the expected values """ - key = "test_image_policy_replace_bulk_00035a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_replace_bulk(key) + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) + yield responses_ep_policy_edit(key) - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + gen_responses = ResponseGenerator(responses()) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + def payloads(): + yield payloads_image_policy_replace_bulk(key) - with does_not_raise(): - instance = image_policy_replace_bulk - instance.results = Results() + gen_payloads = ResponseGenerator(payloads()) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): - instance.payloads = payloads_image_policy_replace_bulk(key) + instance = image_policy_replace_bulk + instance.results = Results() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next instance.commit() - payload_0 = payloads_image_policy_replace_bulk(key)[0] - # sequence_number is added by the Results class - payload_0["sequence_number"] = 1 + response_current = responses_image_policy_replace_bulk(key) + response_current["sequence_number"] = 1 + + result_current = rest_send_result_current(key) + result_current["sequence_number"] = 1 - payload_1 = payloads_image_policy_replace_bulk(key)[1] - payload_1["sequence_number"] = 2 + # Add the sequence_number to the diff for comparison, since we add + # it in the results.register_task_result() method. + diff_compare = copy.deepcopy(payloads_image_policy_replace_bulk(key)) + sequence_number = 1 + for item in diff_compare: + item.update({"sequence_number": sequence_number}) + sequence_number += 1 assert instance.results.action == "replace" assert instance.rest_send.result_current == rest_send_result_current(key) @@ -427,8 +533,8 @@ def mock_dcnm_send(*args, **kwargs): assert len(instance.results.response) == 2 assert instance.results.result[0].get("sequence_number") == 1 assert instance.results.result[1].get("sequence_number") == 2 - assert instance.results.diff[0] == payload_0 - assert instance.results.diff[1] == payload_1 + assert instance.results.diff[0] == diff_compare[0] + assert instance.results.diff[1] == diff_compare[1] assert instance.results.diff[0].get("policyDescr") == "KR5M replaced" assert instance.results.diff[1].get("policyDescr") == "NR3F replaced" assert False in instance.results.failed @@ -444,58 +550,71 @@ def mock_dcnm_send(*args, **kwargs): assert instance.results.metadata[1]["sequence_number"] == 2 -def test_image_policy_replace_bulk_00036( - monkeypatch, image_policy_replace_bulk -) -> None: +def test_image_policy_replace_bulk_00036(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - - _build_payloads_to_commit() - - _send_payloads() + - build_payloads_to_commit() + - send_payloads() - payloads setter - commit() - Summary - Verify behavior when the controller returns a 500 response to an - image policy replace request + ### Summary + Verify ImagePolicyReplaceBulk.commit() sad path. Controller returns + a 500 response to an image policy update request. - Setup - - ImagePolicies().all_policies, is mocked to indicate that one policy + ### Setup + - EpPolicies() endpoint response is mocked to indicate that one policy (KR5M) exists on the controller. - ImagePolicyReplaceBulk().payloads is set to contain the payload for image policy KR5M with policyDescr changed. - - dcnm_send is mocked to return a failure (500) response. + - EpPolicyEdit() endpoint response is mocked to return a failed + (500) response. - Test - - commit calls _build_payloads_to_commit which returns one payload - - commit calls _send_payloads, which populates response_ok, result_ok, + ### Test + - commit calls build_payloads_to_commit which returns one payload + - commit calls send_payloads, which populates response_ok, result_ok, diff_ok, response_nok, result_nok, and diff_nok based on the payload - returned from _build_payloads_to_commit and the failure response + returned from build_payloads_to_commit and the failure response - response_ok, result_ok, and diff_ok are set to empty lists - response_nok, result_nok, and diff_nok are set to expected values """ - key = "test_image_policy_replace_bulk_00036a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_edit(key) - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_replace_bulk(key) + gen_responses = ResponseGenerator(responses()) - with does_not_raise(): - instance = image_policy_replace_bulk - instance.rest_send.unit_test = True - instance.results = Results() - instance.payloads = payloads_image_policy_replace_bulk(key) + def payloads(): + yield payloads_image_policy_replace_bulk(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.timeout = 1 + rest_send.unit_test = True with does_not_raise(): + instance = image_policy_replace_bulk + instance.results = Results() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next instance.commit() response_current = responses_image_policy_replace_bulk(key) response_current["sequence_number"] = 1 + + result_current = rest_send_result_current(key) + result_current["sequence_number"] = 1 + assert instance.results.response_current == response_current assert instance.results.diff_current == {"sequence_number": 1} assert True in instance.results.failed @@ -510,25 +629,23 @@ def mock_dcnm_send(*args, **kwargs): assert instance.results.metadata[0]["sequence_number"] == 1 -def test_image_policy_replace_bulk_00037( - monkeypatch, image_policy_replace_bulk -) -> None: +def test_image_policy_replace_bulk_00037(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyCreateCommon - _process_responses() - ImagePolicyCreateBulk - __init__() - Summary + ### Summary Verify behavior when the controller returns a 200 response to an image policy replace request, followed by a 500 response to a subsequent image policy replace request. - Setup - - instance.payloads is set to contain two payloads + ### Setup + - instance.payloads is set to contain two payloads. - Test + ### Test - Both successful and bad responses are recorded with separate sequence_numbers. - instance.results.failed will be a set() containing both True and False - instance.results.changed will be a set() containing both True and False @@ -536,34 +653,37 @@ def test_image_policy_replace_bulk_00037( - instance.results.result contains two results - instance.results.diff contains two diffs """ - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" - key_policies = "test_image_policy_replace_bulk_00037a" - key_ok = "test_image_policy_replace_bulk_00037b" - key_nok = "test_image_policy_replace_bulk_00037c" - key_payloads = "test_image_policy_replace_bulk_00037d" + key_ok = "test_image_policy_replace_bulk_00037a" + key_nok = "test_image_policy_replace_bulk_00037b" + key_payloads = "test_image_policy_replace_bulk_00037a" def responses(): - yield responses_image_policy_replace_bulk(key_policies) - yield responses_image_policy_replace_bulk(key_ok) - yield responses_image_policy_replace_bulk(key_nok) + yield responses_ep_policies(key_policies) + yield responses_ep_policy_edit(key_ok) + yield responses_ep_policy_edit(key_nok) - gen = GenerateResponses(responses()) + gen_responses = ResponseGenerator(responses()) - def mock_dcnm_send(*args, **kwargs): - item = gen.next - return item + def payloads(): + yield payloads_image_policy_replace_bulk(key_payloads) - with does_not_raise(): - instance = image_policy_replace_bulk - instance.rest_send.unit_test = True - instance.results = Results() - instance.payloads = payloads_image_policy_replace_bulk(key_payloads) + gen_payloads = ResponseGenerator(payloads()) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.timeout = 1 + rest_send.unit_test = True with does_not_raise(): + instance = image_policy_replace_bulk + instance.results = Results() + instance.rest_send = rest_send + instance.payloads = gen_payloads.next instance.commit() assert len(instance.results.diff) == 2 @@ -580,24 +700,24 @@ def mock_dcnm_send(*args, **kwargs): def test_image_policy_replace_bulk_00040(image_policy_replace_bulk) -> None: """ - Classes and Methods + ### Classes and Methods - ImagePolicyReplaceBulk - __init__ - - _default_policy + - default_policy - Summary - Verify that instance._default_policy setter calls fail_json when - passed a policy_name that is not a string. + ### Summary + Verify that instance.default_policy raises ``TypeError`` when + ``policy_name`` is not a string. - Test - - fail_json is called because policy_name is a list + ### Test + - ``TypeError``is raised because ``policy_name`` is a list. """ - match = "ImagePolicyReplaceBulk._default_policy: " - match += "policy_name must be a string. " - match += r"Got type list for value \[\]" - with does_not_raise(): instance = image_policy_replace_bulk instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): - instance._default_policy([]) + + match = r"ImagePolicyReplaceBulk\.default_policy:\s+" + match += r"policy_name must be a string\.\s+" + match += r"Got type list for value \[\]" + with pytest.raises(TypeError, match=match): + instance.default_policy([]) diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py index 3b3ee3125..7c7040bf5 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update.py @@ -78,9 +78,11 @@ def test_image_policy_update_00000(image_policy_update) -> None: } assert instance.payload is None assert instance._payloads_to_commit == [] + assert instance._image_policies.class_name == "ImagePolicies" + assert instance._image_policies.results.class_name == "Results" -def test_image_policy_update_00010(image_policy_update) -> None: +def test_image_policy_update_00020(image_policy_update) -> None: """ ### Classes and Methods - ImagePolicyUpdate diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py index 3af6dfd4d..567573d99 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py @@ -60,8 +60,8 @@ def test_image_policy_update_bulk_00000(image_policy_update_bulk) -> None: Verify that __init__() sets class attributes to the expected values. ### Test - - Class attributes initialized to expected values - - fail_json is not called + - Class attributes initialized to expected values. + - Exceptions are not raised. """ with does_not_raise(): instance = image_policy_update_bulk @@ -78,9 +78,11 @@ def test_image_policy_update_bulk_00000(image_policy_update_bulk) -> None: } assert instance.payloads is None assert instance._payloads_to_commit == [] + assert instance._image_policies.class_name == "ImagePolicies" + assert instance._image_policies.results.class_name == "Results" -def test_image_policy_update_bulk_00010(image_policy_update_bulk) -> None: +def test_image_policy_update_bulk_00020(image_policy_update_bulk) -> None: """ ### Classes and Methods - ImagePolicyUpdateCommon @@ -94,8 +96,8 @@ def test_image_policy_update_bulk_00010(image_policy_update_bulk) -> None: to the expected value. ### Test - - payloads is set to expected value - - fail_json is not called + - payloads is set to expected value. + - Exceptions are not raised. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -120,8 +122,8 @@ def test_image_policy_update_bulk_00021(image_policy_update_bulk) -> None: a list of dict. ### Test - - ``TypeError`` is raised because payloads is not a list - - instance.payloads is not modified, hence it retains its initial value of None + - ``TypeError`` is raised because payloads is not a list. + - ``instance.payloads`` is not modified. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -224,8 +226,8 @@ def test_image_policy_update_bulk_00030(image_policy_update_bulk) -> None: that is present on the controller. ### Test - - payloads_to_commit will contain payload for KR5M since it exists on the controller - and the caller has requested to update it. + - payloads_to_commit will contain payload for KR5M since it exists on + the controller and the caller has requested to update it. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -280,8 +282,8 @@ def test_image_policy_update_bulk_00031(image_policy_update_bulk) -> None: - payloads setter ### Summary - Verify behavior when a request is sent to update a policy that does - not exist on the controller + Verify behavior when a request is sent to update an image policy that does + not exist on the controller. ### Expected behavior ``instance.build_payloads_to_commit()`` does not add a payload @@ -296,7 +298,7 @@ def test_image_policy_update_bulk_00031(image_policy_update_bulk) -> None: ### Test - Exceptions are not raised. - - _payloads_to_commit will be an empty list since policy FOO does not + - _payloads_to_commit is an empty list since policy FOO does not exist on the controller. """ method_name = inspect.stack()[0][3] @@ -304,7 +306,6 @@ def test_image_policy_update_bulk_00031(image_policy_update_bulk) -> None: def responses(): yield responses_ep_policies(key) - yield responses_ep_policy_edit(key) gen_responses = ResponseGenerator(responses()) @@ -355,8 +356,11 @@ def test_image_policy_update_bulk_00032(image_policy_update_bulk) -> None: ### Test - _payloads_to_commit will contain one payload - - The policyName for this payload will be "KR5M", which is the image policy that - exists on the controllers. + - The policyName for this payload is "KR5M", which is the image policy + that exists on the controller. + - The policyDesc for this payload is "KR5M updated", which is the new + image policy description sent to the controller for the merged state + update. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -396,14 +400,12 @@ def test_image_policy_update_bulk_00033(image_policy_update_bulk) -> None: ### Classes and Methods - ImagePolicyUpdateBulk - commit() - - build_payloads_to_commit ### Summary - Verify that build_payloads_to_commit() raises ``ValueError`` when - payloads is not set. + Verify that commit() raises ``ValueError`` when payloads is not set. ### Setup - - ImagePolicyUpdateBulk().payloads is not set + - ImagePolicyUpdateBulk().payloads is not set. ### Test - ``ValueError`` is raised because payloads is None. @@ -437,7 +439,7 @@ def test_image_policy_update_bulk_00034(image_policy_update_bulk) -> None: exist on the controller ### Test - - ImagePolicyUpdateBulk().commit returns without doing anything + - ImagePolicyUpdateBulk().commit returns without doing anything. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -475,7 +477,7 @@ def test_image_policy_update_bulk_00035(image_policy_update_bulk) -> None: ### Classes and Methods - ImagePolicyUpdateBulk - build_payloads_to_commit() - - _send_payloads() + - send_payloads() - payloads setter - commit() @@ -493,8 +495,8 @@ def test_image_policy_update_bulk_00035(image_policy_update_bulk) -> None: ### Test - commit calls build_payloads_to_commit which returns two payloads. - - commit calls _send_payloads, which calls results.register_task_result() - to update the results. + - commit calls ``send_payloads``, which calls + results.register_task_result() to update the results. - results.* are set to the expected values. """ method_name = inspect.stack()[0][3] @@ -575,12 +577,12 @@ def test_image_policy_update_bulk_00036(image_policy_update_bulk) -> None: - ImagePolicyUpdateCommon - payloads setter - build_payloads_to_commit() - - _send_payloads() + - send_payloads() - ImagePolicyUpdateBulk - commit() ### Summary - Verify ImagePolicyUpdateBulk.commit() happy path. Controller returns + Verify ImagePolicyUpdateBulk.commit() sad path. Controller returns a 500 response to an image policy update request. ### Setup @@ -681,7 +683,7 @@ def test_image_policy_update_bulk_00037(image_policy_update_bulk) -> None: request. ### Setup - - instance.payloads is set to contain two payloads + - instance.payloads is set to contain two payloads. ### Test - Both successful and bad responses are recorded with separate sequence_numbers. @@ -694,7 +696,7 @@ def test_image_policy_update_bulk_00037(image_policy_update_bulk) -> None: key_policies = "test_image_policy_update_bulk_00037a" key_ok = "test_image_policy_update_bulk_00037a" key_nok = "test_image_policy_update_bulk_00037b" - key_payloads = "test_image_policy_update_bulk_00037d" + key_payloads = "test_image_policy_update_bulk_00037a" def responses(): yield responses_ep_policies(key_policies) diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py index cee50d768..90f6d68c8 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py @@ -38,6 +38,7 @@ from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.fixture import \ load_fixture + def get_state(action): if action in ["create", "update"]: state = "merged" @@ -51,6 +52,7 @@ def get_state(action): state = "merged" return state + params = { "state": "merged", "check_mode": False, @@ -60,10 +62,12 @@ def get_state(action): "agnostic": False, "description": "NR1F", "platform": "N9K", - "type": "PLATFORM"} - ] + "type": "PLATFORM", + } + ], } + class GenerateResponses: """ Given a generator, return the items in the generator with @@ -235,7 +239,6 @@ def results(self, value): # https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html - @pytest.fixture(name="image_policy_create") def image_policy_create_fixture(): """ From 2a921a4c8e184fb2c5398e2a051903a90a2dc787 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Sun, 30 Jun 2024 17:09:28 -1000 Subject: [PATCH 216/230] More alignment with v2 classes. 1. test_image_policy_payload.py: Updatee unit tests for Config2Payload and Payload2Config. 2. test_image_policy_replace_bulk.py: Update unit tests for ImagePolicyReplaceBulk() 3. test_image_policy_delete.py: Update unit tests for ImagePolicyDelete() --- plugins/module_utils/image_policy/payload.py | 2 +- .../fixtures/responses_EpPolicies.json | 214 +++++++++ .../fixtures/responses_EpPolicyDelete.json | 36 ++ .../test_image_policy_delete.py | 417 ++++++++++++------ .../test_image_policy_payload.py | 327 +++++++------- .../test_image_policy_replace_bulk.py | 5 +- .../modules/dcnm/dcnm_image_policy/utils.py | 31 +- 7 files changed, 718 insertions(+), 314 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyDelete.json diff --git a/plugins/module_utils/image_policy/payload.py b/plugins/module_utils/image_policy/payload.py index 63c71cb7c..4fe70459a 100644 --- a/plugins/module_utils/image_policy/payload.py +++ b/plugins/module_utils/image_policy/payload.py @@ -133,7 +133,7 @@ def commit(self): raise ValueError(msg) msg = f"{self.class_name}.{method_name}: " - msg += f"HERE 1 STATE: {self.params['state']}" + msg += f"state: {self.params['state']}" self.log.debug(msg) if self.params["state"] in ["deleted", "query"]: diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json index 3012c5c0a..bf0ce63f6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json @@ -107,6 +107,220 @@ "message": "" } }, + "test_image_policy_delete_00030a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_delete_00031a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_delete_00032a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_delete_00034a": { + "TEST_NOTES": [ + "No image policies exist on the controller." + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "test_image_policy_delete_00036a": { + "TEST_NOTES": [ + "No image policies exist on the controller." + ], + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [], + "message": "" + } + }, + "test_image_policy_delete_00037a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_delete_00038a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, "test_image_policy_replace_bulk_00030a": { "RETURN_CODE": 200, "METHOD": "GET", diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyDelete.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyDelete.json new file mode 100644 index 000000000..a52acd263 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyDelete.json @@ -0,0 +1,36 @@ +{ + "TEST_NOTES": [ + "Mocked responses for ImagePolicyDelete unit tests.", + "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py" + ], + "test_image_policy_delete_00031a": { + "MESSAGE": "OK", + "METHOD": "DELETE", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policy", + "RETURN_CODE": 200 + }, + "test_image_policy_delete_00032a": { + "MESSAGE": "OK", + "METHOD": "DELETE", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policy", + "RETURN_CODE": 200 + }, + "test_image_policy_delete_00034a": { + "MESSAGE": "OK", + "METHOD": "DELETE", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policy", + "RETURN_CODE": 200 + }, + "test_image_policy_delete_00037a": { + "MESSAGE": "OK", + "METHOD": "DELETE", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policy", + "RETURN_CODE": 200 + }, + "test_image_policy_delete_00038a": { + "MESSAGE": "NOK", + "METHOD": "DELETE", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policy", + "RETURN_CODE": 500 + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py index f47be00cc..886ec3248 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py @@ -29,19 +29,26 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import inspect + import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - MockImagePolicies, does_not_raise, image_policy_delete_fixture, - responses_image_policy_delete, results_image_policy_delete) + MockAnsibleModule, MockImagePolicies, does_not_raise, + image_policy_delete_fixture, params, responses_ep_policies, + responses_ep_policy_delete, results_image_policy_delete) -def test_image_policy_delete_00010(image_policy_delete) -> None: +def test_image_policy_delete_00000(image_policy_delete) -> None: """ ### Classes and Methods - ImagePolicyDelete @@ -49,24 +56,31 @@ def test_image_policy_delete_00010(image_policy_delete) -> None: ### Summary Verify that the class attributes are initialized to expected values - and that fail_json is not called. + and that exceptions are not raised. ### Test - Class attributes are initialized to expected values - - fail_json is not called + - Exceptions are not raised. """ with does_not_raise(): instance = image_policy_delete - assert instance.class_name == "ImagePolicyDelete" assert instance.action == "delete" - assert instance.state == "deleted" - assert isinstance(instance.endpoints, ApiEndpoints) + assert instance.check_mode is None + assert instance.class_name == "ImagePolicyDelete" + assert instance.endpoint.class_name == "EpPolicyDelete" + assert instance.params.get("state") == "deleted" + assert instance.payload is None + assert instance.state is None assert instance.verb == "DELETE" assert ( instance.path == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policy" ) assert instance.policy_names is None + assert instance._policies_to_delete == [] + assert instance._policy_names is None + assert instance._results is None + assert instance._rest_send is None def test_image_policy_delete_00020(image_policy_delete) -> None: @@ -74,16 +88,14 @@ def test_image_policy_delete_00020(image_policy_delete) -> None: ### Classes and Methods - ImagePolicyDelete - __init__() - - policy_names setter + - policy_names.setter ### Summary - policy_names is set correctly to a list of strings. - Verify that instance.policy_names is set to the expected value - and that fail_json is not called. + Verify that ``policy_names`` is set correctly to a list of strings. ### Test - - policy_names is set to expected value. - - No exceptions are raised. + - ``policy_names`` is set to expected value. + - Exceptions are not raised. """ policy_names = ["FOO", "BAR"] with does_not_raise(): @@ -97,22 +109,24 @@ def test_image_policy_delete_00021(image_policy_delete) -> None: ### Classes and Methods - ImagePolicyDelete - __init__() - - policy_names setter + - policy_names.setter ### Summary - policy_names should be a list of strings, but it set to a string. - Verify that fail_json is called with appropriate message. + Verify that ``policy_names.setter`` raises ''TypeError'' when + ``policy_names`` is not a list. ### Test - - fail_json is called because policy_names is not a list - - instance.policy_names is not modified, hence it retains its initial value of None + - ``TypeError`` is raised because`` policy_names`` is not a list. + - The error message matches expectations. + - ``instance.policy_names`` is not modified. """ - match = "ImagePolicyDelete.policy_names: " - match += "policy_names must be a list." with does_not_raise(): instance = image_policy_delete - with pytest.raises(AnsibleFailJson, match=match): + + match = r"ImagePolicyDelete.policy_names:\s+" + match += r"policy_names must be a list\." + with pytest.raises(TypeError, match=match): instance.policy_names = "NOT_A_LIST" assert instance.policy_names is None @@ -122,33 +136,35 @@ def test_image_policy_delete_00022(image_policy_delete) -> None: ### Classes and Methods - ImagePolicyDelete - __init__() - - policy_names setter + - policy_names.setter ### Summary - policy_names is set to a list of non-strings. - Verify that fail_json is called with appropriate message. + Verify that ``policy_names.setter`` raises ''TypeError'' when + ``policy_names`` is a list containing non-strings. ### Test - - fail_json is called because policy_names is a list with a non-string element - - instance.policy_names is not modified, hence it retains its initial value of None + - ``TypeError`` is raised because`` policy_names`` contains elements + that are not strings. + - The error message matches expectations. + - ``instance.policy_names`` is not modified. """ - match = "ImagePolicyDelete.policy_names: " - match += "policy_names must be a list of strings." - with does_not_raise(): instance = image_policy_delete - with pytest.raises(AnsibleFailJson, match=match): + + match = r"ImagePolicyDelete.policy_names:\s+" + match += r"policy_names must be a list of strings\." + with pytest.raises(TypeError, match=match): instance.policy_names = [1, 2, 3] assert instance.policy_names is None -def test_image_policy_delete_00030(monkeypatch, image_policy_delete) -> None: +def test_image_policy_delete_00030(image_policy_delete) -> None: """ ### Classes and Methods - ImagePolicyDelete - __init__() - _verify_image_policy_ref_count() - - policy_names setter + - policy_names.setter - _get_policies_to_delete() ### Summary @@ -156,8 +172,8 @@ def test_image_policy_delete_00030(monkeypatch, image_policy_delete) -> None: Verify that instance._policies_to_delete is an empty list. ### Setup - - ImagePolicies().all_policies, is mocked to indicate that two image policies - (KR5M, NR3F) exist on the controller. + - EpPolicies() endpoint response is mocked to indicate that two image + policies (KR5M, NR3F) exist on the controller. - ImagePolicyDelete.policy_names is set to contain one policy_name (FOO) that does not exist on the controller. @@ -166,21 +182,50 @@ def test_image_policy_delete_00030(monkeypatch, image_policy_delete) -> None: policy_names in instance.policy_names do not exist on the controller and, hence, nothing needs to be deleted. """ - key = "test_image_policy_delete_00030a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + + gen_responses = ResponseGenerator(responses()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = image_policy_delete + instance.results = Results() + instance.rest_send = rest_send + instance.policy_names = ["FOO"] + instance.commit() - instance = image_policy_delete - instance.policy_names = ["FOO"] - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._get_policies_to_delete() assert instance._policies_to_delete == [] + assert False in instance.results.changed + assert instance.results.metadata[0]["action"] == "delete" + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[0]["sequence_number"] == 1 + assert instance.results.metadata[0]["state"] == "deleted" + + assert instance.results.response[0]["RETURN_CODE"] == 200 + assert instance.results.response[0]["MESSAGE"] == "No image policies to delete" + assert instance.results.response[0]["sequence_number"] == 1 + + assert instance.results.result[0]["changed"] is False + assert instance.results.result[0]["success"] is True + assert instance.results.result[0]["sequence_number"] == 1 -def test_image_policy_delete_00031(monkeypatch, image_policy_delete) -> None: +def test_image_policy_delete_00031(image_policy_delete) -> None: """ ### Classes and Methods - ImagePolicyDelete - __init__() - - policy_names setter + - policy_names.setter - _get_policies_to_delete() ### Summary @@ -188,7 +233,7 @@ def test_image_policy_delete_00031(monkeypatch, image_policy_delete) -> None: Verify that instance._policies_to_delete contains the policy name KR5M. ### Setup - - ImagePolicies().all_policies is mocked to indicate that two image policies + - EpPolicies() endpoint response is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyDelete.policy_names is set to contain one policy_name (KR5M) that exists on the controller. @@ -196,20 +241,50 @@ def test_image_policy_delete_00031(monkeypatch, image_policy_delete) -> None: ### Test - instance._policies_to_delete will contain one policy name (KR5M) """ - key = "test_image_policy_delete_00031a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_delete(key) + + gen_responses = ResponseGenerator(responses()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = image_policy_delete + instance.results = Results() + instance.rest_send = rest_send + instance.policy_names = ["KR5M"] + instance.commit() - instance = image_policy_delete - instance.policy_names = ["KR5M"] - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._get_policies_to_delete() assert instance._policies_to_delete == ["KR5M"] + assert True in instance.results.changed + assert instance.results.metadata[0]["action"] == "delete" + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[0]["sequence_number"] == 1 + assert instance.results.metadata[0]["state"] == "deleted" + + assert instance.results.response[0]["RETURN_CODE"] == 200 + assert instance.results.response[0]["MESSAGE"] == "OK" + assert instance.results.response[0]["sequence_number"] == 1 + assert instance.results.result[0]["changed"] is True + assert instance.results.result[0]["success"] is True + assert instance.results.result[0]["sequence_number"] == 1 -def test_image_policy_delete_00032(monkeypatch, image_policy_delete) -> None: + +def test_image_policy_delete_00032(image_policy_delete) -> None: """ ### Classes and Methods - ImagePolicyDelete - - policy_names setter + - policy_names.setter - _get_policies_to_delete() ### Summary @@ -218,22 +293,52 @@ def test_image_policy_delete_00032(monkeypatch, image_policy_delete) -> None: that exists on the controller is added to instance._policies_to_delete. ### Setup - - ImagePolicies().all_policies, is mocked to indicate that two image policies + - EpPolicies() endpoint response is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - ImagePolicyDelete().policy_names is set to contain one image policy name (FOO) that does not exist on the controller and one image policy name (KR5M) that does exist on the controller. Test - - instance._policies_to_delete will contain one policy name (KR5M) + - instance._policies_to_delete contains one policy name (KR5M). """ - key = "test_image_policy_delete_00032a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_delete(key) + + gen_responses = ResponseGenerator(responses()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = image_policy_delete + instance.results = Results() + instance.rest_send = rest_send + instance.policy_names = ["FOO", "KR5M"] + instance.commit() - instance = image_policy_delete - instance.policy_names = ["FOO", "KR5M"] - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance._get_policies_to_delete() assert instance._policies_to_delete == ["KR5M"] + assert True in instance.results.changed + assert instance.results.metadata[0]["action"] == "delete" + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[0]["sequence_number"] == 1 + assert instance.results.metadata[0]["state"] == "deleted" + + assert instance.results.response[0]["RETURN_CODE"] == 200 + assert instance.results.response[0]["MESSAGE"] == "OK" + assert instance.results.response[0]["sequence_number"] == 1 + + assert instance.results.result[0]["changed"] is True + assert instance.results.result[0]["success"] is True + assert instance.results.result[0]["sequence_number"] == 1 def test_image_policy_delete_00033(image_policy_delete) -> None: @@ -241,16 +346,16 @@ def test_image_policy_delete_00033(image_policy_delete) -> None: ### Classes and Methods - ImagePolicyDelete - commit() - - fail_json ### Summary - commit() is called without first setting policy_names. + Verify that ``_validate_commit_parameters`` raises ``ValueError`` when + ``commit`` is called and ``policy_names`` is not set. ### Setup - - ImagePolicyDelete().policy_names is not set + - ImagePolicyDelete().policy_names is not set. ### Test - - fail_json is called because policy_names is None + - ``ValueError`` is raised because policy_names is not set. """ with does_not_raise(): instance = image_policy_delete @@ -258,11 +363,11 @@ def test_image_policy_delete_00033(image_policy_delete) -> None: match = r"ImagePolicyDelete\._validate_commit_parameters: " match += r"policy_names must be set prior to calling commit\." - with pytest.raises(AnsibleFailJson, match=match): + with pytest.raises(ValueError, match=match): instance.commit() -def test_image_policy_delete_00034(monkeypatch, image_policy_delete) -> None: +def test_image_policy_delete_00034(image_policy_delete) -> None: """ ### Classes and Methods - ImagePolicyDelete @@ -274,39 +379,41 @@ def test_image_policy_delete_00034(monkeypatch, image_policy_delete) -> None: ### Setup - ImagePolicyDelete().policy_names is set to an empty list - - ImagePolicies.all_policies is mocked to indicate that no policies + - EpPolicies() endpoint response is mocked to indicate that no policies exist on the controller. - - RestSend.dcnm_send is mocked to return a successful (200) response. ### Test - - ImagePolicyDelete().commit returns without doing anything - - fail_json is not called - - instance.results.changed set() contains False - - instance.results.failed set() contains False + - ImagePolicyDelete().commit returns without doing anything. + - Exceptions are not raised. + - ``instance.results`` matches expectations. """ - key = "test_image_policy_delete_00034a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_delete(key) + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_delete(key) - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + gen_responses = ResponseGenerator(responses()) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_delete instance.results = Results() + instance.rest_send = rest_send instance.policy_names = [] - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - with does_not_raise(): instance.commit() assert False in instance.results.changed assert False in instance.results.failed -def test_image_policy_delete_00036(monkeypatch, image_policy_delete) -> None: +def test_image_policy_delete_00036(image_policy_delete) -> None: """ ### Classes and Methods - ImagePolicyDelete @@ -318,24 +425,38 @@ def test_image_policy_delete_00036(monkeypatch, image_policy_delete) -> None: commit() is called with policy_names set to a policy_name that does not exist on the controller. ### Setup - - ImagePolicies().all_policies is mocked to indicate that no policies exist on the controller. + - EpPolicies() endpoint response is mocked to indicate that no policies exist on the controller. - ImagePolicyDelete().policy_names is set a policy_name that is not on the controller. ### Test - ImagePolicyDelete()._get_policies_to_delete return an empty list - ImagePolicyDelete().commit returns without doing anything - - instance.results.changed set() contains False - - instance.results.failed set() contains False - - fail_json is not called + - instance.results match expectations. + - Exceptions are not raised. """ - key = "test_image_policy_delete_00036a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_delete(key) + + gen_responses = ResponseGenerator(responses()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + with does_not_raise(): instance = image_policy_delete instance.results = Results() + instance.rest_send = rest_send instance.policy_names = ["FOO"] - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - with does_not_raise(): instance.commit() + assert len(instance._policies_to_delete) == 0 assert False in instance.results.changed assert False in instance.results.failed @@ -354,44 +475,60 @@ def test_image_policy_delete_00037(monkeypatch, image_policy_delete) -> None: the controller, and the controller returns a success (200) response. ### Setup - - ImagePolicies().all_policies is mocked to indicate policy (KR5M) exists - on the controller. + - EpPolicies() endpoint response is mocked to indicate image policy + (KR5M) exists on the controller. - ImagePolicyDelete().policy_names is set to contain policy_name KR5M. - - dcnm_send is mocked to return a successful (200) response. + - EpPolicyDelete() endpoint response is mocked to return a successful + (200) response. ### Test - - fail_json is not called + - Exceptions are not raised. - commit calls _get_policies_to_delete which returns a list containing policy_name (KR5M) - - commit calls the mocked dcnm_send, which populates instance.response_current - with a successful (200) response - - instance.result_current is populated by instance._handle_response() - - instance.result_current contains expected values - - instance.changed is set to True - - instance.diff contains expected values + - instance.results match expectations. """ - key = "test_image_policy_delete_00037a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_delete(key) - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + gen_responses = ResponseGenerator(responses()) - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_delete(key) + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender with does_not_raise(): instance = image_policy_delete instance.results = Results() + instance.rest_send = rest_send instance.policy_names = ["KR5M"] - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) - - with does_not_raise(): instance.commit() + assert instance._policies_to_delete == ["KR5M"] assert instance.results.result_current == results_image_policy_delete(key) assert True in instance.results.changed assert False in instance.results.failed - assert instance.results.diff == [{"policyNames": ["KR5M"], "sequence_number": 1}] + + assert instance.results.diff[0]["policyNames"] == ["KR5M"] + assert instance.results.diff[0]["sequence_number"] == 1 + + assert instance.results.metadata[0]["action"] == "delete" + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[0]["sequence_number"] == 1 + assert instance.results.metadata[0]["state"] == "deleted" + + assert instance.results.response[0]["RETURN_CODE"] == 200 + assert instance.results.response[0]["MESSAGE"] == "OK" + assert instance.results.response[0]["sequence_number"] == 1 + + assert instance.results.result[0]["changed"] is True + assert instance.results.result[0]["success"] is True + assert instance.results.result[0]["sequence_number"] == 1 def test_image_policy_delete_00038(monkeypatch, image_policy_delete) -> None: @@ -407,47 +544,59 @@ def test_image_policy_delete_00038(monkeypatch, image_policy_delete) -> None: the controller, and the controller returns a failure (500) response. ### Setup - - ImagePolicies().all_policies is mocked to indicate policy (KR5M) exists on + - EpPolicies() endpoint response is mocked to indicate policy (KR5M) exists on the controller. - ImagePolicyDelete().policy_names is set to contain one payload (KR5M). - - dcnm_send is mocked to return a failure (500) response. + - EpPolicyDelete() endpoint response is mocked to return a failure (500) + response. ### Test - - fail_json is called + - Exceptions are not raised. - commit calls _get_policies_to_delete which returns a list containing policy_name (KR5M) - - commit calls the mocked dcnm_send, which populates - instance.response_current with a failure (500) response - - instance.result_current is populated by instance._handle_response() - - instance.result_current contains expected values - - instance.changed is set to False - - instance.diff is an empty list + - instance.results match expectations. """ - key = "test_image_policy_delete_00038a" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" - def mock_dcnm_send(*args, **kwargs): - return responses_image_policy_delete(key) + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_delete(key) - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + gen_responses = ResponseGenerator(responses()) - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + rest_send.timeout = 1 + rest_send.unit_test = True with does_not_raise(): instance = image_policy_delete - instance.rest_send.unit_test = True instance.results = Results() + instance.rest_send = rest_send instance.policy_names = ["KR5M"] - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - - # match = r"ImagePolicyDelete.commit: Bad response during policies delete\. " - # match += r"policy_names \['KR5M'\]\." - with does_not_raise(): instance.commit() assert instance._policies_to_delete == ["KR5M"] assert instance.results.result_current == results_image_policy_delete(key) assert True in instance.results.failed assert False in instance.results.changed - # assert instance.diff == [] + + assert instance.results.diff[0]["sequence_number"] == 1 + + assert instance.results.metadata[0]["action"] == "delete" + assert instance.results.metadata[0]["check_mode"] is False + assert instance.results.metadata[0]["sequence_number"] == 1 + assert instance.results.metadata[0]["state"] == "deleted" + + assert instance.results.response[0]["RETURN_CODE"] == 500 + assert instance.results.response[0]["MESSAGE"] == "NOK" + assert instance.results.response[0]["sequence_number"] == 1 + + assert instance.results.result[0]["changed"] is False + assert instance.results.result[0]["success"] is False + assert instance.results.result[0]["sequence_number"] == 1 diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py index a17d209fe..d722119b9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py @@ -18,9 +18,11 @@ # pylint: disable=unused-import # Some fixtures need to use *args to match the signature of the function they are mocking # pylint: disable=unused-argument +# pylint: disable=protected-access from __future__ import absolute_import, division, print_function +import inspect import json import pytest @@ -42,92 +44,84 @@ payload2config_fixture) -def test_image_policy_payload_00110(config2payload: Config2Payload) -> None: +def test_image_policy_payload_00100() -> None: """ - Class + ### Classes and Methods - Payload - Config2Payload Function - __init__ - Summary + ### Summary Verify Config2Payload is initialized properly - Test + ### Test - Class attributes initialized to expected values - fail_json is not called """ with does_not_raise(): - instance = config2payload + instance = Config2Payload() assert instance.class_name == "Config2Payload" - assert isinstance(instance.properties, dict) - assert instance.properties.get("config") == {} - assert instance.properties.get("payload") == {} + assert instance._config == {} + assert instance._params == {} + assert instance._payload == {} -def test_image_policy_payload_00120(config2payload: Config2Payload) -> None: +def test_image_policy_payload_00120(config2payload) -> None: """ - Class + ### Classes and Methods - Payload - Config2Payload Function - commit - Summary + ### Summary Verify Config2Payload coverts a configuration to a proper payload. - Test - - fail_json is not called - - commit converts config to a proper payload + ### Test + - Exceptions are not raised. + - commit converts config to a proper payload. """ - key = "test_image_policy_payload_00120a" - data = load_fixture("data_payload") + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + data = load_fixture("data_payload") config = data.get(key, {}).get("config") payload = data.get(key, {}).get("payload") - print(f"config: {json.dumps(config, indent=4, sort_keys=True)}") - print(f"payload: {json.dumps(payload, indent=4, sort_keys=True)}") - with does_not_raise(): instance = config2payload instance.config = config - instance.log.debug( - f"00120: config: {json.dumps(config, indent=4, sort_keys=True)}" - ) - instance.log.debug( - f"00120: payload: {json.dumps(payload, indent=4, sort_keys=True)}" - ) instance.commit() assert payload is not None assert instance.payload == payload -def test_image_policy_payload_00121(config2payload: Config2Payload) -> None: +def test_image_policy_payload_00121(config2payload) -> None: """ - Class + ### Classes and Methods - Payload - Config2Payload Function - commit - Summary + ### Summary Verify Config2Payload coverts a configuration to a proper payload when the packages.install and packages.uninstall keys are empty lists. - Test + ### Test - config packages.install is an empty list - config packages.ininstall is an empty list - commit converts config to a proper payload """ - key = "test_image_policy_payload_00121a" - data = load_fixture("data_payload") + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + data = load_fixture("data_payload") config = data.get(key, {}).get("config") payload = data.get(key, {}).get("payload") + with does_not_raise(): - ansible_module = MockAnsibleModule() - ansible_module.state = "merged" instance = config2payload instance.config = config instance.commit() @@ -135,103 +129,105 @@ def test_image_policy_payload_00121(config2payload: Config2Payload) -> None: assert instance.payload == payload -def test_image_policy_payload_00122(config2payload: Config2Payload) -> None: +def test_image_policy_payload_00122(config2payload) -> None: """ - Class + ### Classes and Methods - Payload - Config2Payload Function - commit - Summary + ### Summary Verify Config2Payload.commit() calls fail_json when config is an empty dict - Test + ### Test - config is set to an empty dict - commit calls fail_json """ - key = "test_image_policy_payload_00122a" - data = load_fixture("data_payload") + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + data = load_fixture("data_payload") config = data.get(key, {}).get("config") with does_not_raise(): - ansible_module = MockAnsibleModule() - ansible_module.state = "merged" instance = config2payload - instance.results = Results() instance.config = config - match = "Config2Payload.commit: config is empty" - with pytest.raises(AnsibleFailJson, match=match): + match = r"Config2Payload\.commit: config is empty" + with pytest.raises(ValueError, match=match): instance.commit() @pytest.mark.parametrize("state", ["deleted", "query"]) -def test_image_policy_payload_00123(config2payload: Config2Payload, state) -> None: +def test_image_policy_payload_00123(config2payload, state) -> None: """ - Class + ### Classes and Methods - Payload - Config2Payload Function - commit - Summary + ### Summary Verify Config2Payload.commit() behavior for Ansible states "query" and "deleted". - Test + ### Test - payload contains only the policyName key - The value of the policyName key == value of the name key in instance.config - fail_json is not called """ - key = "test_image_policy_payload_00123a" - data = load_fixture("data_payload") + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + data = load_fixture("data_payload") config = data.get(key, {}).get("config") + with does_not_raise(): - ansible_module = MockAnsibleModule() - ansible_module.state = state instance = config2payload instance.config = config instance.commit() - assert instance.payload == {"policyName": config["name"]} + assert instance.payload["agnostic"] == config["agnostic"] + assert instance.payload["policyDescr"] == config["description"] + assert instance.payload["policyName"] == config["name"] + assert instance.payload["epldImgName"] == config["epld_image"] + assert instance.payload["nxosVersion"] == config["release"] + assert instance.payload["platform"] == config["platform"] + assert instance.payload["policyType"] == "PLATFORM" -MATCH_00130 = ( - r"Config2Payload.payload: payload must be a dictionary\. got .* for value .*" -) +MATCH_00130 = r"Config2Payload\.payload:\s+" +MATCH_00130 += r"payload must be a dictionary\.\s+" +MATCH_00130 += r"got .* for value .*" @pytest.mark.parametrize( "value, expected", [ ({}, does_not_raise()), - ([], pytest.raises(AnsibleFailJson, match=MATCH_00130)), - ((), pytest.raises(AnsibleFailJson, match=MATCH_00130)), - (None, pytest.raises(AnsibleFailJson, match=MATCH_00130)), - (1, pytest.raises(AnsibleFailJson, match=MATCH_00130)), - (1.1, pytest.raises(AnsibleFailJson, match=MATCH_00130)), - ("foo", pytest.raises(AnsibleFailJson, match=MATCH_00130)), - (True, pytest.raises(AnsibleFailJson, match=MATCH_00130)), - (False, pytest.raises(AnsibleFailJson, match=MATCH_00130)), + ([], pytest.raises(TypeError, match=MATCH_00130)), + ((), pytest.raises(TypeError, match=MATCH_00130)), + (None, pytest.raises(TypeError, match=MATCH_00130)), + (1, pytest.raises(TypeError, match=MATCH_00130)), + (1.1, pytest.raises(TypeError, match=MATCH_00130)), + ("foo", pytest.raises(TypeError, match=MATCH_00130)), + (True, pytest.raises(TypeError, match=MATCH_00130)), + (False, pytest.raises(TypeError, match=MATCH_00130)), ], ) -def test_image_policy_payload_00130( - config2payload: Config2Payload, value, expected -) -> None: +def test_image_policy_payload_00130(config2payload, value, expected) -> None: """ - Class + ### Classes and Methods - Payload - Config2Payload Function - - payload setter + - payload.setter - Summary - Verify payload setter error handling + ### Summary + Verify payload setter error handling. - Test - - payload accepts a dictionary - - payload calls fail_json for non-dictionary values + ### Test + - payload accepts a dictionary. + - payload raises ``ValueError`` for non-dictionary values. """ with does_not_raise(): instance = config2payload @@ -239,41 +235,41 @@ def test_image_policy_payload_00130( instance.payload = value -MATCH_00140 = ( - r"Config2Payload.config: config must be a dictionary\. got .* for value .*" -) +MATCH_00140 = r"Config2Payload\.config:\s+" +MATCH_00140 += r"config must be a dictionary\.\s+" +MATCH_00140 += r"got .* for value .*" @pytest.mark.parametrize( "value, expected", [ ({}, does_not_raise()), - ([], pytest.raises(AnsibleFailJson, match=MATCH_00140)), - ((), pytest.raises(AnsibleFailJson, match=MATCH_00140)), - (None, pytest.raises(AnsibleFailJson, match=MATCH_00140)), - (1, pytest.raises(AnsibleFailJson, match=MATCH_00140)), - (1.1, pytest.raises(AnsibleFailJson, match=MATCH_00140)), - ("foo", pytest.raises(AnsibleFailJson, match=MATCH_00140)), - (True, pytest.raises(AnsibleFailJson, match=MATCH_00140)), - (False, pytest.raises(AnsibleFailJson, match=MATCH_00140)), + ([], pytest.raises(TypeError, match=MATCH_00140)), + ((), pytest.raises(TypeError, match=MATCH_00140)), + (None, pytest.raises(TypeError, match=MATCH_00140)), + (1, pytest.raises(TypeError, match=MATCH_00140)), + (1.1, pytest.raises(TypeError, match=MATCH_00140)), + ("foo", pytest.raises(TypeError, match=MATCH_00140)), + (True, pytest.raises(TypeError, match=MATCH_00140)), + (False, pytest.raises(TypeError, match=MATCH_00140)), ], ) def test_image_policy_payload_00140( config2payload: Config2Payload, value, expected ) -> None: """ - Class + ### Classes and Methods - Payload - Payload2Config Function - config setter - Summary + ### Summary Verify config setter error handling - Test - - config accepts a dictionary - - config calls fail_json for non-dictionary values + ### Test + - config accepts a dictionary. + - config raises ``ValueError`` for non-dictionary values. """ with does_not_raise(): instance = config2payload @@ -281,47 +277,48 @@ def test_image_policy_payload_00140( instance.config = value -def test_image_policy_payload_00210(payload2config: Payload2Config) -> None: +def test_image_policy_payload_00200() -> None: """ - Class + ### Classes and Methods - Payload - Payload2Config Function - __init__ - Summary + ### Summary Verify Payload2Config is initialized properly - Test - - fail_json is not called + ### Test + - Exceptions are not raised. - Class attributes initialized to expected values """ with does_not_raise(): - instance = payload2config + instance = Payload2Config() assert instance.class_name == "Payload2Config" - assert isinstance(instance.properties, dict) - assert instance.properties.get("config") == {} - assert instance.properties.get("payload") == {} + assert instance._config == {} + assert instance._params == {} + assert instance._payload == {} -def test_image_policy_payload_00220(payload2config: Payload2Config) -> None: +def test_image_policy_payload_00220(payload2config) -> None: """ - Class + ### Classes and Methods - Payload - Payload2Config Function - commit - Summary + ### Summary Verify Payload2Config coverts a payload to a proper configuration. - Test - - fail_json is not called - - commit converts the payload to a proper config + ### Test + - Exceptions are not raised. + - commit converts the payload to a proper config. """ - key = "test_image_policy_payload_00220a" - data = load_fixture("data_payload") + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + data = load_fixture("data_payload") config = data.get(key, {}).get("config") payload = data.get(key, {}).get("payload") with does_not_raise(): @@ -332,28 +329,30 @@ def test_image_policy_payload_00220(payload2config: Payload2Config) -> None: assert instance.config == config -def test_image_policy_payload_00221(payload2config: Payload2Config) -> None: +def test_image_policy_payload_00221(payload2config) -> None: """ - Class + ### Classes and Methods - Payload - Payload2Config Function - commit - Summary + ### Summary Verify Payload2Config coverts a payload to a proper configuration when the payload is missing the rpmimages and packageName keys. - Test - - payload is missing rpmimages and packageName keys - - commit converts the payload to a proper config - - missing mandatory key "type" is added to the config + ### Test + - ``payload`` is missing rpmimages and packageName keys. + - ``commit`` converts ``payload`` to ``config`` properly. + - missing mandatory key ``type`` is added to ``config``. """ - key = "test_image_policy_payload_00221a" - data = load_fixture("data_payload") + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + data = load_fixture("data_payload") config = data.get(key, {}).get("config") payload = data.get(key, {}).get("payload") + with does_not_raise(): instance = payload2config instance.payload = payload @@ -362,70 +361,69 @@ def test_image_policy_payload_00221(payload2config: Payload2Config) -> None: assert instance.config == config -def test_image_policy_payload_00222(payload2config: Payload2Config) -> None: +def test_image_policy_payload_00222(payload2config) -> None: """ - Class + ### Classes and Methods - Payload - Payload2Config Function - commit - Summary - Verify Payload2Config.commit() calls fail_json when payload is an empty dict + ### Summary + Verify Payload2Config.commit() raises ``ValueError`` when ``payload`` + is an empty dict - Test - - config is set to an empty dict - - commit calls fail_json + ### Test + - ``commit`` raises ``ValueError`` + - ``config`` is set to an empty dict """ - key = "test_image_policy_payload_00222a" - data = load_fixture("data_payload") + method_name = inspect.stack()[0][3] + key = f"{method_name}a" + data = load_fixture("data_payload") payload = data.get(key, {}).get("payload") + with does_not_raise(): - ansible_module = MockAnsibleModule() - ansible_module.state = "merged" instance = payload2config instance.payload = payload - match = "Payload2Config.commit: payload is empty" - with pytest.raises(AnsibleFailJson, match=match): + match = r"Payload2Config\.commit: payload is empty" + with pytest.raises(ValueError, match=match): instance.commit() + assert instance.config == {} -MATCH_00230 = ( - r"Payload2Config.payload: payload must be a dictionary\. got .* for value .*" -) +MATCH_00230 = r"Payload2Config\.payload:\s+" +MATCH_00230 += r"payload must be a dictionary\. got .* for value .*" @pytest.mark.parametrize( "value, expected", [ ({}, does_not_raise()), - ([], pytest.raises(AnsibleFailJson, match=MATCH_00230)), - ((), pytest.raises(AnsibleFailJson, match=MATCH_00230)), - (None, pytest.raises(AnsibleFailJson, match=MATCH_00230)), - (1, pytest.raises(AnsibleFailJson, match=MATCH_00230)), - (1.1, pytest.raises(AnsibleFailJson, match=MATCH_00230)), - ("foo", pytest.raises(AnsibleFailJson, match=MATCH_00230)), - (True, pytest.raises(AnsibleFailJson, match=MATCH_00230)), - (False, pytest.raises(AnsibleFailJson, match=MATCH_00230)), + ([], pytest.raises(TypeError, match=MATCH_00230)), + ((), pytest.raises(TypeError, match=MATCH_00230)), + (None, pytest.raises(TypeError, match=MATCH_00230)), + (1, pytest.raises(TypeError, match=MATCH_00230)), + (1.1, pytest.raises(TypeError, match=MATCH_00230)), + ("foo", pytest.raises(TypeError, match=MATCH_00230)), + (True, pytest.raises(TypeError, match=MATCH_00230)), + (False, pytest.raises(TypeError, match=MATCH_00230)), ], ) -def test_image_policy_payload_00230( - payload2config: Payload2Config, value, expected -) -> None: +def test_image_policy_payload_00230(payload2config, value, expected) -> None: """ - Class + ### Classes and Methods - Payload - Payload2Config Function - payload setter - Summary - Verify payload setter error handling + ### Summary + Verify payload setter error handling. - Test - - payload accepts a dictionary - - payload calls fail_json for non-dictionary values + ### Test + - ``payload`` accepts a dictionary. + - ``payload`` raises ``TypeError`` for non-dictionary values. """ with does_not_raise(): instance = payload2config @@ -433,39 +431,36 @@ def test_image_policy_payload_00230( instance.payload = value -MATCH_00240 = ( - r"Payload2Config.config: config must be a dictionary\. got .* for value .*" -) +MATCH_00240 = r"Payload2Config\.config:\s+" +MATCH_00240 += r"config must be a dictionary\. got .* for value .*" @pytest.mark.parametrize( "value, expected", [ ({}, does_not_raise()), - ([], pytest.raises(AnsibleFailJson, match=MATCH_00240)), - ((), pytest.raises(AnsibleFailJson, match=MATCH_00240)), - (None, pytest.raises(AnsibleFailJson, match=MATCH_00240)), - (1, pytest.raises(AnsibleFailJson, match=MATCH_00240)), - (1.1, pytest.raises(AnsibleFailJson, match=MATCH_00240)), - ("foo", pytest.raises(AnsibleFailJson, match=MATCH_00240)), - (True, pytest.raises(AnsibleFailJson, match=MATCH_00240)), - (False, pytest.raises(AnsibleFailJson, match=MATCH_00240)), + ([], pytest.raises(TypeError, match=MATCH_00240)), + ((), pytest.raises(TypeError, match=MATCH_00240)), + (None, pytest.raises(TypeError, match=MATCH_00240)), + (1, pytest.raises(TypeError, match=MATCH_00240)), + (1.1, pytest.raises(TypeError, match=MATCH_00240)), + ("foo", pytest.raises(TypeError, match=MATCH_00240)), + (True, pytest.raises(TypeError, match=MATCH_00240)), + (False, pytest.raises(TypeError, match=MATCH_00240)), ], ) -def test_image_policy_payload_00240( - payload2config: Payload2Config, value, expected -) -> None: +def test_image_policy_payload_00240(payload2config, value, expected) -> None: """ - Class + ### Classes and Methods - Payload - Payload2Config Function - config setter - Summary + ### Summary Verify config setter error handling - Test + ### Test - config accepts a dictionary - config calls fail_json for non-dictionary values """ diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py index 52b168cf9..4801ece05 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py @@ -44,9 +44,8 @@ from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - MockAnsibleModule, MockImagePolicies, does_not_raise, - image_policy_replace_bulk_fixture, params, - payloads_image_policy_replace_bulk, responses_ep_policies, + MockAnsibleModule, does_not_raise, image_policy_replace_bulk_fixture, + params, payloads_image_policy_replace_bulk, responses_ep_policies, responses_ep_policy_edit, responses_image_policy_replace_bulk, rest_send_result_current, results_image_policy_replace_bulk) diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py index 90f6d68c8..e68ba9729 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py @@ -40,6 +40,9 @@ def get_state(action): + """ + Return the state based on the action. + """ if action in ["create", "update"]: state = "merged" elif action == "delete": @@ -319,23 +322,21 @@ def image_policy_update_bulk_fixture(): @pytest.fixture(name="config2payload") def config2payload_fixture(): """ - mock Config2Payload - Used in test_image_policy_payload.py + Return Config2Payload with params set. """ - instance = MockAnsibleModule() - instance.state = "merged" - return Config2Payload(instance) + instance = Config2Payload() + instance.params = params + return instance @pytest.fixture(name="payload2config") def payload2config_fixture(): """ - mock Payload2Config - Used in test_image_policy_payload.py + Return Payload2Config with params set. """ - instance = MockAnsibleModule() - instance.state = "merged" - return Payload2Config(instance) + instance = Payload2Config() + instance.params = params + return instance @contextmanager @@ -426,6 +427,16 @@ def responses_ep_policy_create(key: str) -> Dict[str, str]: return data +def responses_ep_policy_delete(key: str) -> Dict[str, str]: + """ + Return responses for EpPolicyDelete() endpoint + """ + data_file = "responses_EpPolicyDelete" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + def responses_ep_policy_edit(key: str) -> Dict[str, str]: """ Return responses for EpPolicyEdit() endpoint From c4be7759a3e816276a0b89d18fd3cd37e43f2849 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 1 Jul 2024 10:19:54 -1000 Subject: [PATCH 217/230] test_image_policy_replace_bulk.py: minor reformatting --- .../test_image_policy_replace_bulk.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py index 4801ece05..23341b38a 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_replace_bulk.py @@ -228,12 +228,10 @@ def test_image_policy_replace_bulk_00030(image_policy_replace_bulk) -> None: def responses(): yield responses_ep_policies(key) yield responses_ep_policy_edit(key) - gen_responses = ResponseGenerator(responses()) def payloads(): yield payloads_image_policy_replace_bulk(key) - gen_payloads = ResponseGenerator(payloads()) sender = Sender() @@ -289,12 +287,10 @@ def test_image_policy_replace_bulk_00031(image_policy_replace_bulk) -> None: def responses(): yield responses_ep_policies(key) - gen_responses = ResponseGenerator(responses()) def payloads(): yield payloads_image_policy_replace_bulk(key) - gen_payloads = ResponseGenerator(payloads()) sender = Sender() @@ -351,12 +347,10 @@ def test_image_policy_replace_bulk_00032(image_policy_replace_bulk) -> None: def responses(): yield responses_ep_policies(key) yield responses_ep_policy_edit(key) - gen_responses = ResponseGenerator(responses()) def payloads(): yield payloads_image_policy_replace_bulk(key) - gen_payloads = ResponseGenerator(payloads()) sender = Sender() @@ -429,12 +423,10 @@ def test_image_policy_replace_bulk_00034(image_policy_replace_bulk) -> None: def responses(): yield responses_ep_policies(key) - gen_responses = ResponseGenerator(responses()) def payloads(): yield payloads_image_policy_replace_bulk(key) - gen_payloads = ResponseGenerator(payloads()) sender = Sender() @@ -489,12 +481,10 @@ def responses(): yield responses_ep_policies(key) yield responses_ep_policy_edit(key) yield responses_ep_policy_edit(key) - gen_responses = ResponseGenerator(responses()) def payloads(): yield payloads_image_policy_replace_bulk(key) - gen_payloads = ResponseGenerator(payloads()) sender = Sender() @@ -584,12 +574,10 @@ def test_image_policy_replace_bulk_00036(image_policy_replace_bulk) -> None: def responses(): yield responses_ep_policies(key) yield responses_ep_policy_edit(key) - gen_responses = ResponseGenerator(responses()) def payloads(): yield payloads_image_policy_replace_bulk(key) - gen_payloads = ResponseGenerator(payloads()) sender = Sender() @@ -661,12 +649,10 @@ def responses(): yield responses_ep_policies(key_policies) yield responses_ep_policy_edit(key_ok) yield responses_ep_policy_edit(key_nok) - gen_responses = ResponseGenerator(responses()) def payloads(): yield payloads_image_policy_replace_bulk(key_payloads) - gen_payloads = ResponseGenerator(payloads()) sender = Sender() From 412e5c69255fd9b99aa4abf126a7f8727071f2d5 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 1 Jul 2024 10:25:43 -1000 Subject: [PATCH 218/230] ImagePolicyQuery: Align with v2 classes. 1. Update unit tests to use v2 classes. 2. query.py: ImagePolicyQuery().__init__(): initialize self._policy_names. --- plugins/module_utils/image_policy/query.py | 6 +- .../test_image_policy_query.py | 316 ++++++++++-------- 2 files changed, 188 insertions(+), 134 deletions(-) diff --git a/plugins/module_utils/image_policy/query.py b/plugins/module_utils/image_policy/query.py index eef2080f8..fa2be85fa 100644 --- a/plugins/module_utils/image_policy/query.py +++ b/plugins/module_utils/image_policy/query.py @@ -69,11 +69,11 @@ class ImagePolicyQuery: def __init__(self): self.class_name = self.__class__.__name__ - self._policies_to_query = [] - self.action = "query" - + self._policies_to_query = [] + self._policy_names = None self._results = None + self.log = logging.getLogger(f"dcnm.{self.class_name}") msg = "ENTERED ImagePolicyQuery(): " msg += f"action {self.action}, " diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_query.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_query.py index e5a51d5b8..a2635a545 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_query.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_query.py @@ -29,20 +29,27 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import inspect + import pytest -from ansible_collections.ansible.netcommon.tests.unit.modules.utils import \ - AnsibleFailJson +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import \ + ResponseHandler +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import \ + RestSend from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ Results +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_file import \ + Sender from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ ImagePolicies +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - GenerateResponses, MockImagePolicies, does_not_raise, - image_policies_all_policies, image_policy_query_fixture, - rest_send_response_current) + MockAnsibleModule, does_not_raise, image_policies_all_policies, + image_policy_query_fixture, params, rest_send_response_current) -def test_image_policy_query_00010(image_policy_query) -> None: +def test_image_policy_query_00000(image_policy_query) -> None: """ ### Classes and Methods - ImagePolicyQuery @@ -50,141 +57,170 @@ def test_image_policy_query_00010(image_policy_query) -> None: ### Test - Class attributes are initialized to expected values - - fail_json is not called + - Exceptions are not raised. """ with does_not_raise(): instance = image_policy_query + assert instance.class_name == "ImagePolicyQuery" assert instance.action == "query" - assert instance.state == "query" - assert isinstance(instance._image_policies, ImagePolicies) - assert instance.policy_names is None + assert instance._results is None assert instance._policies_to_query == [] + assert instance._policy_names is None -def test_image_policy_query_00020(image_policy_query) -> None: +def test_image_policy_query_00010(image_policy_query) -> None: """ ### Classes and Methods - ImagePolicyQuery - __init__() - - policy_names setter + - policy_names.setter ### Test - - policy_names is set to expected value - - fail_json is not called + - ``policy_names`` is set to expected value. + - Exceptions are not raised. """ policy_names = ["FOO", "BAR"] with does_not_raise(): instance = image_policy_query instance.policy_names = policy_names + assert instance.policy_names == policy_names -def test_image_policy_query_00021(image_policy_query) -> None: +def test_image_policy_query_00011(image_policy_query) -> None: """ ### Classes and Methods - ImagePolicyQuery - __init__() - - policy_names setter + - policy_names.setter + + ### Summary + Verify that ``policy_names.setter`` raises ``TypeError`` when + ``policy_names`` is not a list. ### Test - - fail_json is called because policy_names is not a list - - instance.policy_names is not modified, hence it retains its initial value of None + - ``TypeError`` is raised because policy_names is not a list. + - ``instance.policy_names`` is not modified. """ - match = "ImagePolicyQuery.policy_names: " - match += "policy_names must be a list." - with does_not_raise(): instance = image_policy_query - with pytest.raises(AnsibleFailJson, match=match): + + match = r"ImagePolicyQuery.policy_names:\s+" + match += r"policy_names must be a list\.\s+" + match += r"got str for value NOT_A_LIST" + with pytest.raises(TypeError, match=match): instance.policy_names = "NOT_A_LIST" assert instance.policy_names is None -def test_image_policy_query_00022(image_policy_query) -> None: +def test_image_policy_query_00012(image_policy_query) -> None: """ ### Classes and Methods - ImagePolicyQuery - __init__() - - policy_names setter + - policy_names.setter + + ### Summary + Verify that ``policy_names.setter`` raises ``ValueError`` when + ``policy_names`` is set to a list containing non-string elements. ### Test - - fail_json is called because policy_names is a list with a non-string element - - instance.policy_names is not modified, hence it retains its initial value of None + - ``policy_names.setter`` raises ``TypeError``. + - Error message matches expected value. + - ``instance.policy_names`` is not modified. """ - match = "ImagePolicyQuery.policy_names: " - match += "policy_names must be a list of strings." - with does_not_raise(): instance = image_policy_query - with pytest.raises(AnsibleFailJson, match=match): - instance.policy_names = [1, 2, 3] + + match = r"ImagePolicyQuery\.policy_names:\s+" + match += r"policy_names must be a list of strings\.\s+" + match += r"got int for value 3" + with pytest.raises(TypeError, match=match): + instance.policy_names = ["1", "2", 3] + assert instance.policy_names is None -def test_image_policy_query_00023(image_policy_query) -> None: +def test_image_policy_query_00013(image_policy_query) -> None: """ ### Classes and Methods - ImagePolicyQuery - - __init__() - - policy_names setter + - policy_names.setter ### Summary - Verify behavior when policy_names is not set prior to calling commit + Verify that ``policy_names.setter`` raises ``ValueError`` when + ``policy_names`` is set to an empty list. + + ### Setup + - ``policy_names`` is set to an empty list. ### Test - - fail_json is called because policy_names is not set prior to calling commit - - instance.policy_names is not modified, hence it retains its initial value of None + - ``policy_names.setter`` raises ``ValueError``. + - Error message matches expected value. """ - match = "ImagePolicyQuery.commit: " - match += "policy_names must be set prior to calling commit." - - with does_not_raise(): + match = r"ImagePolicyQuery\.policy_names:\s+" + match += r"policy_names must be a list of at least one string\." + with pytest.raises(ValueError, match=match): instance = image_policy_query - instance.results = Results() - with pytest.raises(AnsibleFailJson, match=match): - instance.commit() - assert instance.policy_names is None + instance.policy_names = [] -def test_image_policy_query_00024(image_policy_query) -> None: +def test_image_policy_query_00020(image_policy_query) -> None: """ ### Classes and Methods - ImagePolicyQuery - - policy_names setter + - __init__() + - commit() ### Summary - Verify behavior when policy_names is set to an empty list - - ### Setup - - ImagePolicyQuery().policy_names is set to an empty list + Verify ``commit`` raises ``ValueError`` when ``policy_names`` is not. ### Test - - fail_json is called from policy_names setter + - ``commit`` raises ``ValueError``. + - Error message matches expected value. + - ``instance.policy_names`` is not modified. """ - match = "ImagePolicyQuery.policy_names: policy_names must be a list of " - match += "at least one string." - with pytest.raises(AnsibleFailJson, match=match): + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + image_policies = ImagePolicies() + image_policies.rest_send = rest_send + image_policies.results = Results() + + with does_not_raise(): instance = image_policy_query - instance.policy_names = [] + instance._image_policies = image_policies + + match = r"ImagePolicyQuery\.commit:\s+" + match += r"policy_names must be set prior to calling commit\." + with pytest.raises(ValueError, match=match): + instance.commit() + + assert instance.policy_names is None -def test_image_policy_query_00030(monkeypatch, image_policy_query) -> None: +def test_image_policy_query_00030(image_policy_query) -> None: """ ### Classes and Methods - ImagePolicyQuery - __init__() - _verify_image_policy_ref_count() - - policy_names setter + - policy_names.setter - _get_policies_to_query() - commit() ### Summary - Verify behavior when user queries a policy that does not exist on the controller + Verify behavior when user queries a policy that does not exist on the + controller. ### Setup - - ImagePolicies().all_policies, is mocked to indicate that one image policy - (KR5M) exist on the controller. + - ImagePolicies().all_policies, is mocked to indicate that one image + policy (KR5M) exists on the controller. - ImagePolicyQuery.policy_names is set to contain one policy_name (FOO) that does not exist on the controller. @@ -194,30 +230,34 @@ def test_image_policy_query_00030(monkeypatch, image_policy_query) -> None: - instance.results.changed set() contains False - instance.results.failed set() contains False - commit() returns without doing anything else - - fail_json is not called + - Exceptions are not raised. """ - key = "test_image_policy_query_00030a" - - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" def responses(): yield rest_send_response_current(key) - gen = GenerateResponses(responses()) + gen_responses = ResponseGenerator(responses()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender - def mock_dcnm_send(*args, **kwargs): - item = gen.next - return item + image_policies = ImagePolicies() + image_policies.rest_send = rest_send + image_policies.results = Results() with does_not_raise(): instance = image_policy_query - instance.results = Results() + instance._image_policies = image_policies instance.policy_names = ["FOO"] - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) - instance._image_policies.results = Results() - with does_not_raise(): + instance.results = Results() instance.commit() + assert isinstance(instance.results.diff, list) assert isinstance(instance.results.result, list) assert isinstance(instance.results.response, list) @@ -232,21 +272,21 @@ def mock_dcnm_send(*args, **kwargs): assert True not in instance.results.changed -def test_image_policy_query_00031(monkeypatch, image_policy_query) -> None: +def test_image_policy_query_00031(image_policy_query) -> None: """ ### Classes and Methods - ImagePolicyQuery - __init__() - - policy_names setter + - policy_names.setter - _get_policies_to_query() - commit() ### Summary - Verify behavior when user queries a policy that exists on the controller + Verify behavior when user queries a policy that exists on the controller. ### Setup - - ImagePolicies().all_policies is mocked to indicate that one image policy - (KR5M) exists on the controller. + - ImagePolicies().all_policies is mocked to indicate that one image + policy (KR5M) exists on the controller. - ImagePolicyQuery.policy_names is set to contain one policy_name (KR5M) that exists on the controller. @@ -257,29 +297,34 @@ def test_image_policy_query_00031(monkeypatch, image_policy_query) -> None: - instance.response_current is a dict with key RETURN_CODE == 200 - instance.result is a list with one element - instance.result_current is a dict with key success == True + - Exceptions are not raised. """ - key = "test_image_policy_query_00031a" - - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" def responses(): yield rest_send_response_current(key) - gen = GenerateResponses(responses()) + gen_responses = ResponseGenerator(responses()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender - def mock_dcnm_send(*args, **kwargs): - item = gen.next - return item + image_policies = ImagePolicies() + image_policies.rest_send = rest_send + image_policies.results = Results() with does_not_raise(): instance = image_policy_query - instance.results = Results() + instance._image_policies = image_policies instance.policy_names = ["KR5M"] - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) - instance._image_policies.results = Results() - with does_not_raise(): + instance.results = Results() instance.commit() + assert isinstance(instance.results.diff, list) assert isinstance(instance.results.result, list) assert isinstance(instance.results.response, list) @@ -294,11 +339,11 @@ def mock_dcnm_send(*args, **kwargs): assert True not in instance.results.changed -def test_image_policy_query_00032(monkeypatch, image_policy_query) -> None: +def test_image_policy_query_00032(image_policy_query) -> None: """ ### Classes and Methods - ImagePolicyQuery - - policy_names setter + - policy_names.setter - _get_policies_to_query() - commit() @@ -307,11 +352,11 @@ def test_image_policy_query_00032(monkeypatch, image_policy_query) -> None: on the controller and some of which do not exist on the controller. ### Setup - - ImagePolicies().all_policies, is mocked to indicate that two image policies - (KR5M, NR3F) exist on the controller. - - ImagePolicyQuery().policy_names is set to contain one image policy name (FOO) - that does not exist on the controller and two image policy names (KR5M, NR3F) - that do exist on the controller. + - ImagePolicies().all_policies, is mocked to indicate that two image + policies (KR5M, NR3F) exist on the controller. + - ImagePolicyQuery().policy_names is set to contain one image policy + name (FOO) that does not exist on the controller and two image policy + names (KR5M, NR3F) that do exist on the controller. ### Test - instance.diff is a list containing two elements @@ -321,29 +366,34 @@ def test_image_policy_query_00032(monkeypatch, image_policy_query) -> None: - instance.response_current is a dict with key RETURN_CODE == 200 - instance.result is a list with one element - instance.result_current is a dict with key success == True + - Exceptions are not raised. """ - key = "test_image_policy_query_00032a" - - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" def responses(): yield rest_send_response_current(key) - gen = GenerateResponses(responses()) + gen_responses = ResponseGenerator(responses()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender - def mock_dcnm_send(*args, **kwargs): - item = gen.next - return item + image_policies = ImagePolicies() + image_policies.rest_send = rest_send + image_policies.results = Results() with does_not_raise(): instance = image_policy_query - instance.results = Results() + instance._image_policies = image_policies instance.policy_names = ["KR5M", "NR3F", "FOO"] - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) - instance._image_policies.results = Results() - with does_not_raise(): + instance.results = Results() instance.commit() + assert isinstance(instance.results.diff, list) assert isinstance(instance.results.result, list) assert isinstance(instance.results.response, list) @@ -360,12 +410,12 @@ def mock_dcnm_send(*args, **kwargs): assert True not in instance.results.changed -def test_image_policy_query_00033(monkeypatch, image_policy_query) -> None: +def test_image_policy_query_00033(image_policy_query) -> None: """ ### Classes and Methods - ImagePolicyQuery - __init__() - - policy_names setter + - policy_names.setter - _get_policies_to_query() - commit() @@ -374,41 +424,45 @@ def test_image_policy_query_00033(monkeypatch, image_policy_query) -> None: queries for an image policy that, of course, does not exist. ### Setup - - ImagePolicies().all_policies, is mocked to indicate that no image policies - exist on the controller. - - ImagePolicyQuery.policy_names is set to contain one policy_name (FOO) - that does not exist on the controller. + - ImagePolicies().all_policies, is mocked to indicate that no image + policies exist on the controller. + - ImagePolicyQuery.policy_names is set to contain one policy_name + (FOO) that does not exist on the controller. ### Test - - commit() calls _get_policies_to_query() which sets instance._policies_to_query - to an empty list. + - commit() calls _get_policies_to_query() which sets + ``instance._policies_to_query`` to an empty list. - commit() sets instance.changed to False - commit() sets instance.failed to False - commit() returns without doing anything else - - fail_json is not called + - Exceptions are not raised. """ - key = "test_image_policy_query_00033a" - - PATCH_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." - PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" + method_name = inspect.stack()[0][3] + key = f"{method_name}a" def responses(): yield rest_send_response_current(key) - gen = GenerateResponses(responses()) + gen_responses = ResponseGenerator(responses()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender - def mock_dcnm_send(*args, **kwargs): - item = gen.next - return item + image_policies = ImagePolicies() + image_policies.rest_send = rest_send + image_policies.results = Results() with does_not_raise(): instance = image_policy_query - instance.results = Results() + instance._image_policies = image_policies instance.policy_names = ["FOO"] - monkeypatch.setattr(PATCH_DCNM_SEND, mock_dcnm_send) - instance._image_policies.results = Results() - with does_not_raise(): + instance.results = Results() instance.commit() + assert isinstance(instance.results.diff, list) assert isinstance(instance.results.result, list) assert isinstance(instance.results.response, list) From c72bea82fe7e321a6f3ad30ec3a5813a61f3c194 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 1 Jul 2024 10:31:23 -1000 Subject: [PATCH 219/230] Config2Payload / Payload2Config: Align with v2 classes. 1. payload.py: use copy to ensure we're working with copies. Probably not needed...just being safe. 2. Update unit tests to use ResponseGenerator() --- plugins/module_utils/image_policy/payload.py | 67 +++++---- .../fixtures/configs_Config2Payload.json | 48 ++++++ .../fixtures/configs_Payload2Config.json | 37 +++++ .../fixtures/data_payload.json | 138 ------------------ .../fixtures/payloads_Config2Payload.json | 36 +++++ .../fixtures/payloads_Payload2Config.json | 27 ++++ .../test_image_policy_payload.py | 121 +++++++++------ .../modules/dcnm/dcnm_image_policy/utils.py | 40 +++++ 8 files changed, 304 insertions(+), 210 deletions(-) create mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/configs_Config2Payload.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/configs_Payload2Config.json delete mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/data_payload.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_Config2Payload.json create mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_Payload2Config.json diff --git a/plugins/module_utils/image_policy/payload.py b/plugins/module_utils/image_policy/payload.py index 4fe70459a..821491d7f 100644 --- a/plugins/module_utils/image_policy/payload.py +++ b/plugins/module_utils/image_policy/payload.py @@ -17,6 +17,7 @@ __metaclass__ = type __author__ = "Allen Robel" +import copy import inspect import json import logging @@ -54,7 +55,7 @@ def config(self, value): msg += f"got {type(value).__name__} for " msg += f"value {value}" raise TypeError(msg) - self._config = value + self._config = copy.deepcopy(value) @property def params(self): @@ -72,7 +73,7 @@ def params(self, value): msg += f"got {type(value).__name__} for " msg += f"value {value}" raise TypeError(msg) - self._params = value + self._params = copy.deepcopy(value) @property def payload(self): @@ -90,7 +91,7 @@ def payload(self, value): msg += f"got {type(value).__name__} for " msg += f"value {value}" raise TypeError(msg) - self._payload = value + self._payload = copy.deepcopy(value) class Config2Payload(Payload): @@ -132,25 +133,34 @@ def commit(self): msg += "config is empty" raise ValueError(msg) - msg = f"{self.class_name}.{method_name}: " + + config = copy.deepcopy(self.config) + + msg = f"ZZZZ: {self.class_name}.{method_name}: " msg += f"state: {self.params['state']}" self.log.debug(msg) + msg = f"ZZZZ: {self.class_name}.{method_name}: " + msg += f"config: {json.dumps(config, indent=4, sort_keys=True)}" + self.log.debug(msg) + msg = f"ZZZZ: {self.class_name}.{method_name}: " + msg += f"payload: {json.dumps(self.payload, indent=4, sort_keys=True)}" + self.log.debug(msg) if self.params["state"] in ["deleted", "query"]: - self.payload["policyName"] = self.config["name"] + self.payload["policyName"] = config["name"] return - self.payload["agnostic"] = self.config["agnostic"] - self.payload["epldImgName"] = self.config["epld_image"] - self.payload["nxosVersion"] = self.config["release"] - self.payload["platform"] = self.config["platform"] - self.payload["policyDescr"] = self.config["description"] - self.payload["policyName"] = self.config["name"] - self.payload["policyType"] = self.config.get("type", "PLATFORM") - - if len(self.config.get("packages", {}).get("install", [])) != 0: - self.payload["packageName"] = ",".join(self.config["packages"]["install"]) - if len(self.config.get("packages", {}).get("uninstall", [])) != 0: - self.payload["rpmimages"] = ",".join(self.config["packages"]["uninstall"]) + self.payload["agnostic"] = config["agnostic"] + self.payload["epldImgName"] = config["epld_image"] + self.payload["nxosVersion"] = config["release"] + self.payload["platform"] = config["platform"] + self.payload["policyDescr"] = config["description"] + self.payload["policyName"] = config["name"] + self.payload["policyType"] = config.get("type", "PLATFORM") + + if len(config.get("packages", {}).get("install", [])) != 0: + self.payload["packageName"] = ",".join(config["packages"]["install"]) + if len(config.get("packages", {}).get("uninstall", [])) != 0: + self.payload["rpmimages"] = ",".join(config["packages"]["uninstall"]) msg = f"{self.class_name}.{method_name}: " msg += f"self.payload {json.dumps(self.payload, indent=4, sort_keys=True)}" @@ -185,20 +195,21 @@ def commit(self): msg += "payload is empty" raise ValueError(msg) - self.config["agnostic"] = self.payload["agnostic"] - self.config["epld_image"] = self.payload["epldImgName"] - self.config["release"] = self.payload["nxosVersion"] - self.config["platform"] = self.payload["platform"] - self.config["description"] = self.payload["policyDescr"] - self.config["name"] = self.payload["policyName"] - self.config["type"] = self.payload["policyType"] + payload = copy.deepcopy(self.payload) + self.config["agnostic"] = payload["agnostic"] + self.config["epld_image"] = payload["epldImgName"] + self.config["release"] = payload["nxosVersion"] + self.config["platform"] = payload["platform"] + self.config["description"] = payload["policyDescr"] + self.config["name"] = payload["policyName"] + self.config["type"] = payload["policyType"] self.config["packages"] = {} - if self.payload.get("packageName", "") != "": - self.config["packages"]["install"] = self.payload["packageName"].split(",") + if payload.get("packageName", "") != "": + self.config["packages"]["install"] = payload["packageName"].split(",") else: self.config["packages"]["install"] = [] - if self.payload.get("rpmimages", "") != "": - self.config["packages"]["uninstall"] = self.payload["rpmimages"].split(",") + if payload.get("rpmimages", "") != "": + self.config["packages"]["uninstall"] = payload["rpmimages"].split(",") else: self.config["packages"]["uninstall"] = [] diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/configs_Config2Payload.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/configs_Config2Payload.json new file mode 100644 index 000000000..e3a03393c --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/configs_Config2Payload.json @@ -0,0 +1,48 @@ +{ + "TEST_NOTES": [ + "Mocked payloads for ImagePolicyUpdate unit tests.", + "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py" + ], + "test_image_policy_payload_00120a": { + "agnostic": false, + "description": "image policy of 10.3(3)F", + "epld_image": "n9000-epld.10.3.2.F.img", + "name": "FOO", + "packages": { + "install": [ + "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm" + ], + "uninstall": [ + "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" + ] + }, + "platform": "N9K", + "release": "10.3.1_nxos64-cs_64bit", + "type": "PLATFORM" + }, + "test_image_policy_payload_00121a": { + "agnostic": false, + "description": "BAR", + "epld_image": "", + "name": "BAR", + "packages": { + "install": [], + "uninstall": [] + }, + "platform": "N9K", + "release": "10.3.1_nxos64-cs_64bit" + }, + "test_image_policy_payload_00122a": {}, + "test_image_policy_payload_00123a": { + "agnostic": false, + "description": "BAR", + "epld_image": "", + "name": "BAR", + "packages": { + "install": [], + "uninstall": [] + }, + "platform": "N9K", + "release": "10.3.1_nxos64-cs_64bit" + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/configs_Payload2Config.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/configs_Payload2Config.json new file mode 100644 index 000000000..d6e1b1ef3 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/configs_Payload2Config.json @@ -0,0 +1,37 @@ +{ + "TEST_NOTES": [ + "Mocked payloads for ImagePolicyUpdate unit tests.", + "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py" + ], + "test_image_policy_payload_00220a": { + "agnostic": false, + "description": "image policy of 10.3(3)F", + "epld_image": "n9000-epld.10.3.2.F.img", + "name": "FOO", + "packages": { + "install": [ + "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm" + ], + "uninstall": [ + "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" + ] + }, + "platform": "N9K", + "release": "10.3.1_nxos64-cs_64bit", + "type": "PLATFORM" + }, + "test_image_policy_payload_00221a": { + "agnostic": false, + "description": "BAR", + "epld_image": "", + "name": "BAR", + "packages": { + "install": [], + "uninstall": [] + }, + "platform": "N9K", + "release": "10.3.1_nxos64-cs_64bit", + "type": "PLATFORM" + }, + "test_image_policy_payload_00222a": {} +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/data_payload.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/data_payload.json deleted file mode 100644 index 77f60cb3a..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/data_payload.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "test_image_policy_payload_00120a": { - "config": { - "agnostic": false, - "description": "image policy of 10.3(3)F", - "epld_image": "n9000-epld.10.3.2.F.img", - "name": "FOO", - "packages": { - "install": [ - "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm" - ], - "uninstall": [ - "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - ] - }, - "platform": "N9K", - "release": "10.3.1_nxos64-cs_64bit", - "type": "PLATFORM" - }, - "payload": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.2.F.img", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K", - "policyDescr": "image policy of 10.3(3)F", - "policyName": "FOO", - "policyType": "PLATFORM", - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - }, - "test_image_policy_payload_00121a": { - "config": { - "agnostic": false, - "description": "BAR", - "epld_image": "", - "name": "BAR", - "packages": { - "install": [], - "uninstall": [] - }, - "platform": "N9K", - "release": "10.3.1_nxos64-cs_64bit" - }, - "payload": { - "agnostic": false, - "epldImgName": "", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "platform": "N9K", - "policyDescr": "BAR", - "policyName": "BAR", - "policyType": "PLATFORM" - } - }, - "test_image_policy_payload_00122a": { - "config": {} - }, - "test_image_policy_payload_00123a": { - "config": { - "agnostic": false, - "description": "BAR", - "epld_image": "", - "name": "BAR", - "packages": { - "install": [], - "uninstall": [] - }, - "platform": "N9K", - "release": "10.3.1_nxos64-cs_64bit" - }, - "payload": { - "agnostic": false, - "epldImgName": "", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "platform": "N9K", - "policyDescr": "BAR", - "policyName": "BAR", - "policyType": "PLATFORM" - } - }, - "test_image_policy_payload_00220a": { - "config": { - "agnostic": false, - "description": "image policy of 10.3(3)F", - "epld_image": "n9000-epld.10.3.2.F.img", - "name": "FOO", - "packages": { - "install": [ - "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm" - ], - "uninstall": [ - "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - ] - }, - "platform": "N9K", - "release": "10.3.1_nxos64-cs_64bit", - "type": "PLATFORM" - }, - "payload": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.2.F.img", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K", - "policyDescr": "image policy of 10.3(3)F", - "policyName": "FOO", - "policyType": "PLATFORM", - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - }, - "test_image_policy_payload_00221a": { - "config": { - "agnostic": false, - "description": "BAR", - "epld_image": "", - "name": "BAR", - "packages": { - "install": [], - "uninstall": [] - }, - "platform": "N9K", - "release": "10.3.1_nxos64-cs_64bit", - "type": "PLATFORM" - }, - "payload": { - "agnostic": false, - "epldImgName": "", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "platform": "N9K", - "policyDescr": "BAR", - "policyName": "BAR", - "policyType": "PLATFORM" - } - }, - "test_image_policy_payload_00222a": { - "payload": {} - } -} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_Config2Payload.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_Config2Payload.json new file mode 100644 index 000000000..b41bd3dc9 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_Config2Payload.json @@ -0,0 +1,36 @@ +{ + "TEST_NOTES": [ + "Mocked payloads for ImagePolicyUpdate unit tests.", + "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py" + ], + "test_image_policy_payload_00120a": { + "agnostic": false, + "epldImgName": "n9000-epld.10.3.2.F.img", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", + "platform": "N9K", + "policyDescr": "image policy of 10.3(3)F", + "policyName": "FOO", + "policyType": "PLATFORM", + "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" + }, + "test_image_policy_payload_00121a": { + "agnostic": false, + "epldImgName": "", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "platform": "N9K", + "policyDescr": "BAR", + "policyName": "BAR", + "policyType": "PLATFORM" + }, + "test_image_policy_payload_00122a": {}, + "test_image_policy_payload_00123a": { + "agnostic": false, + "epldImgName": "", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "platform": "N9K", + "policyDescr": "BAR", + "policyName": "BAR", + "policyType": "PLATFORM" + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_Payload2Config.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_Payload2Config.json new file mode 100644 index 000000000..2bdb71284 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/payloads_Payload2Config.json @@ -0,0 +1,27 @@ +{ + "TEST_NOTES": [ + "Mocked payloads for ImagePolicyUpdate unit tests.", + "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_update_bulk.py" + ], + "test_image_policy_payload_00220a": { + "agnostic": false, + "epldImgName": "n9000-epld.10.3.2.F.img", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", + "platform": "N9K", + "policyDescr": "image policy of 10.3(3)F", + "policyName": "FOO", + "policyType": "PLATFORM", + "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" + }, + "test_image_policy_payload_00221a": { + "agnostic": false, + "epldImgName": "", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "platform": "N9K", + "policyDescr": "BAR", + "policyName": "BAR", + "policyType": "PLATFORM" + }, + "test_image_policy_payload_00222a": {} +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py index d722119b9..f5b2f938d 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py @@ -33,15 +33,14 @@ __author__ = "Allen Robel" -from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ - Results from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.payload import ( Config2Payload, Payload2Config) -from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.fixture import \ - load_fixture +from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ + ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - AnsibleFailJson, MockAnsibleModule, config2payload_fixture, does_not_raise, - payload2config_fixture) + config2payload_fixture, configs_config2payload, configs_payload2config, + does_not_raise, payload2config_fixture, payloads_config2payload, + payloads_payload2config) def test_image_policy_payload_00100() -> None: @@ -56,8 +55,8 @@ def test_image_policy_payload_00100() -> None: Verify Config2Payload is initialized properly ### Test - - Class attributes initialized to expected values - - fail_json is not called + - Class attributes initialized to expected values + - Exceptions are not raised. """ with does_not_raise(): instance = Config2Payload() @@ -85,15 +84,21 @@ def test_image_policy_payload_00120(config2payload) -> None: method_name = inspect.stack()[0][3] key = f"{method_name}a" - data = load_fixture("data_payload") - config = data.get(key, {}).get("config") - payload = data.get(key, {}).get("payload") + def configs(): + yield configs_config2payload(key) + gen_configs = ResponseGenerator(configs()) + + def payloads(): + yield payloads_config2payload(key) + gen_payloads = ResponseGenerator(payloads()) + config = gen_configs.next + payload = gen_payloads.next with does_not_raise(): instance = config2payload + instance.params = {"state": "merged", "check_mode": False} instance.config = config instance.commit() - assert payload is not None assert instance.payload == payload @@ -106,24 +111,32 @@ def test_image_policy_payload_00121(config2payload) -> None: - commit ### Summary - Verify Config2Payload coverts a configuration to a proper payload when + Verify ``Config2Payload`` coverts a configuration to a proper payload when the packages.install and packages.uninstall keys are empty lists. ### Test - - config packages.install is an empty list - - config packages.ininstall is an empty list - - commit converts config to a proper payload + - config packages.install is an empty list. + - config packages.ininstall is an empty list. + - commit converts config to a proper payload. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" - data = load_fixture("data_payload") - config = data.get(key, {}).get("config") - payload = data.get(key, {}).get("payload") + def configs(): + yield configs_config2payload(key) + gen_configs = ResponseGenerator(configs()) + + def payloads(): + yield payloads_config2payload(key) + gen_payloads = ResponseGenerator(payloads()) + + config = gen_configs.next + payload = gen_payloads.next with does_not_raise(): instance = config2payload instance.config = config + instance.params = {"state": "merged", "check_mode": False} instance.commit() assert payload is not None assert instance.payload == payload @@ -138,20 +151,25 @@ def test_image_policy_payload_00122(config2payload) -> None: - commit ### Summary - Verify Config2Payload.commit() calls fail_json when config is an empty dict + Verify ``Config2Payload.commit()`` raises ``ValueError`` when config + is an empty dict. ### Test - - config is set to an empty dict - - commit calls fail_json + - ``config`` is set to an empty dict. + - ``commit`` raises ``ValueError``. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" - data = load_fixture("data_payload") - config = data.get(key, {}).get("config") + def configs(): + yield configs_config2payload(key) + gen_configs = ResponseGenerator(configs()) + + config = gen_configs.next with does_not_raise(): - instance = config2payload + instance = Config2Payload() + instance.params = {"state": "deleted", "check_mode": False} instance.config = config match = r"Config2Payload\.commit: config is empty" with pytest.raises(ValueError, match=match): @@ -174,25 +192,23 @@ def test_image_policy_payload_00123(config2payload, state) -> None: ### Test - payload contains only the policyName key - The value of the policyName key == value of the name key in instance.config - - fail_json is not called + - Exceptions are not raised. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" - data = load_fixture("data_payload") - config = data.get(key, {}).get("config") + def configs(): + yield configs_config2payload(key) + gen_configs = ResponseGenerator(configs()) + + config = gen_configs.next with does_not_raise(): instance = config2payload instance.config = config + instance.params = {"state": state, "check_mode": False} instance.commit() - assert instance.payload["agnostic"] == config["agnostic"] - assert instance.payload["policyDescr"] == config["description"] assert instance.payload["policyName"] == config["name"] - assert instance.payload["epldImgName"] == config["epld_image"] - assert instance.payload["nxosVersion"] == config["release"] - assert instance.payload["platform"] == config["platform"] - assert instance.payload["policyType"] == "PLATFORM" MATCH_00130 = r"Config2Payload\.payload:\s+" @@ -318,9 +334,16 @@ def test_image_policy_payload_00220(payload2config) -> None: method_name = inspect.stack()[0][3] key = f"{method_name}a" - data = load_fixture("data_payload") - config = data.get(key, {}).get("config") - payload = data.get(key, {}).get("payload") + def configs(): + yield configs_payload2config(key) + gen_configs = ResponseGenerator(configs()) + + def payloads(): + yield payloads_payload2config(key) + gen_payloads = ResponseGenerator(payloads()) + + config = gen_configs.next + payload = gen_payloads.next with does_not_raise(): instance = payload2config instance.payload = payload @@ -349,9 +372,16 @@ def test_image_policy_payload_00221(payload2config) -> None: method_name = inspect.stack()[0][3] key = f"{method_name}a" - data = load_fixture("data_payload") - config = data.get(key, {}).get("config") - payload = data.get(key, {}).get("payload") + def configs(): + yield configs_payload2config(key) + gen_configs = ResponseGenerator(configs()) + + def payloads(): + yield payloads_payload2config(key) + gen_payloads = ResponseGenerator(payloads()) + + config = gen_configs.next + payload = gen_payloads.next with does_not_raise(): instance = payload2config @@ -380,8 +410,11 @@ def test_image_policy_payload_00222(payload2config) -> None: method_name = inspect.stack()[0][3] key = f"{method_name}a" - data = load_fixture("data_payload") - payload = data.get(key, {}).get("payload") + def payloads(): + yield payloads_payload2config(key) + gen_payloads = ResponseGenerator(payloads()) + + payload = gen_payloads.next with does_not_raise(): instance = payload2config @@ -461,8 +494,8 @@ def test_image_policy_payload_00240(payload2config, value, expected) -> None: Verify config setter error handling ### Test - - config accepts a dictionary - - config calls fail_json for non-dictionary values + - `config accepts a dictionary. + - `config raises ``TypeError`` for non-dictionary values. """ with does_not_raise(): instance = payload2config diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py index e68ba9729..2a96109bb 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py @@ -357,6 +357,46 @@ def data_payload(key: str) -> Dict[str, str]: return data +def configs_config2payload(key: str) -> Dict[str, str]: + """ + Return configs for Config2Payload + """ + data_file = "configs_Config2Payload" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def configs_payload2config(key: str) -> Dict[str, str]: + """ + Return configs for Payload2Config + """ + data_file = "configs_Payload2Config" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def payloads_config2payload(key: str) -> Dict[str, str]: + """ + Return payloads for Config2Payload + """ + data_file = "payloads_Config2Payload" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + +def payloads_payload2config(key: str) -> Dict[str, str]: + """ + Return payloads for Payload2Config + """ + data_file = "payloads_Payload2Config" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + return data + + def payloads_image_policy_create(key: str) -> Dict[str, str]: """ Return payloads for ImagePolicyCreate From 824be641c3306710121b0b4e6c68c9d7ffc91f2f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 1 Jul 2024 10:35:27 -1000 Subject: [PATCH 220/230] Remove ApiEndpoints() and associated test cases. These are replaced with the Api() endpoints. --- .../module_utils/image_policy/endpoints.py | 138 ------------------ plugins/modules/dcnm_image_policy.py | 5 - .../test_image_policy_endpoints.py | 110 -------------- 3 files changed, 253 deletions(-) delete mode 100644 plugins/module_utils/image_policy/endpoints.py delete mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_endpoints.py diff --git a/plugins/module_utils/image_policy/endpoints.py b/plugins/module_utils/image_policy/endpoints.py deleted file mode 100644 index ff41dca7e..000000000 --- a/plugins/module_utils/image_policy/endpoints.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import logging - - -class ApiEndpoints: - """ - Endpoints for image policy API calls - """ - - def __init__(self): - self.class_name = self.__class__.__name__ - - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED ApiEndpoints()") - - self.endpoint_api_v1 = "/appcenter/cisco/ndfc/api/v1" - - self.endpoint_image_management = f"{self.endpoint_api_v1}" - self.endpoint_image_management += "/imagemanagement" - - self.endpoint_policy_mgnt = f"{self.endpoint_image_management}" - self.endpoint_policy_mgnt += "/rest/policymgnt" - - @property - def policies_attached_info(self): - """ - return endpoint GET /rest/policymgnt/all-attached-policies - """ - path = f"{self.endpoint_policy_mgnt}/all-attached-policies" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def policies_info(self): - """ - return endpoint GET /rest/policymgnt/policies - """ - path = f"{self.endpoint_policy_mgnt}/policies" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint - - @property - def policy_attach(self): - """ - return endpoint POST /rest/policymgnt/attach-policy - """ - path = f"{self.endpoint_policy_mgnt}/attach-policy" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def policy_create(self): - """ - return endpoint POST /rest/policymgnt/platform-policy - """ - path = f"{self.endpoint_policy_mgnt}/platform-policy" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def policy_delete(self): - """ - return endpoint DELETE /rest/policymgnt/policy - This expects a request body with the following: - - policyNames: comma separated list of policy names to delete. - - { - "policyNames": "policyA,policyB,etc" - } - """ - path = f"{self.endpoint_policy_mgnt}/policy" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "DELETE" - return endpoint - - @property - def policy_detach(self): - """ - return endpoint DELETE /rest/policymgnt/detach-policy - """ - path = f"{self.endpoint_policy_mgnt}/detach-policy" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "DELETE" - return endpoint - - @property - def policy_edit(self): - """ - return endpoint POST /rest/policymgnt/edit-policy - """ - path = f"{self.endpoint_policy_mgnt}/edit-policy" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "POST" - return endpoint - - @property - def policy_info(self): - """ - return endpoint GET /rest/policymgnt/image-policy/__POLICY_NAME__ - - Replace __POLICY_NAME__ with the policy_name to query - e.g. path.replace("__POLICY_NAME__", "NR1F") - """ - path = f"{self.endpoint_policy_mgnt}/image-policy/__POLICY_NAME__" - endpoint = {} - endpoint["path"] = path - endpoint["verb"] = "GET" - return endpoint diff --git a/plugins/modules/dcnm_image_policy.py b/plugins/modules/dcnm_image_policy.py index 80bff4664..a01bc1904 100644 --- a/plugins/modules/dcnm_image_policy.py +++ b/plugins/modules/dcnm_image_policy.py @@ -281,8 +281,6 @@ ImagePolicyCreateBulk from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.delete import \ ImagePolicyDelete -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ ImagePolicies from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.params_spec_v2 import \ @@ -315,9 +313,6 @@ def __init__(self, params): method_name = inspect.stack()[0][3] self.params = params - self.endpoints = ApiEndpoints() - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.check_mode = self.params.get("check_mode", None) if self.check_mode is None: msg = f"{self.class_name}.{method_name}: " diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_endpoints.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_endpoints.py deleted file mode 100644 index aa9603dd7..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_endpoints.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." -__author__ = "Allen Robel" - -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.endpoints import \ - ApiEndpoints - - -def test_image_policy_endpoints_00001() -> None: - """ - Endpoints.__init__ - """ - endpoints = ApiEndpoints() - assert endpoints.endpoint_api_v1 == "/appcenter/cisco/ndfc/api/v1" - assert ( - endpoints.endpoint_image_management - == "/appcenter/cisco/ndfc/api/v1/imagemanagement" - ) - assert ( - endpoints.endpoint_policy_mgnt - == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt" - ) - - -def test_image_policy_endpoints_00010() -> None: - """ - Endpoints.policies_attached_info - """ - endpoints = ApiEndpoints() - assert endpoints.policies_attached_info.get("verb") == "GET" - assert ( - endpoints.policies_attached_info.get("path") - == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/all-attached-policies" - ) - - -def test_image_policy_endpoints_00020() -> None: - """ - Endpoints.policies_info - """ - endpoints = ApiEndpoints() - assert endpoints.policies_info.get("verb") == "GET" - assert ( - endpoints.policies_info.get("path") - == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies" - ) - - -def test_image_policy_endpoints_00030() -> None: - """ - Endpoints.policy_attach - """ - endpoints = ApiEndpoints() - assert endpoints.policy_attach.get("verb") == "POST" - assert ( - endpoints.policy_attach.get("path") - == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/attach-policy" - ) - - -def test_image_policy_endpoints_00040() -> None: - """ - Endpoints.policy_create - """ - endpoints = ApiEndpoints() - assert endpoints.policy_create.get("verb") == "POST" - assert ( - endpoints.policy_create.get("path") - == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy" - ) - - -def test_image_policy_endpoints_00050() -> None: - """ - Endpoints.policy_detach - """ - endpoints = ApiEndpoints() - assert endpoints.policy_detach.get("verb") == "DELETE" - assert ( - endpoints.policy_detach.get("path") - == "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/detach-policy" - ) - - -def test_image_policy_endpoints_00060() -> None: - """ - Endpoints.policy_info - """ - path = "/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/" - path += "image-policy/__POLICY_NAME__" - endpoints = ApiEndpoints() - assert endpoints.policy_info.get("verb") == "GET" - assert endpoints.policy_info.get("path") == path From 30bbd1067aa36e88f4ea3c052d8a4ce655aadbd0 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 1 Jul 2024 10:44:59 -1000 Subject: [PATCH 221/230] Final cleanup. 1. replace params_spec.py with params_spec_v2.py (i.e. rename params_spec_v2.py to params_spec.py, removing the original params_spec.py). 2. dcnm_image_policy.py: Change import ParamsSpec() from the new params_spec. 3. payload.py: remove debug statements. 4. --- .../module_utils/image_policy/params_spec.py | 165 +++++++------ .../image_policy/params_spec_v2.py | 225 ------------------ plugins/module_utils/image_policy/payload.py | 11 - plugins/modules/dcnm_image_policy.py | 2 +- .../test_image_policy_delete.py | 2 +- 5 files changed, 90 insertions(+), 315 deletions(-) delete mode 100644 plugins/module_utils/image_policy/params_spec_v2.py diff --git a/plugins/module_utils/image_policy/params_spec.py b/plugins/module_utils/image_policy/params_spec.py index a4494292c..e76ac0215 100644 --- a/plugins/module_utils/image_policy/params_spec.py +++ b/plugins/module_utils/image_policy/params_spec.py @@ -27,39 +27,45 @@ class ParamsSpec: Parameter specifications for the dcnm_image_policy module. """ - def __init__(self, ansible_module): + def __init__(self): self.class_name = self.__class__.__name__ - self.ansible_module = ansible_module - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.log.debug("ENTERED ParamsSpec()") - self._params_spec: Dict[str, Any] = {} + self._params_spec: dict = {} + self.valid_states = set() + self.valid_states.add("deleted") + self.valid_states.add("merged") + self.valid_states.add("overridden") + self.valid_states.add("query") + self.valid_states.add("replaced") + + self.log.debug("ENTERED ParamsSpec() v2") def commit(self): """ - build the parameter specification based on the state + Build the parameter specification based on the state + + ## Raises + - ``ValueError`` if params is not set + """ method_name = inspect.stack()[0][3] - if self.ansible_module.params["state"] is None: - self.ansible_module.fail_json(msg="state is None") + if self._params is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"params must be set before calling {method_name}." + raise ValueError(msg) - if self.ansible_module.params["state"] == "merged": - # self._build_params_spec_for_merged_state() - self._build_params_spec_for_merged_state_proposed() - elif self.ansible_module.params["state"] == "replaced": - self._build_params_spec_for_replaced_state() - elif self.ansible_module.params["state"] == "overridden": - self._build_params_spec_for_overridden_state() - elif self.ansible_module.params["state"] == "deleted": + if self.params["state"] == "deleted": self._build_params_spec_for_deleted_state() - elif self.ansible_module.params["state"] == "query": + if self.params["state"] == "merged": + self._build_params_spec_for_merged_state() + if self.params["state"] == "overridden": + self._build_params_spec_for_overridden_state() + if self.params["state"] == "query": self._build_params_spec_for_query_state() - else: - msg = f"{self.class_name}.{method_name}: " - msg += f"Invalid state {self.ansible_module.params['state']}" - self.ansible_module.fail_json(msg) + if self.params["state"] == "replaced": + self._build_params_spec_for_replaced_state() def _build_params_spec_for_merged_state(self) -> None: """ @@ -69,57 +75,7 @@ def _build_params_spec_for_merged_state(self) -> None: Return: params_spec, a dictionary containing playbook parameter specifications. """ - # print("Building params spec for merged state") - self._params_spec: Dict[str, Any] = {} - - self._params_spec["agnostic"] = {} - self._params_spec["agnostic"]["required"] = False - self._params_spec["agnostic"]["type"] = "bool" - self._params_spec["agnostic"]["default"] = False - - self._params_spec["description"] = {} - self._params_spec["description"]["default"] = "" - self._params_spec["description"]["required"] = False - self._params_spec["description"]["type"] = "str" - - self._params_spec["disabled_rpm"] = {} - self._params_spec["disabled_rpm"]["default"] = "" - self._params_spec["disabled_rpm"]["required"] = False - self._params_spec["disabled_rpm"]["type"] = "str" - - self._params_spec["epld_image"] = {} - self._params_spec["epld_image"]["default"] = "" - self._params_spec["epld_image"]["required"] = False - self._params_spec["epld_image"]["type"] = "str" - - self._params_spec["name"] = {} - self._params_spec["name"]["required"] = True - self._params_spec["name"]["type"] = "str" - - self._params_spec["platform"] = {} - self._params_spec["platform"]["required"] = True - self._params_spec["platform"]["type"] = "str" - self._params_spec["platform"]["choices"] = ["N9K", "N7K", "N77", "N6K", "N5K"] - - self._params_spec["release"] = {} - self._params_spec["release"]["required"] = True - self._params_spec["release"]["type"] = "str" - - self._params_spec["packages"] = {} - self._params_spec["packages"]["default"] = [] - self._params_spec["packages"]["required"] = False - self._params_spec["packages"]["type"] = "list" - - def _build_params_spec_for_merged_state_proposed(self) -> None: - """ - Build the specs for the parameters expected when state == merged. - - Caller: _validate_configs() - Return: params_spec, a dictionary containing playbook - parameter specifications. - """ - # print("Building params spec for merged state PROPOSED") - self._params_spec: Dict[str, Any] = {} + self._params_spec: dict = {} self._params_spec["agnostic"] = {} self._params_spec["agnostic"]["default"] = False @@ -170,10 +126,10 @@ def _build_params_spec_for_merged_state_proposed(self) -> None: self._params_spec["type"]["type"] = "str" def _build_params_spec_for_overridden_state(self) -> None: - self._build_params_spec_for_merged_state_proposed() + self._build_params_spec_for_merged_state() def _build_params_spec_for_replaced_state(self) -> None: - self._build_params_spec_for_merged_state_proposed() + self._build_params_spec_for_merged_state() def _build_params_spec_for_deleted_state(self) -> None: """ @@ -183,7 +139,7 @@ def _build_params_spec_for_deleted_state(self) -> None: Return: params_spec, a dictionary containing playbook parameter specifications. """ - self._params_spec: Dict[str, Any] = {} + self._params_spec: dict = {} self._params_spec["name"] = {} self._params_spec["name"]["required"] = True @@ -197,14 +153,69 @@ def _build_params_spec_for_query_state(self) -> None: Return: params_spec, a dictionary containing playbook parameter specifications. """ - self._params_spec: Dict[str, Any] = {} + self._params_spec: dict = {} self._params_spec["name"] = {} self._params_spec["name"]["required"] = True self._params_spec["name"]["type"] = "str" def _build_params_spec_for_replaced_state(self) -> None: - self._build_params_spec_for_merged_state_proposed() + self._build_params_spec_for_merged_state() + + @property + def params(self) -> dict: + """ + ### Summary + Expects value to be a dictionary containing, at mimimum, + the key "state" with value of one of: + - deleted + - merged + - overridden + - query + - replaced + + ### Raises + - setter: ``ValueError`` if value is not a dict + - setter: ``ValueError`` if value["state"] is missing + - setter: ``ValueError`` if value["state"] is not a valid state + + ### Details + - Valid params: + - ``{"state": "deleted"}`` + - ``{"state": "merged"}`` + - ``{"state": "overridden"}`` + - ``{"state": "query"}`` + - ``{"state": "replaced"}`` + - getter: return the params + - setter: set the params + """ + return self._params + + @params.setter + def params(self, value: dict) -> None: + """ + - setter: set the params + """ + method_name = inspect.stack()[0][3] + if not isinstance(value, dict): + msg = f"{self.class_name}.{method_name}.setter: " + msg += "Invalid type. Expected dict but " + msg += f"got type {type(value).__name__}, " + msg += f"value {value}." + raise TypeError(msg) + + if value.get("state", None) is None: + msg = f"{self.class_name}.{method_name}.setter: " + msg += "params.state is required but missing." + raise ValueError(msg) + + if value["state"] not in self.valid_states: + msg = f"{self.class_name}.{method_name}.setter: " + msg += f"params.state is invalid: {value['state']}. " + msg += f"Expected one of {', '.join(self.valid_states)}." + raise ValueError(msg) + + self._params = value @property def params_spec(self) -> Dict[str, Any]: diff --git a/plugins/module_utils/image_policy/params_spec_v2.py b/plugins/module_utils/image_policy/params_spec_v2.py deleted file mode 100644 index e76ac0215..000000000 --- a/plugins/module_utils/image_policy/params_spec_v2.py +++ /dev/null @@ -1,225 +0,0 @@ -# Copyright (c) 2024 Cisco and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type -__author__ = "Allen Robel" - -import inspect -import logging -from typing import Any, Dict - - -class ParamsSpec: - """ - Parameter specifications for the dcnm_image_policy module. - """ - - def __init__(self): - self.class_name = self.__class__.__name__ - self.log = logging.getLogger(f"dcnm.{self.class_name}") - - self._params_spec: dict = {} - self.valid_states = set() - self.valid_states.add("deleted") - self.valid_states.add("merged") - self.valid_states.add("overridden") - self.valid_states.add("query") - self.valid_states.add("replaced") - - self.log.debug("ENTERED ParamsSpec() v2") - - def commit(self): - """ - Build the parameter specification based on the state - - ## Raises - - ``ValueError`` if params is not set - - """ - method_name = inspect.stack()[0][3] - - if self._params is None: - msg = f"{self.class_name}.{method_name}: " - msg += f"params must be set before calling {method_name}." - raise ValueError(msg) - - if self.params["state"] == "deleted": - self._build_params_spec_for_deleted_state() - if self.params["state"] == "merged": - self._build_params_spec_for_merged_state() - if self.params["state"] == "overridden": - self._build_params_spec_for_overridden_state() - if self.params["state"] == "query": - self._build_params_spec_for_query_state() - if self.params["state"] == "replaced": - self._build_params_spec_for_replaced_state() - - def _build_params_spec_for_merged_state(self) -> None: - """ - Build the specs for the parameters expected when state == merged. - - Caller: _validate_configs() - Return: params_spec, a dictionary containing playbook - parameter specifications. - """ - self._params_spec: dict = {} - - self._params_spec["agnostic"] = {} - self._params_spec["agnostic"]["default"] = False - self._params_spec["agnostic"]["required"] = False - self._params_spec["agnostic"]["type"] = "bool" - - self._params_spec["description"] = {} - self._params_spec["description"]["default"] = "" - self._params_spec["description"]["required"] = False - self._params_spec["description"]["type"] = "str" - - self._params_spec["epld_image"] = {} - self._params_spec["epld_image"]["default"] = "" - self._params_spec["epld_image"]["required"] = False - self._params_spec["epld_image"]["type"] = "str" - - self._params_spec["name"] = {} - self._params_spec["name"]["required"] = True - self._params_spec["name"]["type"] = "str" - - self._params_spec["platform"] = {} - self._params_spec["platform"]["required"] = True - self._params_spec["platform"]["type"] = "str" - self._params_spec["platform"]["choices"] = ["N9K", "N7K", "N77", "N6K", "N5K"] - - self._params_spec["packages"] = {} - self._params_spec["packages"]["default"] = {} - self._params_spec["packages"]["required"] = False - self._params_spec["packages"]["type"] = "dict" - - self._params_spec["packages"]["install"] = {} - self._params_spec["packages"]["install"]["default"] = [] - self._params_spec["packages"]["install"]["required"] = False - self._params_spec["packages"]["install"]["type"] = "list" - - self._params_spec["packages"]["uninstall"] = {} - self._params_spec["packages"]["uninstall"]["default"] = [] - self._params_spec["packages"]["uninstall"]["required"] = False - self._params_spec["packages"]["uninstall"]["type"] = "list" - - self._params_spec["release"] = {} - self._params_spec["release"]["required"] = True - self._params_spec["release"]["type"] = "str" - - self._params_spec["type"] = {} - self._params_spec["type"]["default"] = "PLATFORM" - self._params_spec["type"]["required"] = False - self._params_spec["type"]["type"] = "str" - - def _build_params_spec_for_overridden_state(self) -> None: - self._build_params_spec_for_merged_state() - - def _build_params_spec_for_replaced_state(self) -> None: - self._build_params_spec_for_merged_state() - - def _build_params_spec_for_deleted_state(self) -> None: - """ - Build the specs for the parameters expected when state == deleted. - - Caller: _validate_configs() - Return: params_spec, a dictionary containing playbook - parameter specifications. - """ - self._params_spec: dict = {} - - self._params_spec["name"] = {} - self._params_spec["name"]["required"] = True - self._params_spec["name"]["type"] = "str" - - def _build_params_spec_for_query_state(self) -> None: - """ - Build the specs for the parameters expected when state == query. - - Caller: _validate_configs() - Return: params_spec, a dictionary containing playbook - parameter specifications. - """ - self._params_spec: dict = {} - - self._params_spec["name"] = {} - self._params_spec["name"]["required"] = True - self._params_spec["name"]["type"] = "str" - - def _build_params_spec_for_replaced_state(self) -> None: - self._build_params_spec_for_merged_state() - - @property - def params(self) -> dict: - """ - ### Summary - Expects value to be a dictionary containing, at mimimum, - the key "state" with value of one of: - - deleted - - merged - - overridden - - query - - replaced - - ### Raises - - setter: ``ValueError`` if value is not a dict - - setter: ``ValueError`` if value["state"] is missing - - setter: ``ValueError`` if value["state"] is not a valid state - - ### Details - - Valid params: - - ``{"state": "deleted"}`` - - ``{"state": "merged"}`` - - ``{"state": "overridden"}`` - - ``{"state": "query"}`` - - ``{"state": "replaced"}`` - - getter: return the params - - setter: set the params - """ - return self._params - - @params.setter - def params(self, value: dict) -> None: - """ - - setter: set the params - """ - method_name = inspect.stack()[0][3] - if not isinstance(value, dict): - msg = f"{self.class_name}.{method_name}.setter: " - msg += "Invalid type. Expected dict but " - msg += f"got type {type(value).__name__}, " - msg += f"value {value}." - raise TypeError(msg) - - if value.get("state", None) is None: - msg = f"{self.class_name}.{method_name}.setter: " - msg += "params.state is required but missing." - raise ValueError(msg) - - if value["state"] not in self.valid_states: - msg = f"{self.class_name}.{method_name}.setter: " - msg += f"params.state is invalid: {value['state']}. " - msg += f"Expected one of {', '.join(self.valid_states)}." - raise ValueError(msg) - - self._params = value - - @property - def params_spec(self) -> Dict[str, Any]: - """ - return the parameter specification - """ - return self._params_spec diff --git a/plugins/module_utils/image_policy/payload.py b/plugins/module_utils/image_policy/payload.py index 821491d7f..f9dced4b0 100644 --- a/plugins/module_utils/image_policy/payload.py +++ b/plugins/module_utils/image_policy/payload.py @@ -133,19 +133,8 @@ def commit(self): msg += "config is empty" raise ValueError(msg) - config = copy.deepcopy(self.config) - msg = f"ZZZZ: {self.class_name}.{method_name}: " - msg += f"state: {self.params['state']}" - self.log.debug(msg) - msg = f"ZZZZ: {self.class_name}.{method_name}: " - msg += f"config: {json.dumps(config, indent=4, sort_keys=True)}" - self.log.debug(msg) - msg = f"ZZZZ: {self.class_name}.{method_name}: " - msg += f"payload: {json.dumps(self.payload, indent=4, sort_keys=True)}" - self.log.debug(msg) - if self.params["state"] in ["deleted", "query"]: self.payload["policyName"] = config["name"] return diff --git a/plugins/modules/dcnm_image_policy.py b/plugins/modules/dcnm_image_policy.py index a01bc1904..b922d5c03 100644 --- a/plugins/modules/dcnm_image_policy.py +++ b/plugins/modules/dcnm_image_policy.py @@ -283,7 +283,7 @@ ImagePolicyDelete from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.image_policies import \ ImagePolicies -from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.params_spec_v2 import \ +from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.params_spec import \ ParamsSpec from ansible_collections.cisco.dcnm.plugins.module_utils.image_policy.payload import \ Config2Payload diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py index 886ec3248..1051f98e4 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py @@ -43,7 +43,7 @@ from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - MockAnsibleModule, MockImagePolicies, does_not_raise, + MockAnsibleModule, does_not_raise, image_policy_delete_fixture, params, responses_ep_policies, responses_ep_policy_delete, results_image_policy_delete) From b71415b5f3e44d2a5310091b5fe55f9cc6ca69ff Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 1 Jul 2024 11:41:24 -1000 Subject: [PATCH 222/230] ImagePolicyCreate*: update unit tests to use ResponseGenerator() 1. Update remaining unit tests which were still using MockImagePolicies() to use ResponseGenerator() instead. 2. utils.py: Remove MockImagePolicies() and its associated fixtures. --- .../fixtures/all_policies_ImagePolicies.json | 742 ------------------ .../fixtures/responses_EpPolicies.json | 222 ++++++ .../fixtures/responses_EpPolicyCreate.json | 21 + .../fixtures/responses_ImagePolicies.json | 24 - .../fixtures/results_ImagePolicies.json | 20 - .../test_image_policy_create.py | 127 ++- .../test_image_policy_create_bulk.py | 194 +++-- .../modules/dcnm/dcnm_image_policy/utils.py | 104 --- 8 files changed, 430 insertions(+), 1024 deletions(-) delete mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/all_policies_ImagePolicies.json delete mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_ImagePolicies.json delete mode 100644 tests/unit/modules/dcnm/dcnm_image_policy/fixtures/results_ImagePolicies.json diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/all_policies_ImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/all_policies_ImagePolicies.json deleted file mode 100644 index 83ff30d95..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/all_policies_ImagePolicies.json +++ /dev/null @@ -1,742 +0,0 @@ -{ - "TEST_NOTES": [ - "Mock return values for the ImagePolicies().all_policies property", - "i.e. the controller response consisting of all image policies that", - "exist on the controller, keyed on policy_name." - ], - "test_image_policy_create_00030a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_create_00031a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_create_00034a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_create_00035a": {}, - "test_image_policy_create_bulk_00030a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_create_bulk_00031a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_create_bulk_00032a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_create_bulk_00034a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - }, - "test_image_policy_create_bulk_00035a": {}, - "test_image_policy_create_bulk_00036a": {}, - "test_image_policy_create_bulk_00037a": {}, - "test_image_policy_delete_00030a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_delete_00031a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_delete_00032a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_delete_00034a": {}, - "test_image_policy_delete_00036a": {}, - "test_image_policy_delete_00037a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "" - } - }, - "test_image_policy_delete_00038a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "" - } - }, - "test_image_policy_query_00030a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_query_00031a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_query_00032a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_query_00033a": {}, - "test_image_policy_replace_bulk_00030a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_replace_bulk_00031a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_replace_bulk_00032a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_replace_bulk_00034a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - }, - "test_image_policy_replace_bulk_00035a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_replace_bulk_00036a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - }, - "test_image_policy_update_bulk_00030a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_update_bulk_00031a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_update_bulk_00032a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_update_bulk_00034a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - }, - "test_image_policy_update_bulk_00035a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - }, - "NR3F": { - "agnostic": false, - "epldImgName": "n9000-epld.10.3.1.F.img", - "imageName": "nxos64-cs.10.3.1.F.bin", - "nxosVersion": "10.3.1_nxos64-cs_64bit", - "packageName": "", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "NR3F", - "policyName": "NR3F", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": null - } - }, - "test_image_policy_update_bulk_00036a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 0, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - }, - "test_image_policy_update_bulk_00050a": { - "KR5M": { - "agnostic": false, - "epldImgName": "n9000-epld.10.2.5.M.img", - "imageName": "nxos64-cs.10.2.5.M.bin", - "nxosVersion": "10.2.5_nxos64-cs_64bit", - "packageName": "mtx-openconfig-all-2.0.0.0-10.4.1.src.rpm", - "platform": "N9K/N3K", - "platformPolicies": "", - "policyDescr": "KR5M", - "policyName": "KR5M", - "policyType": "PLATFORM", - "ref_count": 2, - "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" - } - } -} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json index bf0ce63f6..2b27caa71 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicies.json @@ -3,6 +3,84 @@ "Mocked responses for endpoint EpPolicies class used in the following unit tests.", "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py" ], + "test_image_policy_create_00030a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_create_00031a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, "test_image_policy_create_00035a": { "TEST_NOTES": [ "No image policies exist on the controller." @@ -31,6 +109,150 @@ "message": "" } }, + "test_image_policy_create_bulk_00030a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_create_bulk_00031a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, + "test_image_policy_create_bulk_00032a": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies", + "MESSAGE": "OK", + "DATA": { + "status": "SUCCESS", + "lastOperDataObject": [ + { + "policyName": "KR5M", + "policyType": "PLATFORM", + "nxosVersion": "10.2.5_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "KR5M", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.2.5.M.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.2.5.M.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + }, + { + "policyName": "NR3F", + "policyType": "PLATFORM", + "nxosVersion": "10.3.1_nxos64-cs_64bit", + "packageName": "", + "platform": "N9K/N3K", + "policyDescr": "NR3F", + "platformPolicies": "", + "epldImgName": "n9000-epld.10.3.1.F.img", + "rpmimages": "", + "imageName": "nxos64-cs.10.3.1.F.bin", + "agnostic": "false", + "role": "", + "fabricPolicyName": "", + "unInstall": "false", + "ref_count": 0, + "imagePresent": "Present" + } + ], + "message": "" + } + }, "test_image_policy_create_bulk_00035a": { "TEST_NOTES": [ "No image policies exist on the controller." diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json index 68b96f517..bd626fcc2 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json +++ b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_EpPolicyCreate.json @@ -3,6 +3,13 @@ "Mocked responses for EpPolicyCreate endpoint.", "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py" ], + "test_image_policy_create_00031a": { + "DATA": "Policy created successfully.", + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", + "RETURN_CODE": 200 + }, "test_image_policy_create_00035a": { "DATA": "Policy created successfully.", "MESSAGE": "OK", @@ -17,6 +24,20 @@ "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", "RETURN_CODE": 500 }, + "test_image_policy_create_bulk_00031a": { + "DATA": "Policy created successfully.", + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", + "RETURN_CODE": 200 + }, + "test_image_policy_create_bulk_00032a": { + "DATA": "Policy created successfully.", + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", + "RETURN_CODE": 200 + }, "test_image_policy_create_bulk_00035a": { "DATA": "Policy created successfully.", "MESSAGE": "OK", diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_ImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_ImagePolicies.json deleted file mode 100644 index 4f9163b42..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/responses_ImagePolicies.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "TEST_NOTES": [ - "Mocked responses for ImagePolicies class used in the following unit tests.", - "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_query.py" - ], - "test_image_policy_query_00030a": { - "MESSAGE": "OK", - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policy", - "RETURN_CODE": 200 - }, - "test_image_policy_query_00031a": { - "MESSAGE": "OK", - "METHOD": "GET", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policy", - "RETURN_CODE": 200 - }, - "test_image_policy_query_00032a": { - "MESSAGE": "OK", - "METHOD": "DELETE", - "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policy", - "RETURN_CODE": 200 - } -} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/results_ImagePolicies.json b/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/results_ImagePolicies.json deleted file mode 100644 index 6e80b0648..000000000 --- a/tests/unit/modules/dcnm/dcnm_image_policy/fixtures/results_ImagePolicies.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - - "TEST_NOTES": [ - "Mocked results used by tests/unit/dcnm_image_policy/utils.py MockImagePolicies.", - "These are used in the following unit tests.", - "tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_query.py" - ], - "test_image_policy_query_00031a": { - "changed": true, - "success": true - }, - "test_image_policy_query_00032a": { - "changed": true, - "success": true - }, - "test_image_policy_query_00038a": { - "changed": false, - "success": false - } -} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create.py index 8526d5337..5be2e4ba6 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create.py @@ -43,10 +43,10 @@ from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - MockAnsibleModule, MockImagePolicies, does_not_raise, - image_policy_create_fixture, params, payloads_image_policy_create, - responses_ep_policies, responses_ep_policy_create, - responses_image_policy_create, rest_send_result_current) + MockAnsibleModule, does_not_raise, image_policy_create_fixture, params, + payloads_image_policy_create, responses_ep_policies, + responses_ep_policy_create, responses_image_policy_create, + rest_send_result_current) def test_image_policy_create_00000(image_policy_create) -> None: @@ -172,7 +172,7 @@ def test_image_policy_create_00022(image_policy_create, key, match) -> None: assert instance.payload is None -def test_image_policy_create_00030(monkeypatch, image_policy_create) -> None: +def test_image_policy_create_00030(image_policy_create) -> None: """ ### Classes and Methods - ImagePolicyCreateCommon @@ -187,29 +187,49 @@ def test_image_policy_create_00030(monkeypatch, image_policy_create) -> None: image policy that already exists on the controller. ### Setup - - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), - is mocked to indicate that two image policies (KR5M, NR3F) exist on the - controller. + - EpPolicies endpoint response is mocked to indicate that two image + policies (KR5M, NR3F) exist on the controller. - ImagePolicyCreate().payload is set to contain one payload (KR5M) - that is present in all_policies. + that is present on the controller. ### Test - payloads_to_commit will an empty list because the payload in instance.payload exists on the controller. + - ``commit`` returns without sending a request to create the image + policy. + - Exceptions are not raised. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" + def responses(): + yield responses_ep_policies(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_create(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + with does_not_raise(): instance = image_policy_create instance.results = Results() - instance.payload = payloads_image_policy_create(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance.build_payloads_to_commit() + instance.rest_send = rest_send + instance.params = params + instance.payload = gen_payloads.next + instance.commit() assert instance._payloads_to_commit == [] -def test_image_policy_create_00031(monkeypatch, image_policy_create) -> None: +def test_image_policy_create_00031(image_policy_create) -> None: """ ### Classes and Methods - ImagePolicyCreateCommon @@ -225,25 +245,45 @@ def test_image_policy_create_00031(monkeypatch, image_policy_create) -> None: that does not exist on the controller. ### Setup - - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), - is mocked to indicate that two image policies (KR5M, NR3F) exist on the - controller. + - EpPolicies endpoint response is mocked to indicate that one image + policy (KR5M) exists on the controller. - ImagePolicyCreate().payload is set to contain one payload containing - an image policy (FOO) that is not present in all_policies. + an image policy (FOO) that is not present on the controller. ### Test - - _payloads_to_commit will equal list(instance.payload) since none of the - image policies in instance.payloads exist on the controller. + - _payloads_to_commit will equal list(instance.payload) since none of + the image policies in instance.payloads exist on the controller. + - Exceptions are not raised. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_create(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_create(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + with does_not_raise(): instance = image_policy_create instance.results = Results() - instance.payload = payloads_image_policy_create(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance.build_payloads_to_commit() + instance.rest_send = rest_send + instance.params = params + instance.payload = gen_payloads.next + instance.commit() + assert len(instance._payloads_to_commit) == 1 assert instance._payloads_to_commit == [payloads_image_policy_create(key)] @@ -273,47 +313,6 @@ def test_image_policy_create_00033(image_policy_create) -> None: instance.commit() -def test_image_policy_create_00034(monkeypatch, image_policy_create) -> None: - """ - ### Classes and Methods - - ImagePolicyCreateCommon - - __init__() - - build_payloads_to_commit() - - ImagePolicyCreate - - __init__() - - payload setter - - commit() - - ### Summary - Verify that ImagePolicyCreate.commit() works as expected when the image policy - already exists on the controller. This is similar to test_image_policy_create_00030 - but tests that the commit method returns when _payloads_to_commit is empty. - - ### Setup - - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), - is mocked to indicate that two image policies (KR5M, NR3F) exist on the - controller. - - ImagePolicyCreate().payload is set to contain one payload (KR5M) - that is present in all_policies. - - ### Test - - payloads_to_commit will an empty list because all payloads in - instance.payloads exist on the controller. - - commit will return without calling send_payloads - - Exceptions are not raised. - """ - method_name = inspect.stack()[0][3] - key = f"{method_name}a" - - with does_not_raise(): - instance = image_policy_create - instance.results = Results() - instance.payload = payloads_image_policy_create(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance.commit() - assert instance._payloads_to_commit == [] - - def test_image_policy_create_00035(image_policy_create) -> None: """ ### Classes and Methods @@ -371,8 +370,6 @@ def payloads(): instance.results = Results() instance.rest_send = rest_send instance.params = params - - with does_not_raise(): instance.payload = gen_payloads.next instance.commit() diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py index 7b055c0ba..bcf5191d9 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py @@ -29,6 +29,7 @@ __copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." __author__ = "Allen Robel" +import copy import inspect import pytest @@ -43,9 +44,8 @@ from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - MockAnsibleModule, MockImagePolicies, does_not_raise, - image_policy_create_bulk_fixture, params, - payloads_image_policy_create_bulk, responses_ep_policies, + MockAnsibleModule, does_not_raise, image_policy_create_bulk_fixture, + params, payloads_image_policy_create_bulk, responses_ep_policies, responses_ep_policy_create, rest_send_result_current) @@ -173,7 +173,7 @@ def test_image_policy_create_bulk_00022(image_policy_create_bulk, key, match) -> assert instance.payloads is None -def test_image_policy_create_bulk_00030(monkeypatch, image_policy_create_bulk) -> None: +def test_image_policy_create_bulk_00030(image_policy_create_bulk) -> None: """ ### Classes and Methods - ImagePolicyCreateCommon @@ -188,30 +188,52 @@ def test_image_policy_create_bulk_00030(monkeypatch, image_policy_create_bulk) - image policy that already exists on the controller. ### Setup - - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), - is mocked to indicate that two image policies (KR5M, NR3F) exist on the - controller. - - ImagePolicyCreateCommon().payloads is set to contain one payload (KR5M) - that is present in all_policies. + - EpPolicies endpoint response is mocked to indicate that two image + policies (KR5M, NR3F) exist on the controller. + - ``payloads`` is set to contain one payload (KR5M) that is present + on the controller. ### Test - payloads_to_commit will an empty list because all payloads in instance.payloads exist on the controller. + - ``commit`` returns without sending any image policy create requests + to the controller. + - Exceptions are not raised. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" - instance = image_policy_create_bulk - instance.results = Results() - instance.payloads = payloads_image_policy_create_bulk(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance.build_payloads_to_commit() + def responses(): + yield responses_ep_policies(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_create_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = image_policy_create_bulk + instance.results = Results() + instance.rest_send = rest_send + instance.params = params + instance.payloads = gen_payloads.next + instance.commit() + assert instance._payloads_to_commit == [] assert len(instance.results.failed) == 0 assert len(instance.results.changed) == 0 -def test_image_policy_create_bulk_00031(monkeypatch, image_policy_create_bulk) -> None: +def test_image_policy_create_bulk_00031(image_policy_create_bulk) -> None: """ ### Classes and Methods - ImagePolicyCreateCommon @@ -227,30 +249,65 @@ def test_image_policy_create_bulk_00031(monkeypatch, image_policy_create_bulk) - that does not exist on the controller. ### Setup - - ImagePolicies().all_policies, called from instance.build_payloads_to_commit(), - is mocked to indicate that two image policies (KR5M, NR3F) exist on the - controller. - - ImagePolicyCreateCommon().payloads is set to contain one payload containing - an image policy (FOO) that is not present in all_policies. + - EpPolicies endpoint response is mocked to indicate that two image + policies (KR5M, NR3F) exist on the controller. + - ``payloads`` is set to contain one payload containing + an image policy (FOO) that is not present on the controller. ### Test - _payloads_to_commit will equal instance.payloads since none of the image policies in instance.payloads exist on the controller. + - ``commit`` sends a request to the controller to create the image + policy. + - Exceptions are not raised. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_create(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_create_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + with does_not_raise(): instance = image_policy_create_bulk instance.results = Results() - instance.payloads = payloads_image_policy_create_bulk(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance.build_payloads_to_commit() + instance.rest_send = rest_send + instance.params = params + instance.payloads = gen_payloads.next + instance.commit() + + compare_diff = copy.deepcopy(payloads_image_policy_create_bulk(key)[0]) + compare_diff["sequence_number"] = 1 + assert len(instance._payloads_to_commit) == 1 assert instance._payloads_to_commit == payloads_image_policy_create_bulk(key) + assert instance.results.action == "create" + assert instance.results.diff_current == compare_diff + assert False in instance.results.failed + assert True not in instance.results.failed + assert False not in instance.results.changed + assert True in instance.results.changed + assert len(instance.results.metadata) == 1 + assert instance.results.metadata[0]["action"] == "create" + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[0]["sequence_number"] == 1 -def test_image_policy_create_bulk_00032(monkeypatch, image_policy_create_bulk) -> None: +def test_image_policy_create_bulk_00032(image_policy_create_bulk) -> None: """ ### Classes and Methods - ImagePolicyCreateCommon @@ -265,13 +322,11 @@ def test_image_policy_create_bulk_00032(monkeypatch, image_policy_create_bulk) - on the controller. ### Setup - - ImagePolicies().all_policies, called from - instance.build_payloads_to_commit(), is mocked to indicate that two + - EpPolicies endpoint response is mocked to indicate that two image policies (KR5M, NR3F) exist on the controller. - - ImagePolicyCreateCommon().payloads is set to contain one payload - containing an image policy (FOO) that is not present in all_policies - and one payload containing an image policy (KR5M) that does exist on - the controller. + - ``payloads`` is set to contain one payload containing an image policy + (FOO) that does not exist on the controller and one payload containing + an image policy (KR5M) that does exist on the controller. ### Test - _payloads_to_commit will contain one payload @@ -281,12 +336,47 @@ def test_image_policy_create_bulk_00032(monkeypatch, image_policy_create_bulk) - method_name = inspect.stack()[0][3] key = f"{method_name}a" - instance = image_policy_create_bulk - instance.payloads = payloads_image_policy_create_bulk(key) - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - instance.build_payloads_to_commit() + def responses(): + yield responses_ep_policies(key) + yield responses_ep_policy_create(key) + + gen_responses = ResponseGenerator(responses()) + + def payloads(): + yield payloads_image_policy_create_bulk(key) + + gen_payloads = ResponseGenerator(payloads()) + + sender = Sender() + sender.ansible_module = MockAnsibleModule() + sender.gen = gen_responses + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + with does_not_raise(): + instance = image_policy_create_bulk + instance.results = Results() + instance.rest_send = rest_send + instance.params = params + instance.payloads = gen_payloads.next + instance.commit() + + compare_diff = copy.deepcopy(payloads_image_policy_create_bulk(key)[0]) + compare_diff["sequence_number"] = 1 + assert len(instance._payloads_to_commit) == 1 assert instance._payloads_to_commit[0]["policyName"] == "FOO" + assert instance.results.action == "create" + assert instance.results.diff_current == compare_diff + assert False in instance.results.failed + assert True not in instance.results.failed + assert False not in instance.results.changed + assert True in instance.results.changed + assert len(instance.results.metadata) == 1 + assert instance.results.metadata[0]["action"] == "create" + assert instance.results.metadata[0]["state"] == "merged" + assert instance.results.metadata[0]["sequence_number"] == 1 def test_image_policy_create_bulk_00033(image_policy_create_bulk) -> None: @@ -316,40 +406,6 @@ def test_image_policy_create_bulk_00033(image_policy_create_bulk) -> None: instance.commit() -def test_image_policy_create_bulk_00034(monkeypatch, image_policy_create_bulk) -> None: - """ - ### Classes and Methods - - ImagePolicyCreateCommon - - payloads setter - - ImagePolicyCreateBulk - - commit() - - ### Summary - Verify that ImagePolicyCreateBulk.commit() returns without doing anything - if payloads is an empty list. - - ### Setup - - ImagePolicyCreateCommon().payloads is set to an empty list - - ### Test - - ImagePolicyCreateBulk().results.changed is empty. - """ - method_name = inspect.stack()[0][3] - key = f"{method_name}a" - - with does_not_raise(): - instance = image_policy_create_bulk - instance.payloads = [] - - monkeypatch.setattr(instance, "_image_policies", MockImagePolicies(key)) - - with does_not_raise(): - instance.rest_send = RestSend(params) - instance.results = Results() - instance.commit() - assert len(instance.results.changed) == 0 - - def test_image_policy_create_bulk_00035(image_policy_create_bulk) -> None: """ ### Classes and Methods @@ -365,7 +421,7 @@ def test_image_policy_create_bulk_00035(image_policy_create_bulk) -> None: to an image create request with a 200 response. ### Setup responses - - EpPolicies endpoint response contains DATA indicating no image policies + - EpPolicies endpoint response is mocked to indicate no image policies exist on the controller. - ImagePolicyCreateCommon().payloads is set to contain one payload that contains an image policy (FOO) which does not exist on the controller. diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py index 2a96109bb..03743d685 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/utils.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/utils.py @@ -156,88 +156,6 @@ def public_method_for_pylint(self) -> Any: """ -class MockImagePolicies: - """ - Mock the ImagePolicies class to return various values for all_policies - """ - - def __init__(self, key: str) -> None: - self.key = key - self.properties = {} - self.properties["policy_name"] = None - self.properties["results"] = None - - def refresh(self) -> None: - """ - bypass dcnm_send - """ - - @property - def all_policies(self): - """ - Mock the return value of all_policies - all_policies contains all image policies that exist on the controller - """ - return image_policies_all_policies(self.key) - - @property - def name(self): - """ - Return the name of the policy matching self.policy_name, - if it exists. - Return None otherwise - """ - try: - return ( - image_policies_all_policies(self.key) - .get(self.policy_name, None) - .get("policyName") - ) - except AttributeError: - return None - - @property - def policy_name(self): - """ - Set the name of the policy to query. - - This must be set prior to accessing any other properties - """ - return self.properties.get("policy_name") - - @policy_name.setter - def policy_name(self, value): - self.properties["policy_name"] = value - - @property - def ref_count(self): - """ - Return the reference count of the policy matching self.policy_name, - if it exists. The reference count is the number of switches using - this policy. - Return None otherwise - """ - try: - return ( - image_policies_all_policies(self.key) - .get(self.policy_name, None) - .get("ref_count") - ) - except AttributeError: - return None - - @property - def results(self): - """ - An instance of the Results class. - """ - return self.properties["results"] - - @results.setter - def results(self, value): - self.properties["results"] = value - - # See the following for explanation of why fixtures are explicitely named # https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html @@ -487,17 +405,6 @@ def responses_ep_policy_edit(key: str) -> Dict[str, str]: return data -def responses_image_policies(key: str) -> Dict[str, str]: - """ - Return responses for ImagePolicies - Used in MockImagePolicies - """ - data_file = "responses_ImagePolicies" - data = load_fixture(data_file).get(key) - print(f"{data_file}: {key} : {data}") - return data - - def responses_image_policy_create(key: str) -> Dict[str, str]: """ Return responses for ImagePolicyCreate @@ -558,17 +465,6 @@ def responses_image_policy_update_bulk(key: str) -> Dict[str, str]: return data -def results_image_policies(key: str) -> Dict[str, str]: - """ - Return results for ImagePolicies - Used in MockImagePolicies - """ - data_file = "results_ImagePolicies" - data = load_fixture(data_file).get(key) - print(f"{data_file}: {key} : {data}") - return data - - def results_image_policy_create_bulk(key: str) -> Dict[str, str]: """ Return results for ImagePolicyCreateBulk From d668ea61e06173b6b3232d836b9d68f6eaaa48b1 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 1 Jul 2024 13:07:11 -1000 Subject: [PATCH 223/230] Use private addressing. --- playbooks/roles/dcnm_image_policy/dcnm_tests.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/playbooks/roles/dcnm_image_policy/dcnm_tests.yaml b/playbooks/roles/dcnm_image_policy/dcnm_tests.yaml index 60f275830..87d4937d3 100644 --- a/playbooks/roles/dcnm_image_policy/dcnm_tests.yaml +++ b/playbooks/roles/dcnm_image_policy/dcnm_tests.yaml @@ -19,12 +19,12 @@ # testcase: dcnm_image_policy_replaced switch_username: admin switch_password: "foobar" - spine1: 172.22.150.114 - spine2: 172.22.150.115 - leaf1: 172.22.150.103 - leaf2: 172.22.150.104 - leaf3: 172.22.150.108 - leaf4: 172.22.150.109 + spine1: 192.168.1.2 + spine2: 192.168.1.3 + leaf1: 192.168.1.4 + leaf2: 192.168.1.5 + leaf3: 192.168.1.6 + leaf4: 192.168.1.7 image_policy_1: "KR5M" image_policy_2: "NR3F" epld_image_1: n9000-epld.10.2.5.M.img From 8696b90b726d42f488f46fca9b1541b645b65da1 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Mon, 1 Jul 2024 13:33:29 -1000 Subject: [PATCH 224/230] EpPolicyDelete(): Fix PEP8 too-many-black-lines. --- .../api/v1/imagemanagement/rest/policymgnt/policymgnt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py index 371125ad9..aff504d0f 100644 --- a/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py +++ b/plugins/module_utils/common/api/v1/imagemanagement/rest/policymgnt/policymgnt.py @@ -133,8 +133,6 @@ def verb(self): return "GET" - - class EpPolicyAttach(PolicyMgnt): """ ## V1 API - PolicyMgnt().EpPolicyAttach() @@ -251,6 +249,7 @@ class EpPolicyDelete(PolicyMgnt): } ``` """ + def __init__(self): super().__init__() self.class_name = self.__class__.__name__ From d3ca3c68fd104293d5e77be8f2996edf83d9bee6 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 2 Jul 2024 08:03:42 -1000 Subject: [PATCH 225/230] create.py: update docstrings, more... 1. create.py - ImagePolicyCreate().commit(): raise ValueError in the same cases as ImagePolicyCreateBulk().commit() - ImagePolicyCreateCommit().payloads.setter: catch exceptions raised by verify_payload. - Update docstrings throughout 2. test_image_policy_create_bulk: - Update docstrings throughout --- plugins/module_utils/image_policy/create.py | 122 ++++++++++++++---- .../test_image_policy_create_bulk.py | 76 ++++++----- 2 files changed, 141 insertions(+), 57 deletions(-) diff --git a/plugins/module_utils/image_policy/create.py b/plugins/module_utils/image_policy/create.py index aa38fd117..bf0defdb0 100644 --- a/plugins/module_utils/image_policy/create.py +++ b/plugins/module_utils/image_policy/create.py @@ -37,9 +37,12 @@ @Properties.add_params class ImagePolicyCreateCommon: """ + ### Summary Common methods and properties for: - ImagePolicyCreate - ImagePolicyCreateBulk + + See respective class docstrings for more information. """ def __init__(self): @@ -112,9 +115,9 @@ def build_payloads_to_commit(self): ### Notes Expects self.payloads to be a list of dict, with each dict - being a payload for the image policy create API endpoint. + being a payload for endpoint ``EpPolicyCreate()``. - Populates self._payloads_to_commit with a list of payloads + Populate ``self._payloads_to_commit`` with a list of payloads to commit. """ method_name = inspect.stack()[0][3] @@ -157,7 +160,10 @@ def send_payloads(self): self.rest_send.payload = payload self.rest_send.commit() - msg = f"rest_send.result_current: {json.dumps(self.rest_send.result_current, indent=4, sort_keys=True)}" + msg = "rest_send.result_current: " + msg += ( + f"{json.dumps(self.rest_send.result_current, indent=4, sort_keys=True)}" + ) self.log.debug(msg) if self.rest_send.result_current["success"] is False: @@ -177,33 +183,60 @@ def send_payloads(self): @property def payloads(self): """ - Return the image policy payloads + ### Summary + Return the image policy payloads. - Payloads must be a list of dict. Each dict is a - payload for the image policy create API endpoint. + Payloads must be a list of dict. Each dict is a payload for endpoint + ``EpPolicyCreate()``. + + ### Raises + - ``TypeError`` if: + - ``payloads`` is not a list. + - Any element within ``payloads`` is not a dict. + - Any element within ``payloads`` is missing mandatory keys. """ return self._payloads @payloads.setter def payloads(self, value): method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " if not isinstance(value, list): - msg = f"{self.class_name}.{method_name}: " msg += "payloads must be a list of dict. " msg += f"got {type(value).__name__} for " msg += f"value {value}" raise TypeError(msg) + msg += "Error verifying payload: " for item in value: - self.verify_payload(item) + try: + self.verify_payload(item) + except ValueError as error: + msg += f"{error}" + raise ValueError(msg) from error + except TypeError as error: + msg += f"{error}" + raise TypeError(msg) from error self._payloads = value class ImagePolicyCreateBulk(ImagePolicyCreateCommon): """ + ### Summary Given a properly-constructed list of payloads, bulk-create the image policies therein. The payload format is given below. - Payload format: + ### Raises + - ``ValueError`` if + - ``payloads`` is not set prior to calling ``commit``. + - ``rest_send`` is not set prior to calling ``commit``. + - ``results`` is not set prior to calling ``commit``. + - ``params`` is not set prior to calling ``commit``. + - ``TypeError`` if + - ``payloads`` is not a list. + - ``payload`` is not a dict. + + ### Payload format + ``` agnostic bool(), optional. true or false epldImgName str(), optional. name of an EPLD image to install. nxosVersion str(), required. NX-OS version as version_type_arch @@ -213,9 +246,10 @@ class ImagePolicyCreateBulk(ImagePolicyCreateCommon): policyName: str(), required. Name of the image policy. policyType str(), required. PLATFORM or UMBRELLA rpmimages: str(), optional. A comma-separated list of packages to uninstall + ``` - Example list of payloads: - + ### Example list of payloads: + ```json [ { "agnostic": False, @@ -234,6 +268,7 @@ class ImagePolicyCreateBulk(ImagePolicyCreateCommon): "policyType": "PLATFORM" } ] + ``` """ def __init__(self): @@ -247,8 +282,15 @@ def __init__(self): def commit(self): """ - create policies. Skip any policies that already exist - on the controller, + ### Summary + Create policies. Skip policies that exist on the controller. + + ### Raises + - ``ValueError`` if: + - ``params`` is not set prior to calling ``commit``. + - ``payloads`` is not set prior to calling ``commit``. + - ``rest_send`` is not set prior to calling ``commit``. + - ``results`` is not set prior to calling ``commit``. """ method_name = inspect.stack()[0][3] @@ -280,12 +322,23 @@ def commit(self): class ImagePolicyCreate(ImagePolicyCreateCommon): """ - NOTE: This class is not being used currently. - - Given a properly-constructed image policy payload (python dict), - send an image policy create request to the controller. The payload - format is given below. - + ### Summary + This class is not used by dcnm_image_policy. + + Given an image policy payload, send an image policy create request + to controller endpoint ``EpPolicyCreate()``. + + ### Raises + - ``ValueError`` if: + - ``payload`` is not set prior to calling ``commit``. + - ``rest_send`` is not set prior to calling ``commit``. + - ``results`` is not set prior to calling ``commit``. + - ``params`` is not set prior to calling ``commit``. + - ``TypeError`` if: + - ``payload`` is not a dict. + + ### Payload format + ``` agnostic bool(), optional. true or false epldImgName str(), optional. name of an EPLD image to install. nxosVersion str(), required. NX-OS version as version_type_arch @@ -295,9 +348,10 @@ class ImagePolicyCreate(ImagePolicyCreateCommon): policyName: str(), required. Name of the image policy. policyType str(), required. PLATFORM or UMBRELLA rpmimages: str(), optional. A comma-separated list of packages to uninstall + ``` - Example: - + ### Example payload + ```json { "agnostic": false, "epldImgName": "n9000-epld.10.3.2.F.img", @@ -309,7 +363,7 @@ class ImagePolicyCreate(ImagePolicyCreateCommon): "policyType": "PLATFORM", "rpmimages": "mtx-grpctunnel-2.1.0.0-10.4.1.lib32_64_n9000" } - + ``` """ def __init__(self): @@ -328,6 +382,9 @@ def payload(self): """ ### Summary An image policy payload. See class docstring for the payload structure. + + ### Raises + - ``TypeError`` if payload is not a dict. """ return self._payload @@ -343,14 +400,33 @@ def commit(self): Create policy. If policy already exists on the controller, do nothing. ### Raises - - ``ValueError`` if payload is not set prior to calling commit(). + - ``ValueError`` if: + - ``params`` is not set prior to calling ``commit``. + - ``payload`` is not set prior to calling ``commit``. + - ``rest_send`` is not set prior to calling ``commit``. + - ``results`` is not set prior to calling ``commit``. """ method_name = inspect.stack()[0][3] + if self.params is None: # pylint: disable=no-member + msg = f"{self.class_name}.{method_name}: " + msg += "params must be set prior to calling commit." + raise ValueError(msg) + if self.payload is None: msg = f"{self.class_name}.{method_name}: " msg += "payload must be set prior to calling commit." raise ValueError(msg) + if self.rest_send is None: # pylint: disable=no-member + msg = f"{self.class_name}.{method_name}: " + msg += "rest_send must be set prior to calling commit." + raise ValueError(msg) + + if self.results is None: # pylint: disable=no-member + msg = f"{self.class_name}.{method_name}: " + msg += "results must be set prior to calling commit." + raise ValueError(msg) + self.build_payloads_to_commit() if len(self._payloads_to_commit) == 0: diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py index bcf5191d9..028148eab 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_create_bulk.py @@ -86,7 +86,7 @@ def test_image_policy_create_bulk_00010(image_policy_create_bulk) -> None: ### Classes and Methods - ImagePolicyCreateCommon - __init__() - - payloads setter + - payloads.setter - ImagePolicyCreateBulk - __init__() @@ -151,7 +151,7 @@ def test_image_policy_create_bulk_00022(image_policy_create_bulk, key, match) -> ### Classes and Methods - ImagePolicyCreateCommon - __init__() - - payloads setter + - payloads.setter - ImagePolicyCreateBulk - __init__() @@ -178,10 +178,11 @@ def test_image_policy_create_bulk_00030(image_policy_create_bulk) -> None: ### Classes and Methods - ImagePolicyCreateCommon - __init__() - - payloads setter + - payloads.setter - build_payloads_to_commit() - ImagePolicyCreateBulk - __init__() + - commit() ### Summary Verify behavior when the user sends an image create payload for an @@ -238,10 +239,11 @@ def test_image_policy_create_bulk_00031(image_policy_create_bulk) -> None: ### Classes and Methods - ImagePolicyCreateCommon - __init__() - - payloads setter + - payloads.setter - build_payloads_to_commit() - ImagePolicyCreateBulk - __init__() + - commit() ### Summary Verify that instance.build_payloads_to_commit() adds a payload to the @@ -311,10 +313,11 @@ def test_image_policy_create_bulk_00032(image_policy_create_bulk) -> None: """ ### Classes and Methods - ImagePolicyCreateCommon - - payloads setter + - payloads.setter - build_payloads_to_commit() - ImagePolicyCreateBulk - __init__() + - commit() ### Summary Verify that instance.build_payloads_to_commit() adds a payload to the @@ -386,14 +389,13 @@ def test_image_policy_create_bulk_00033(image_policy_create_bulk) -> None: - commit() ### Summary - Verify that ImagePolicyCreateBulk.commit() raises ``ValueError`` when - payloads is None. + Verify that ``commit()`` raises ``ValueError`` when payloads is not set. ### Setup - - ImagePolicyCreateCommon().payloads is not set. + - ``payloads`` is not set. ### Test - - ValueError is called because payloads is None. + - ``ValueError`` is raised because payloads is not set. """ with does_not_raise(): results = Results() @@ -417,8 +419,8 @@ def test_image_policy_create_bulk_00035(image_policy_create_bulk) -> None: - commit() ### Summary - Verify ImagePolicyCreateBulk.commit() happy path. Controller responds - to an image create request with a 200 response. + Verify ``commit()`` happy path. Controller returns a 200 response + to an image policy create request. ### Setup responses - EpPolicies endpoint response is mocked to indicate no image policies @@ -503,26 +505,28 @@ def test_image_policy_create_bulk_00036(image_policy_create_bulk) -> None: - commit() ### Summary - Verify ImagePolicyCreateBulk.commit() sad path. Controller returns a 500 - response to an image policy create request. + Verify ``commit()`` sad path. Controller returns a 500 response + to an image policy create request. ### Setup - - EpPolicies endpoint response contains DATA indicating no image policies - exist on the controller. - - ImagePolicyCreateBulk().payloads is set to contain one payload that - contains an image policy (FOO) which does not exist on the controller. - - EpPolicyCreate endpoint response contains a 500 response. + - ``EpPolicies`` endpoint response is mocked to indicate no image + policies exist on the controller. + - ``payloads`` is set to contain one payload that contains an + image policy (FOO) which does not exist on the controller. + - ``EpPolicyCreate`` endpoint response contains a 500 response. ### Test - - A sequence_number key is added to instance.results.response_current - - instance.results.diff_current is set to a dict with only - the key "sequence_number", since no changes were made - - instance.results.failed set() contains True and does not contain False - - instance.results.changed set() contains False and does not contain True - - instance.results.metadata contains one dict - - The value of instance.results.metadata "action" is "create" - - The value of instance.results.metadata "state" is "merged" - - The value of instance.results.metadata "sequence_number" is 1 + - A sequence_number key is added to results.response_current. + - results.diff_current is set to a dict with only the key + ``sequence_number``, since no changes were made. + - results.failed set() contains True. + - results.failed set() does not contain False. + - results.changed set() contains False. + - results.changed set() does not contain True. + - results.metadata contains one dict. + - The value of results.metadata "action" is "create". + - The value of results.metadata "state" is "merged". + - The value of results.metadata "sequence_number" is 1. """ method_name = inspect.stack()[0][3] key = f"{method_name}a" @@ -577,21 +581,25 @@ def test_image_policy_create_bulk_00037(image_policy_create_bulk) -> None: - _process_responses() - ImagePolicyCreateBulk - __init__() + - commit() ### Summary Simulate a succussful response from the controller, followed by a bad response from the controller during policy create. ### Setup - - instance.payloads is set to contain two payloads + - ``payloads`` is set to contain two payloads. ### Test - - Both successful and bad responses are recorded with separate sequence_numbers. - - instance.results.failed will be a set() containing both True and False - - instance.results.changed will be a set() containing both True and False - - instance.results.response contains two responses - - instance.results.result contains two results - - instance.results.diff contains two diffs + - Both successful and bad responses are recorded with separate + sequence_numbers. + - results.failed set() contains True. + - results.failed set() contains False. + - results.changed set() contains True. + - results.changed set() contains False. + - results.response contains two responses. + - results.result contains two results. + - results.diff contains two diffs. """ key_policies = "test_image_policy_create_bulk_00037a" From 047181d8b5fa7da01cdf215c9e24b8fc3060526c Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 2 Jul 2024 08:33:44 -1000 Subject: [PATCH 226/230] delete.py: update docstrings, more... 1. delete.py - ImagePolicyDelete().commit(): Add period to end of error message. - ImagePolicyDelete().commit(): catch and re-raise ValueError from _validate_commit_parameters() - ImagePolicyDelete()._get_policies_to_delete(): catch and re-raise ValueError from _verify_image_policy_ref_count() - Update docstrings 2. test_image_policy_delete.py - Change assert to add period at end of error message. --- plugins/module_utils/image_policy/delete.py | 57 +++++++++++++++---- .../test_image_policy_delete.py | 8 +-- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/plugins/module_utils/image_policy/delete.py b/plugins/module_utils/image_policy/delete.py index dadbe7c11..000bfa213 100644 --- a/plugins/module_utils/image_policy/delete.py +++ b/plugins/module_utils/image_policy/delete.py @@ -125,12 +125,25 @@ def _verify_image_policy_ref_count(self, instance, policy_names): def _get_policies_to_delete(self) -> None: """ + ### Summary Retrieve policies from the controller and return the list of controller policies that are in our policy_names list. + + ### Raises + - ``ValueError`` if any policy in policy_names has a ref_count + greater than 0 (i.e. devices are using the policy). """ - self._image_policies.rest_send = self.rest_send # pylint: disable=no-member + method_name = inspect.stack()[0][3] + # pylint: disable=no-member + self._image_policies.rest_send = self.rest_send + # pylint: enable=no-member self._image_policies.refresh() - self._verify_image_policy_ref_count(self._image_policies, self.policy_names) + try: + self._verify_image_policy_ref_count(self._image_policies, self.policy_names) + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) from error self._policies_to_delete = [] for policy_name in self.policy_names: @@ -143,7 +156,15 @@ def _get_policies_to_delete(self) -> None: # pylint: disable=no-member def _validate_commit_parameters(self): """ - validate the parameters for commit + ### Summary + Validate the parameters for commit. + + ### Raises + - ``ValueError`` if: + - ``params`` is not set prior to calling commit. + - ``policy_names`` is not set prior to calling commit. + - ``rest_send`` is not set prior to calling commit. + - ``results`` is not set prior to calling commit. """ method_name = inspect.stack()[0][3] if self.params is None: @@ -168,10 +189,23 @@ def _validate_commit_parameters(self): def commit(self): """ - delete each of the image policies in self.policy_names + ### Summary + delete each of the image policies in self.policy_names. + + ### Raises + - ``ValueError`` if: + - ``params`` is not set. + - ``policy_names`` is not set. + - ``rest_send`` is not set. + - ``results`` is not set. """ method_name = inspect.stack()[0][3] - self._validate_commit_parameters() + try: + self._validate_commit_parameters() + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += f"{error}" + raise ValueError(msg) from error self.check_mode = self.params.get("check_mode") self.state = self.params.get("state") @@ -185,32 +219,33 @@ def commit(self): if len(self._policies_to_delete) != 0: self._send_requests() else: + msg = "No image policies to delete." + self.log.debug(msg) self.results.action = self.action self.results.check_mode = self.check_mode self.results.state = self.state self.results.diff_current = {} self.results.result_current = {"success": True, "changed": False} - msg = "No image policies to delete" self.results.changed = False self.results.failed = False self.results.response_current = {"RETURN_CODE": 200, "MESSAGE": msg} - self.log.debug(msg) self.results.register_task_result() def _send_requests(self): """ ### Summary - - If check_mode is False, send the requests to the controller - - If check_mode is True, do not send the requests to the controller - - In both cases, populate the following lists: + - If check_mode is False, send the requests to the controller. + - If check_mode is True, do not send the requests to the controller. + - In both cases, populate the following lists. + ```text - self.response_ok : list of controller responses associated with success result - self.result_ok : list of results where success is True - self.diff_ok : list of payloads for which the request succeeded - self.response_nok : list of controller responses associated with failed result - self.result_nok : list of results where success is False - self.diff_nok : list of payloads for which the request failed + ``` """ method_name = inspect.stack()[0][3] self.rest_send.save_settings() diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py index 1051f98e4..58c8b08f3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_delete.py @@ -43,9 +43,9 @@ from ansible_collections.cisco.dcnm.tests.unit.module_utils.common.common_utils import \ ResponseGenerator from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_image_policy.utils import ( - MockAnsibleModule, does_not_raise, - image_policy_delete_fixture, params, responses_ep_policies, - responses_ep_policy_delete, results_image_policy_delete) + MockAnsibleModule, does_not_raise, image_policy_delete_fixture, params, + responses_ep_policies, responses_ep_policy_delete, + results_image_policy_delete) def test_image_policy_delete_00000(image_policy_delete) -> None: @@ -212,7 +212,7 @@ def responses(): assert instance.results.metadata[0]["state"] == "deleted" assert instance.results.response[0]["RETURN_CODE"] == 200 - assert instance.results.response[0]["MESSAGE"] == "No image policies to delete" + assert instance.results.response[0]["MESSAGE"] == "No image policies to delete." assert instance.results.response[0]["sequence_number"] == 1 assert instance.results.result[0]["changed"] is False From 71f668588e460c079bf3fddfbe2cf9185a5ac98f Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 2 Jul 2024 09:07:44 -1000 Subject: [PATCH 227/230] image_policies.py: update docstrings. --- .../image_policy/image_policies.py | 110 +++++++++++------- 1 file changed, 65 insertions(+), 45 deletions(-) diff --git a/plugins/module_utils/image_policy/image_policies.py b/plugins/module_utils/image_policy/image_policies.py index bae0f7e17..3d803166f 100644 --- a/plugins/module_utils/image_policy/image_policies.py +++ b/plugins/module_utils/image_policy/image_policies.py @@ -35,6 +35,7 @@ @Properties.add_results class ImagePolicies: """ + ### Summary Retrieve image policy details from the controller and provide property accessors for the policy attributes. @@ -58,12 +59,13 @@ class ImagePolicies: policy_name = instance.name platform = instance.platform epd_image_name = instance.epld_image_name + ``` etc... - Policies can be refreshed by calling instance.refresh(). + Policies can be refreshed by calling ``instance.refresh()``. - Endpoint: - /appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies + ### Endpoint: + ``/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policies`` """ def __init__(self): @@ -195,7 +197,8 @@ def _get(self, item): @property def all_policies(self) -> dict: """ - Return dict containing all policies, keyed on policy_name + ### Summary + Return dict containing all policies, keyed on policy_name. """ if self._all_policies is None: return {} @@ -204,33 +207,37 @@ def all_policies(self) -> dict: @property def description(self): """ - Return the policyDescr of the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the ``policyDescr`` of the policy matching ``policy_name``, + if it exists. + - Return None otherwise. """ return self._get("policyDescr") @property def epld_image_name(self): """ - Return the epldImgName of the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the ``epldImgName`` of the policy matching ``policy_name``, + if it exists. + - Return None otherwise. """ return self._get("epldImgName") @property def name(self): """ - Return the name of the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the ``name`` of the policy matching ``policy_name``, + if it exists. + - Return None otherwise. """ return self._get("policyName") @property def policy_name(self): """ + ### Summary Set the name of the policy to query. This must be set prior to accessing any other properties @@ -244,25 +251,30 @@ def policy_name(self, value): @property def policy(self): """ - Return the policy data of the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the policy data of the policy matching ``policy_name``, + if it exists. + - Return None otherwise. """ return self._get("policy") @property def policy_type(self): """ - Return the policyType of the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the ``policyType`` of the policy matching ``policy_name``, + if it exists. + - Return None otherwise. """ return self._get("policyType") @property def response_data(self) -> dict: """ - Return dict containing the DATA portion of a controller response, keyed on policy_name + ### Summary + - Return dict containing the DATA portion of a controller response, + keyed on ``policy_name``. + - Return an empty dict otherwise. """ if self._response_data is None: return {} @@ -271,72 +283,80 @@ def response_data(self) -> dict: @property def nxos_version(self): """ - Return the nxosVersion of the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the ``nxosVersion`` of the policy matching ``policy_name``, + if it exists. + - Return None otherwise. """ return self._get("nxosVersion") @property def package_name(self): """ - Return the packageName of the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the ``packageName`` of the policy matching ``policy_name``, + if it exists. + - Return None otherwise. """ return self._get("packageName") @property def platform(self): """ - Return the platform of the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the ``platform`` of the policy matching ``policy_name``, + if it exists. + - Return None otherwise. """ return self._get("platform") @property def platform_policies(self): """ - Return the platformPolicies of the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the ``platformPolicies`` of the policy matching + ``policy_name``, if it exists. + - Return None otherwise. """ return self._get("platformPolicies") @property def ref_count(self): """ - Return the reference count of the policy matching self.policy_name, - if it exists. The reference count is the number of switches using - this policy. - Return None otherwise + ### Summary + - Return the reference count of the policy matching ``policy_name``, + if it exists. The reference count indicates the number of + switches using this policy. + - Return None otherwise. """ return self._get("ref_count") @property def rpm_images(self): """ - Return the rpmimages of the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the ``rpmimages`` of the policy matching ``policy_name``, + if it exists. + - Return None otherwise. """ return self._get("rpmimages") @property def image_name(self): """ - Return the imageName of the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the ``imageName`` of the policy matching ``policy_name``, + if it exists. + - Return None otherwise. """ return self._get("imageName") @property def agnostic(self): """ - Return the value of agnostic for the policy matching self.policy_name, - if it exists. - Return None otherwise + ### Summary + - Return the value of agnostic for the policy matching + ``policy_name``, if it exists. + - Return None otherwise. """ return self._get("agnostic") From 758ea825832def926a25171a1b013cba5b214a1d Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 2 Jul 2024 09:27:04 -1000 Subject: [PATCH 228/230] Update docstrings. 1. Update docstrings. - params_spec.py - module_utils/common/properties.py --- plugins/module_utils/common/properties.py | 36 +++++++++++- .../module_utils/image_policy/params_spec.py | 58 +++++++++++++------ 2 files changed, 73 insertions(+), 21 deletions(-) diff --git a/plugins/module_utils/common/properties.py b/plugins/module_utils/common/properties.py index 5798aa201..65e52392d 100644 --- a/plugins/module_utils/common/properties.py +++ b/plugins/module_utils/common/properties.py @@ -41,9 +41,39 @@ class Properties: def params(self): """ ### Summary - A dictionary containing the following parameters: - - ``state``: The state of the module. - - ``check_mode``: A boolean indicating whether the module is in check mode. + Expects value to be a dictionary containing, at mimimum, the keys + ``state`` and ``check_mode``. + + ### Raises + - setter: ``ValueError`` if value is not a dict. + - setter: ``ValueError`` if value["state"] is missing. + - setter: ``ValueError`` if value["state"] is not a valid state. + - setter: ``ValueError`` if value["check_mode"] is missing. + + ### Valid values + + #### ``state`` + - deleted + - merged + - overridden + - query + - replaced + + #### ``check_mode`` + - ``False`` - The Ansible module should make requested changes. + - ``True`` - The Ansible module should not make requested changed + and should only report what changes it would make if ``check_mode`` + was ``False``. + + ### Details + - Example Valid params: + - ``{"state": "deleted", "check_mode": False}`` + - ``{"state": "merged", "check_mode": False}`` + - ``{"state": "overridden", "check_mode": False}`` + - ``{"state": "query", "check_mode": False}`` + - ``{"state": "replaced", "check_mode": False}`` + - getter: return the params + - setter: set the params """ return self._params diff --git a/plugins/module_utils/image_policy/params_spec.py b/plugins/module_utils/image_policy/params_spec.py index e76ac0215..f6db51a05 100644 --- a/plugins/module_utils/image_policy/params_spec.py +++ b/plugins/module_utils/image_policy/params_spec.py @@ -24,7 +24,11 @@ class ParamsSpec: """ + ### Summary Parameter specifications for the dcnm_image_policy module. + + ### Raises + - ``ValueError`` if params is not set before calling ``commit()`` """ def __init__(self): @@ -43,10 +47,11 @@ def __init__(self): def commit(self): """ + ### Summary Build the parameter specification based on the state ## Raises - - ``ValueError`` if params is not set + - ``ValueError`` if ``params`` is not set. """ method_name = inspect.stack()[0][3] @@ -69,11 +74,12 @@ def commit(self): def _build_params_spec_for_merged_state(self) -> None: """ - Build the specs for the parameters expected when state == merged. + ### Summary + Build the specs for the parameters expected when state is + ``merged``. - Caller: _validate_configs() - Return: params_spec, a dictionary containing playbook - parameter specifications. + ### Raises + None """ self._params_spec: dict = {} @@ -126,18 +132,35 @@ def _build_params_spec_for_merged_state(self) -> None: self._params_spec["type"]["type"] = "str" def _build_params_spec_for_overridden_state(self) -> None: + """ + ### Summary + Build the specs for the parameters expected when state is + ``overridden``. + + ### Raises + None + """ self._build_params_spec_for_merged_state() def _build_params_spec_for_replaced_state(self) -> None: + """ + ### Summary + Build the specs for the parameters expected when state is + ``replaced``. + + ### Raises + None + """ self._build_params_spec_for_merged_state() def _build_params_spec_for_deleted_state(self) -> None: """ - Build the specs for the parameters expected when state == deleted. + ### Summary + Build the specs for the parameters expected when state is + ``deleted``. - Caller: _validate_configs() - Return: params_spec, a dictionary containing playbook - parameter specifications. + ### Raises + None """ self._params_spec: dict = {} @@ -147,11 +170,12 @@ def _build_params_spec_for_deleted_state(self) -> None: def _build_params_spec_for_query_state(self) -> None: """ - Build the specs for the parameters expected when state == query. + ### Summary + Build the specs for the parameters expected when state is + ``query``. - Caller: _validate_configs() - Return: params_spec, a dictionary containing playbook - parameter specifications. + ### Raises + None """ self._params_spec: dict = {} @@ -167,7 +191,7 @@ def params(self) -> dict: """ ### Summary Expects value to be a dictionary containing, at mimimum, - the key "state" with value of one of: + the key ``state`` with value of one of: - deleted - merged - overridden @@ -193,9 +217,6 @@ def params(self) -> dict: @params.setter def params(self, value: dict) -> None: - """ - - setter: set the params - """ method_name = inspect.stack()[0][3] if not isinstance(value, dict): msg = f"{self.class_name}.{method_name}.setter: " @@ -220,6 +241,7 @@ def params(self, value: dict) -> None: @property def params_spec(self) -> Dict[str, Any]: """ - return the parameter specification + ### Summary + Return the parameter specification """ return self._params_spec From 8b2f2b0387713b275b547168e15fad2b896bf6a2 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Tue, 2 Jul 2024 12:22:26 -1000 Subject: [PATCH 229/230] Update docstrings, more... 1. payload.py - Add period (.) to end of error messages. - Update docstrings. 2. test_image_policy_payload.py - Update unit tests to account for added period (.) at end of error messages. - Run through linters. --- plugins/module_utils/image_policy/payload.py | 47 ++++++++++++++----- .../test_image_policy_payload.py | 15 +++++- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/plugins/module_utils/image_policy/payload.py b/plugins/module_utils/image_policy/payload.py index f9dced4b0..02fcdf2eb 100644 --- a/plugins/module_utils/image_policy/payload.py +++ b/plugins/module_utils/image_policy/payload.py @@ -25,6 +25,7 @@ class Payload: """ + ### Summary Base class for Config2Payload and Payload2Config """ @@ -42,7 +43,11 @@ def __init__(self): @property def config(self): """ - return the playbook configuration + ### Summary + Return the playbook configuration. + + ### Raises + - ``TypeError`` if config is not a dictionary. """ return self._config @@ -60,7 +65,11 @@ def config(self, value): @property def params(self): """ - return the params dict + ### Summary + Return the params dictionary. + + ### Raises + - ``TypeError`` if params is not a dictionary. """ return self._params @@ -78,7 +87,11 @@ def params(self, value): @property def payload(self): """ - return the payload + ### Summary + Return the payload. + + ### Raises + - ``TypeError`` if payload is not a dictionary. """ return self._payload @@ -102,8 +115,8 @@ class Config2Payload(Payload): ### Raises - ``ValueError`` if: - - self.config is empty - - self.params is is not set prior to calling commit() + - ``config`` is empty. + - ``params`` is is not set prior to calling ``commit``. """ def __init__(self): @@ -115,7 +128,13 @@ def __init__(self): def commit(self): """ - Convert self_payload into a playbook configuration + ### Summary + Convert ``payload`` into a playbook configuration. + + ### Raises + - ``ValueError`` if: + - ``params`` is not set. + - ``config`` is empty. """ method_name = inspect.stack()[0][3] @@ -125,12 +144,13 @@ def commit(self): raise ValueError(msg) msg = f"{self.class_name}.{method_name}: " - msg += f"self.config {json.dumps(self.config, indent=4, sort_keys=True)}" + msg += "config: " + msg += f"{json.dumps(self.config, indent=4, sort_keys=True)}" self.log.debug(msg) if self.config == {}: msg = f"{self.class_name}.{method_name}: " - msg += "config is empty" + msg += "config is empty." raise ValueError(msg) config = copy.deepcopy(self.config) @@ -158,8 +178,11 @@ def commit(self): class Payload2Config(Payload): """ - Convert an image-policy endpoint payload into a playbook - configuration. + ### Summary + Convert an image-policy endpoint payload into a playbook configuration. + + ### Raises + - ``ValueError`` if payload is empty. """ def __init__(self): @@ -175,13 +198,13 @@ def commit(self): build the config from the payload ### Raises - - ``ValueError`` if payload is empty + - ``ValueError`` if payload is empty. """ method_name = inspect.stack()[0][3] if self.payload == {}: msg = f"{self.class_name}.{method_name}: " - msg += "payload is empty" + msg += "payload is empty." raise ValueError(msg) payload = copy.deepcopy(self.payload) diff --git a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py index f5b2f938d..b825b5fa3 100644 --- a/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py +++ b/tests/unit/modules/dcnm/dcnm_image_policy/test_image_policy_payload.py @@ -86,10 +86,12 @@ def test_image_policy_payload_00120(config2payload) -> None: def configs(): yield configs_config2payload(key) + gen_configs = ResponseGenerator(configs()) def payloads(): yield payloads_config2payload(key) + gen_payloads = ResponseGenerator(payloads()) config = gen_configs.next @@ -124,10 +126,12 @@ def test_image_policy_payload_00121(config2payload) -> None: def configs(): yield configs_config2payload(key) + gen_configs = ResponseGenerator(configs()) def payloads(): yield payloads_config2payload(key) + gen_payloads = ResponseGenerator(payloads()) config = gen_configs.next @@ -163,6 +167,7 @@ def test_image_policy_payload_00122(config2payload) -> None: def configs(): yield configs_config2payload(key) + gen_configs = ResponseGenerator(configs()) config = gen_configs.next @@ -171,7 +176,7 @@ def configs(): instance = Config2Payload() instance.params = {"state": "deleted", "check_mode": False} instance.config = config - match = r"Config2Payload\.commit: config is empty" + match = r"Config2Payload\.commit: config is empty\." with pytest.raises(ValueError, match=match): instance.commit() @@ -199,6 +204,7 @@ def test_image_policy_payload_00123(config2payload, state) -> None: def configs(): yield configs_config2payload(key) + gen_configs = ResponseGenerator(configs()) config = gen_configs.next @@ -336,10 +342,12 @@ def test_image_policy_payload_00220(payload2config) -> None: def configs(): yield configs_payload2config(key) + gen_configs = ResponseGenerator(configs()) def payloads(): yield payloads_payload2config(key) + gen_payloads = ResponseGenerator(payloads()) config = gen_configs.next @@ -374,10 +382,12 @@ def test_image_policy_payload_00221(payload2config) -> None: def configs(): yield configs_payload2config(key) + gen_configs = ResponseGenerator(configs()) def payloads(): yield payloads_payload2config(key) + gen_payloads = ResponseGenerator(payloads()) config = gen_configs.next @@ -412,6 +422,7 @@ def test_image_policy_payload_00222(payload2config) -> None: def payloads(): yield payloads_payload2config(key) + gen_payloads = ResponseGenerator(payloads()) payload = gen_payloads.next @@ -419,7 +430,7 @@ def payloads(): with does_not_raise(): instance = payload2config instance.payload = payload - match = r"Payload2Config\.commit: payload is empty" + match = r"Payload2Config\.commit: payload is empty\." with pytest.raises(ValueError, match=match): instance.commit() assert instance.config == {} From 3b86662c8d5d8531cf3ab195cae143f00172d3be Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 3 Jul 2024 10:05:05 -1000 Subject: [PATCH 230/230] ImagePolicyDelete(): Reduce controller requests, more... 1. Reduce the number of controller requests to the EpPolicies() endpoint. - dcnm_image_policy.py: Deleted().get_policies_to_delete(): - Remove call to get_have(). (saving a call to EpPolicies) - If "config" section of playbook is missing, return a list containing a single element that indicates to ImagePolicyDelete that all image policies should be deleted. - Otherwise, return all image policy names in get_have() - delete.py: ImagePolicyDelete()._get_policies_to_delete(): - Retrieve image policies from controller (this was the 2nd call to EpPolicies endpoint and is now the only call in the delete path.) - If policy_names contains a single element, and that element is "delete_all_image_policies", then rewrite policy_names will all image policy names on the controller. - Else, use policy_names as-is. - Continue with original code flow (check ref_count, etc). 2. Add integration test for deleting all policies when "config" is missing from the playbook. 3. The following unrelated chages were also made: - dcnm_image_policy.py: - Remove class-specific log assignment. log is inherited by all classes from Common(). - Replace deprecated typing.Dict with dict. - Replace deprecated type.List iwth list. - Remove typing import. - Add a few debug logs to better track code flow. 3. image_policies.py - Add debug log to refresh() to track how many times this is called. --- plugins/module_utils/image_policy/delete.py | 30 +- .../image_policy/image_policies.py | 2 + plugins/modules/dcnm_image_policy.py | 80 +++-- ...cnm_image_policy_deleted_all_policies.yaml | 295 ++++++++++++++++++ 4 files changed, 371 insertions(+), 36 deletions(-) create mode 100644 tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted_all_policies.yaml diff --git a/plugins/module_utils/image_policy/delete.py b/plugins/module_utils/image_policy/delete.py index 000bfa213..4c5fd2c7c 100644 --- a/plugins/module_utils/image_policy/delete.py +++ b/plugins/module_utils/image_policy/delete.py @@ -53,12 +53,19 @@ class ImagePolicyDelete: - ``policy_names`` is not a list. - ``policy_names`` is not a list of strings. - ### Usage + ### Usage - Delete specific image policies ```python instance = ImagePolicyDelete() instance.policy_names = ["IMAGE_POLICY_1", "IMAGE_POLICY_2"] instance.commit() ``` + + ### Usage - Delete all image policies + ```python + instance = ImagePolicyDelete() + instance.policy_names = ["delete_all_image_policies"] + instance.commit() + ``` """ def __init__(self): @@ -126,8 +133,12 @@ def _verify_image_policy_ref_count(self, instance, policy_names): def _get_policies_to_delete(self) -> None: """ ### Summary - Retrieve policies from the controller and return the list of - controller policies that are in our policy_names list. + Retrieve image policies from the controller and return the + list of controller policies that are in our policy_names list. + + If policy_names list contains a single element, and that element + is "delete_all_image_policies", then all policies on the controller + are returned. ### Raises - ``ValueError`` if any policy in policy_names has a ref_count @@ -138,6 +149,11 @@ def _get_policies_to_delete(self) -> None: self._image_policies.rest_send = self.rest_send # pylint: enable=no-member self._image_policies.refresh() + if ( + "delete_all_image_policies" in self.policy_names + and len(self.policy_names) == 1 + ): + self.policy_names = list(self._image_policies.all_policies.keys()) try: self._verify_image_policy_ref_count(self._image_policies, self.policy_names) except ValueError as error: @@ -200,6 +216,9 @@ def commit(self): - ``results`` is not set. """ method_name = inspect.stack()[0][3] + msg = f"ENTERED {self.class_name}.{method_name}" + self.log.debug(msg) + try: self._validate_commit_parameters() except ValueError as error: @@ -248,6 +267,9 @@ def _send_requests(self): ``` """ method_name = inspect.stack()[0][3] + msg = f"ENTERED {self.class_name}.{method_name}" + self.log.debug(msg) + self.rest_send.save_settings() self.rest_send.check_mode = self.check_mode @@ -292,7 +314,7 @@ def register_result(self): def policy_names(self): """ ### Summary - Return the policy names + A list of policy names to delete. ### Raises - ``TypeError`` if: diff --git a/plugins/module_utils/image_policy/image_policies.py b/plugins/module_utils/image_policy/image_policies.py index 3d803166f..3008eabc2 100644 --- a/plugins/module_utils/image_policy/image_policies.py +++ b/plugins/module_utils/image_policy/image_policies.py @@ -109,6 +109,8 @@ def refresh(self): @Properties class decorators. """ method_name = inspect.stack()[0][3] + msg = f"ENTERED {self.class_name}.{method_name}" + self.log.debug(msg) if self.rest_send is None: msg = f"{self.class_name}.{method_name}: " diff --git a/plugins/modules/dcnm_image_policy.py b/plugins/modules/dcnm_image_policy.py index b922d5c03..fe98f8212 100644 --- a/plugins/modules/dcnm_image_policy.py +++ b/plugins/modules/dcnm_image_policy.py @@ -256,7 +256,6 @@ import inspect import json import logging -from typing import Dict, List from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import \ @@ -311,6 +310,9 @@ class Common: def __init__(self, params): self.class_name = self.__class__.__name__ method_name = inspect.stack()[0][3] + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.params = params self.check_mode = self.params.get("check_mode", None) @@ -365,7 +367,7 @@ def __init__(self, params): self.need_query = [] self.validated_configs = [] - msg = "ENTERED Common(): " + msg = f"ENTERED Common().{method_name}: " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) @@ -376,8 +378,10 @@ def get_have(self) -> None: self.have consists of the current image policies on the controller """ - msg = f"ENTERED {self.class_name}.get_have()" + method_name = inspect.stack()[0][3] + msg = f"ENTERED {self.class_name}.{method_name}" self.log.debug(msg) + self.have = ImagePolicies() self.have.results = self.results self.have.rest_send = self.rest_send # pylint: disable=no-member @@ -391,8 +395,10 @@ def get_want(self) -> None: 2. Convert the validated configs to payloads 3. Update self.want with this list of payloads """ - msg = "ENTERED" + method_name = inspect.stack()[0][3] + msg = f"ENTERED {self.class_name}.{method_name}" self.log.debug(msg) + # Generate the params_spec used to validate the configs params_spec = ParamsSpec() params_spec.params = self.params @@ -443,10 +449,9 @@ def __init__(self, params): msg += f"Error detail: {error}" raise ValueError(msg) from error - self.log = logging.getLogger(f"dcnm.{self.class_name}") self.replace = ImagePolicyReplaceBulk() - msg = "ENTERED Replaced(): " + msg = f"ENTERED {self.class_name}().{method_name}: " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) @@ -484,11 +489,9 @@ def __init__(self, params): msg += f"Error detail: {error}" raise ValueError(msg) from error - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.delete = ImagePolicyDelete() - msg = "ENTERED Deleted(): " + msg = f"ENTERED {self.class_name}().{method_name}: " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) @@ -506,24 +509,22 @@ def commit(self) -> None: self.delete.params = self.params self.delete.commit() - def get_policies_to_delete(self) -> List[str]: + def get_policies_to_delete(self) -> list[str]: """ Return a list of policy names to delete - In config is present, return list of image policy names - in self.want that exist on the controller - - If config is not present, return list of all image policy - names on the controller + in self.want. + - If config is not present, return ["delete_all_image_policies"], + which ``ImagePolicyDelete()`` interprets as "delete all image + policies on the controller". """ if not self.config: - self.get_have() - return list(self.have.all_policies.keys()) + return ["delete_all_image_policies"] self.get_want() - self.get_have() policy_names_to_delete = [] for want in self.want: - if want["policyName"] in self.have.all_policies: - policy_names_to_delete.append(want["policyName"]) + policy_names_to_delete.append(want["policyName"]) return policy_names_to_delete @@ -544,11 +545,10 @@ def __init__(self, params): msg += f"Error detail: {error}" raise ValueError(msg) from error - self.log = logging.getLogger(f"dcnm.{self.class_name}") self.query = ImagePolicyQuery() self.image_policies = ImagePolicies() - msg = f"ENTERED {self.class_name}.{method_name}: " + msg = f"ENTERED {self.class_name}().{method_name}: " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) @@ -558,11 +558,15 @@ def commit(self) -> None: 1. query the fabrics in self.want that exist on the controller """ method_name = inspect.stack()[0][3] + msg = f"ENTERED {self.class_name}.{method_name}: " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + self.results.state = self.state self.results.check_mode = self.check_mode self.get_want() - # self.get_have() if len(self.want) == 0: msg = f"{self.class_name}.{method_name}: " @@ -604,12 +608,10 @@ def __init__(self, params): msg += f"Error detail: {error}" raise ValueError(msg) from error - self.log = logging.getLogger(f"dcnm.{self.class_name}") - self.delete = ImagePolicyDelete() self.merged = Merged(params) - msg = "ENTERED Overridden(): " + msg = f"ENTERED {self.class_name}().{method_name}: " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) @@ -621,6 +623,10 @@ def commit(self) -> None: - Instantiate`` Merged()`` and call ``Merged().commit()`` """ method_name = inspect.stack()[0][3] + msg = f"ENTERED {self.class_name}.{method_name}: " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) self.results.state = self.state self.results.check_mode = self.check_mode @@ -699,8 +705,6 @@ def __init__(self, params): msg += f"Error detail: {error}" raise ValueError(msg) from error - self.log = logging.getLogger(f"dcnm.{self.class_name}") - msg = f"params: {json_pretty(self.params)}" self.log.debug(msg) if not params.get("config"): @@ -710,16 +714,16 @@ def __init__(self, params): self.create = ImagePolicyCreateBulk() self.update = ImagePolicyUpdateBulk() - msg = f"ENTERED {self.class_name}.{method_name}: " - msg += f"state: {self.state}, " - msg += f"check_mode: {self.check_mode}" - self.log.debug(msg) - # new policies to be created self.need_create: list = [] # existing policies to be updated self.need_update: list = [] + msg = f"ENTERED {self.class_name}().{method_name}: " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + def get_need(self): """ ### Summary @@ -738,6 +742,12 @@ def get_need(self): are identical, do not append the policy to self.need_update (i.e. do nothing). """ + method_name = inspect.stack()[0][3] + msg = f"ENTERED {self.class_name}.{method_name}: " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + for want in self.want: self.have.policy_name = want.get("policyName") @@ -759,6 +769,12 @@ def commit(self) -> None: """ Commit the merged state requests """ + method_name = inspect.stack()[0][3] + msg = f"ENTERED {self.class_name}.{method_name}: " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + self.results.state = self.state self.results.check_mode = self.check_mode @@ -768,7 +784,7 @@ def commit(self) -> None: self.send_need_create() self.send_need_update() - def _prepare_for_merge(self, have: Dict, want: Dict): + def _prepare_for_merge(self, have: dict, want: dict): """ ### Summary - Remove fields in "have" that are not part of a request payload i.e. diff --git a/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted_all_policies.yaml b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted_all_policies.yaml new file mode 100644 index 000000000..b6f2f3e5d --- /dev/null +++ b/tests/integration/targets/dcnm_image_policy/tests/dcnm_image_policy_deleted_all_policies.yaml @@ -0,0 +1,295 @@ +################################################################################ +# RUNTIME +################################################################################ + +# Recent run times (MM:SS.ms): +# 00:18.960 +# 00:19.240 +# 00:18.836 +################################################################################ +# STEPS +################################################################################ +# +# WARNING!!! THIS TEST WILL DELETE ALL IMAGE POLICIES ON THE CONTROLLER. +# DO NOT RUN THIS TEST ON A PRODUCTION CONTROLLER. +# +# SETUP +# 1. The following images must already be uploaded to the controller +# See vars: section in cisco/dcnm/playbooks/dcnm_tests.yaml +# - nxos_image_1 +# - nxos_image_2 +# - epld_image_1 +# - epld_image_2 +# 2. No need for fabric or switches +# 3. Delete image policies under test, if they exist +# - image_policy_1 +# - image_policy_2 +# TEST +# 4. Create image policies and verify result +# - image_policy_1 +# - image_policy_2 +# 5. Delete ALL image policies and verify result. +# CLEANUP +# 7. No cleanup required + +################################################################################ +# REQUIREMENTS +################################################################################ + +# 1. The following images must already be uploaded to the controller +# See vars: section below +# - nxos_image_1 +# - nxos_image_2 +# - epld_image_1 +# - epld_image_2 +# 2. No need for fabric or switches +# +# Example vars for dcnm_image_policy integration tests +# Add to cisco/dcnm/playbooks/dcnm_tests.yaml) +# +# vars: +# # This testcase field can run any test in the tests directory for the role +# testcase: dcnm_image_policy_deleted_all_policies +# fabric_name: f1 +# username: admin +# password: "foobar" +# switch_username: admin +# switch_password: "foobar" +# spine1: 172.22.150.114 +# spine2: 172.22.150.115 +# leaf1: 172.22.150.106 +# leaf2: 172.22.150.107 +# leaf3: 172.22.150.108 +# leaf4: 172.22.150.109 +# # for dcnm_image_policy role +# image_policy_1: "KR5M" +# image_policy_2: "NR1F" +# epld_image_1: n9000-epld.10.2.5.M.img +# epld_image_2: n9000-epld.10.3.1.F.img +# nxos_image_1: n9000-dk9.10.2.5.M.bin +# nxos_image_2: n9000-dk9.10.3.1.F.bin +# nxos_release_1: 10.2.5_nxos64-cs_64bit +# nxos_release_2: 10.3.1_nxos64-cs_64bit + +################################################################################ +# SETUP +################################################################################ + +- name: DELETED - SETUP - Delete image policies + cisco.dcnm.dcnm_image_policy: + state: deleted + config: + - name: "{{ image_policy_1 }}" + - name: "{{ image_policy_2 }}" + register: result + +- debug: + var: result +################################################################################ +# DELETED - TEST - Create two image policies and verify +################################################################################ +# Expected result +# ok: [dcnm] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "agnostic": false, +# "epldImgName": "n9000-epld.10.2.5.M.img", +# "nxosVersion": "10.2.5_nxos64-cs_64bit", +# "platform": "N9K", +# "policyDescr": "KR5M", +# "policyName": "KR5M", +# "policyType": "PLATFORM", +# "sequence_number": 1 +# }, +# { +# "agnostic": false, +# "epldImgName": "n9000-epld.10.3.1.F.img", +# "nxosVersion": "10.3.1_nxos64-cs_64bit", +# "platform": "N9K", +# "policyDescr": "NR1F", +# "policyName": "NR1F", +# "policyType": "PLATFORM", +# "sequence_number": 2 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "create", +# "check_mode": false, +# "sequence_number": 1, +# "state": "merged" +# }, +# { +# "action": "create", +# "check_mode": false, +# "sequence_number": 2, +# "state": "merged" +# } +# ], +# "response": [ +# { +# "DATA": "Policy created successfully.", +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# }, +# { +# "DATA": "Policy created successfully.", +# "MESSAGE": "OK", +# "METHOD": "POST", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/platform-policy", +# "RETURN_CODE": 200, +# "sequence_number": 2 +# } +# ], +# "result": [ +# { +# "found": true, +# "sequence_number": 0, +# "success": true +# }, +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# }, +# { +# "changed": true, +# "sequence_number": 2, +# "success": true +# } +# ] +# } +# } + +- name: DELETED - TEST - Create two image policies and verify + cisco.dcnm.dcnm_image_policy: + state: merged + config: + - name: "{{ image_policy_1 }}" + agnostic: false + description: "{{ image_policy_1 }}" + epld_image: "{{ epld_image_1 }}" + platform: N9K + release: "{{ nxos_release_1 }}" + type: PLATFORM + - name: "{{ image_policy_2 }}" + description: "{{ image_policy_2 }}" + platform: N9K + epld_image: "{{ epld_image_2 }}" + release: "{{ nxos_release_2 }}" + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 2 + - result.diff[0].policyName == image_policy_1 + - result.diff[0].policyDescr == image_policy_1 + - result.diff[0].epldImgName == epld_image_1 + - result.diff[0].nxosVersion == nxos_release_1 + - result.diff[0].sequence_number == 1 + - result.diff[1].policyName == image_policy_2 + - result.diff[1].policyDescr == image_policy_2 + - result.diff[1].epldImgName == epld_image_2 + - result.diff[1].nxosVersion == nxos_release_2 + - result.diff[1].sequence_number == 2 + - (result.metadata | length) == 2 + - result.metadata[0].action == "create" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "merged" + - result.metadata[1].action == "create" + - result.metadata[1].check_mode == False + - result.metadata[1].sequence_number == 2 + - result.metadata[1].state == "merged" + - (result.response | length) == 2 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "POST" + - result.response[0].RETURN_CODE == 200 + - result.response[0].sequence_number == 1 + - result.response[1].MESSAGE == "OK" + - result.response[1].METHOD == "POST" + - result.response[1].RETURN_CODE == 200 + - result.response[1].sequence_number == 2 + +################################################################################ +# DELETED - TEST - Delete all image policies and verify the result +################################################################################ +# ok: [172.22.150.244] => { +# "result": { +# "changed": true, +# "diff": [ +# { +# "policyNames": [ +# "KR5M", +# "NR1F" +# ], +# "sequence_number": 1 +# } +# ], +# "failed": false, +# "metadata": [ +# { +# "action": "delete", +# "check_mode": false, +# "sequence_number": 1, +# "state": "deleted" +# } +# ], +# "response": [ +# { +# "DATA": "Selected policy(s) deleted successfully.", +# "MESSAGE": "OK", +# "METHOD": "DELETE", +# "REQUEST_PATH": "https://172.22.150.244:443/appcenter/cisco/ndfc/api/v1/imagemanagement/rest/policymgnt/policy", +# "RETURN_CODE": 200, +# "sequence_number": 1 +# } +# ], +# "result": [ +# { +# "changed": true, +# "sequence_number": 1, +# "success": true +# } +# ] +# } +# } +- name: DELETED - TEST - Delete all image policies and verify the result + cisco.dcnm.dcnm_image_policy: + state: deleted + register: result + +- debug: + var: result + +- assert: + that: + - result.changed == true + - result.failed == false + - (result.diff | length) == 1 + - image_policy_1 in result.diff[0].policyNames + - image_policy_2 in result.diff[0].policyNames + - (result.response | length) == 1 + - result.response[0].MESSAGE == "OK" + - result.response[0].METHOD == "DELETE" + - result.response[0].RETURN_CODE == 200 + - (result.metadata | length) == 1 + - result.metadata[0].action == "delete" + - result.metadata[0].check_mode == False + - result.metadata[0].sequence_number == 1 + - result.metadata[0].state == "deleted" + - (result.result | length) == 1 + - result.result[0].changed == true + - result.result[0].sequence_number == 1 + - result.result[0].success == true