From 35e926bb78db72814cc8316f9f1763ce4b2cfd6e Mon Sep 17 00:00:00 2001 From: Shangxin Du Date: Tue, 18 Jun 2024 05:51:36 -0700 Subject: [PATCH 1/6] feat[dcnm_policy]: adding the functions to handle use_desc_as_key (#285) * adding the functions to handle use_desc_as_key * adding doc for the new parameter * add bulk update API enpoints for dcnm 11 * fix a pep8 error * address comments and fix a bug when templateNmae is updated, wrong templateName is reported * adding support for the state query, update the document and examples add test cases for use_desc_as_key * fix pep8 errors * fix yamllint error * fix yamllint error * address comments and update the doc --- .vscode/settings.json | 5 + docs/cisco.dcnm.dcnm_policy_module.rst | 60 ++ plugins/modules/dcnm_policy.py | 245 ++++- .../dcnm/fixtures/dcnm_policy_configs.json | 891 ++++++++++-------- .../dcnm/fixtures/dcnm_policy_payloads.json | 369 ++++---- tests/unit/modules/dcnm/test_dcnm_policy.py | 147 +++ 6 files changed, 1095 insertions(+), 622 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..d969f962b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.testing.pytestArgs": ["tests"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} diff --git a/docs/cisco.dcnm.dcnm_policy_module.rst b/docs/cisco.dcnm.dcnm_policy_module.rst index 8324e8522..3871646ac 100644 --- a/docs/cisco.dcnm.dcnm_policy_module.rst +++ b/docs/cisco.dcnm.dcnm_policy_module.rst @@ -347,6 +347,26 @@ Parameters
The required state of the configuration after module completion.
+ + +
+ use_desc_as_key + +
+ boolean +
+ + + + + +
Flag to enforce using the description parameter as the unique key for policy management.
+
When set to True, the description parameter must be unique and non-empty for each policy in the playbook. The module will also use the description to find the policy to modify or delete. If exsiting policies have the same description, the module will raise an error. If the existing policy with the matching description is using differnet template name, the module will delete the existing policy and create a new one.
+ +
@@ -535,6 +555,46 @@ Examples - switch: - ip: "{{ ansible_switch1 }}" + # Use the description as key + + # NOTE: As the description of the policy in NDFC/DCNM is not unique, + # the user must make sure no policies with the same description are created on NDFC out of the playbook. + # If the description is not unique, the module will raise an error. + + ## Below task will create policies with description "policy_radius" on swtich1, switch2 and switch3, + ## and only create policy "feature bfd" and "feature bash-shell" on the switch1 only + + - name: Create policies + cisco.dcnm.dcnm_policy: + fabric: fabric_prod + use_desc_as_key: true + config: + - name: switch_freeform + create_additional_policy: false + description: policy_radius + policy_vars: + CONF: | + radius-server host 10.1.1.2 key 7 "ljw3976!" authentication accounting + - switch: + - ip: "{{ switch1 }}" + policies: + - name: switch_freeform + create_additional_policy: false + priority: 101 + description: feature bfd + policy_vars: + CONF: | + feature bfd + - name: switch_freeform + create_additional_policy: false + priority: 102 + description: feature bash-shell + policy_vars: + CONF: | + feature bash-shell + - ip: "{{ switch2 }}" + - ip: "{{ switch3 }}" + diff --git a/plugins/modules/dcnm_policy.py b/plugins/modules/dcnm_policy.py index 9bdd101a6..42d2ae29d 100644 --- a/plugins/modules/dcnm_policy.py +++ b/plugins/modules/dcnm_policy.py @@ -44,6 +44,17 @@ - query default: merged + use_desc_as_key: + description: + - Flag to enforce using the description parameter as the unique key for policy management. + - When set to True, the description parameter must be unique and non-empty for each policy in the playbook. + The module will also use the description to find the policy to modify or delete. + If exsiting policies have the same description, the module will raise an error. + If the existing policy with the matching description is using differnet template name, the module will delete the existing policy and create a new one. + type: bool + required: false + default: false + deploy: description: - A flag specifying if a policy is to be deployed on the switches @@ -338,6 +349,46 @@ - name: POLICY-103103 - switch: - ip: "{{ ansible_switch1 }}" + +# Use the description as key + +# NOTE: As the description of the policy in NDFC/DCNM is not unique, +# the user must make sure no policies with the same description are created on NDFC out of the playbook. +# If the description is not unique, the module will raise an error. + +## Below task will create policies with description "policy_radius" on swtich1, switch2 and switch3, +## and only create policy "feature bfd" and "feature bash-shell" on the switch1 only + +- name: Create policies + cisco.dcnm.dcnm_policy: + fabric: fabric_prod + use_desc_as_key: true + config: + - name: switch_freeform + create_additional_policy: false + description: policy_radius + policy_vars: + CONF: | + radius-server host 10.1.1.2 key 7 "ljw3976!" authentication accounting + - switch: + - ip: "{{ switch1 }}" + policies: + - name: switch_freeform + create_additional_policy: false + priority: 101 + description: feature bfd + policy_vars: + CONF: | + feature bfd + - name: switch_freeform + create_additional_policy: false + priority: 102 + description: feature bash-shell + policy_vars: + CONF: | + feature bash-shell + - ip: "{{ switch2 }}" + - ip: "{{ switch3 }}" """ import json @@ -363,6 +414,7 @@ class DcnmPolicy: "POLICY_WITH_ID": "/rest/control/policies/{}", "POLICY_GET_SWITCHES": "/rest/control/policies/switches?serialNumber={}", "POLICY_BULK_CREATE": "/rest/control/policies/bulk-create", + "POLICY_BULK_UPDATE": "/rest/control/policies/{}/bulk", "POLICY_MARK_DELETE": "/rest/control/policies/{}/mark-delete", "POLICY_DEPLOY": "/rest/control/policies/deploy", "POLICY_CFG_DEPLOY": "/rest/control/fabrics/{}/config-deploy/", @@ -373,6 +425,7 @@ class DcnmPolicy: "POLICY_WITH_ID": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/policies/{}", "POLICY_GET_SWITCHES": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/policies/switches?serialNumber={}", "POLICY_BULK_CREATE": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/policies/bulk-create", + "POLICY_BULK_UPDATE": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/policies/{}/bulk", "POLICY_MARK_DELETE": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/policies/{}/mark-delete", "POLICY_DEPLOY": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/policies/deploy", "POLICY_CFG_DEPLOY": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{}/config-deploy/", @@ -385,6 +438,7 @@ def __init__(self, module): self.module = module self.params = module.params self.fabric = module.params["fabric"] + self.use_desc_as_key = module.params["use_desc_as_key"] self.config = copy.deepcopy(module.params.get("config")) self.deploy = True # Global 'deploy' flag self.pb_input = [] @@ -458,6 +512,8 @@ def dcnm_policy_validate_input(self): switch=dict(required=True, type="list"), ) + desc_hash = {} # hash table to store the count of policy descriptions + for cfg in self.config: clist = [] @@ -470,7 +526,30 @@ def dcnm_policy_validate_input(self): cfg["name"], invalid_params ) self.module.fail_json(msg=mesg) + if self.use_desc_as_key: + # Fail the module when use_desc_as_key is True but description is not given or empty + if cfg.get("description", "") == "": + mesg = f"description can't be empty when use_desc_as_key is True: {cfg}" + self.module.fail_json(msg=mesg) + # Count the occurance of the "description|switch" + for sw in cfg["switch"]: + if desc_hash.get(f"{cfg['description']}|{sw}", -1) == -1: + desc_hash[f"{cfg['description']}|{sw}"] = 1 + else: + desc_hash[f"{cfg['description']}|{sw}"] += 1 + self.policy_info.extend(policy_info) + # Find the duplicated description per swtich + dup_desc = [] + for desc in desc_hash.keys(): + if desc_hash[desc] == 1: + continue + dup_desc.append( + f"description: {desc.split('|')[0]}, switch: {desc.split('|')[1]}" + ) + if dup_desc != []: + mesg = f"duplicated description found: description: {dup_desc}" + self.module.fail_json(msg=mesg) def dcnm_get_policy_payload_with_template_name(self, pelem, sw): @@ -637,12 +716,26 @@ def dcnm_policy_get_have(self): pl for pl in plist for wp in self.want - if (pl["templateName"] == wp["templateName"]) + # exclude the policies that have the source + # when the user modifies a policy but has not deployed the policy, + # a sub-policy might be created with the same description, but marked as deleted + # The signature of this kind of policy is that it as a original policyId as the source + # it should be excluded from the match list + # as the policy will be deleted once the user deploys the configuration + if pl.get("source", "") == "" + and ( + (pl["templateName"] == wp["templateName"]) + or ( + not self.use_desc_as_key + or (pl.get("description") == wp.get("description", "")) + ) + ) ] # match_pol can be a list of dicts, containing duplicates. Remove the duplicate entries + # also exclude the policies that are marked for deletion for pol in match_pol: - if pol not in self.have: + if pol not in self.have and not pol.get("deleted", True): self.have.append(pol) def dcnm_policy_compare_nvpairs(self, pnv, hnv): @@ -658,8 +751,12 @@ def dcnm_policy_compare_nvpairs(self, pnv, hnv): return "DCNM_POLICY_MATCH" def dcnm_policy_compare_policies(self, policy): - - found = False + # use list to instead of boolean + # need to handle two templates associated with switch have the same description + # when use_desc_as_key is true, raise error + found = [] + match = False + template_changed = False if self.have == []: return ("DCNM_POLICY_ADD_NEW", None) @@ -669,6 +766,8 @@ def dcnm_policy_compare_policies(self, policy): if policy.get("policyId", None) is not None: key = "policyId" + elif self.use_desc_as_key: + key = "description" else: key = "templateName" @@ -676,8 +775,16 @@ def dcnm_policy_compare_policies(self, policy): if (have[key] == policy[key]) and ( have.get("serialNumber", None) == policy["serialNumber"] ): - found = True - # Have a policy with matching template name. Check for other objects + found.append(have) + # if use description as key, use policyId got from the target + # if templateName is changed, remove the original policy and create a new one + if self.use_desc_as_key: + policy["policyId"] = have.get("policyId") + if have["templateName"] != policy["templateName"]: + template_changed = True + continue + + # Have a policy with matching key. Check for other objects if have.get("description", None) == policy["description"]: if have.get("priority", None) == policy["priority"]: if ( @@ -687,11 +794,22 @@ def dcnm_policy_compare_policies(self, policy): ) == "DCNM_POLICY_MATCH" ): - return ("DCNM_POLICY_DONT_ADD", have["policyId"]) - if found is True: + match = True + + if len(found) == 1 and not match and not template_changed: # Found a matching policy with the given template name, but other objects don't match. # Go ahead and merge the objects into the existing policy - return ("DCNM_POLICY_MERGE", have["policyId"]) + return ("DCNM_POLICY_MERGE", found[0]["policyId"]) + elif len(found) == 1 and not match and template_changed: + return ("DCNM_POLICY_TEMPLATE_CHANGED", found[0]["policyId"]) + elif len(found) == 1 and match: + return ("DCNM_POLICY_DONT_ADD", found[0]["policyId"]) + elif len(found) > 1 and self.use_desc_as_key: + # module will raise error when duplicated description is found + return ("DCNM_POLICY_DUPLICATED", None) + elif len(found) > 1 and not self.use_desc_as_key: + # if not using description as the key, new + return ("DCNM_POLICY_DONT_ADD", found[0]["policyId"]) else: return ("DCNM_POLICY_ADD_NEW", None) @@ -715,7 +833,11 @@ def dcnm_policy_get_diff_merge(self): rc, policy_id = self.dcnm_policy_compare_policies(policy) - if rc == "DCNM_POLICY_ADD_NEW": + if rc == "DCNM_POLICY_DUPLICATED": + self.module.fail_json( + f"Multiple policies found with the same description in DCNM/NDFC: {self.use_desc_as_key}, {policy['description']}" + ) + elif rc == "DCNM_POLICY_ADD_NEW": # A policy does not exists, create a new one. Even if one exists, if create_additional_policy # is specified, then create the policy if (policy not in self.diff_create) or ( @@ -730,6 +852,11 @@ def dcnm_policy_get_diff_merge(self): # will not know which policy the user is referring to. In the case where a user is providing # a templateName and we are here, ignore the policy. if policy.get("policyId", None) is not None: + # id is needed for policy update + id = policy["policyId"].split("-")[1] + policy["id"] = id + policy["policy_id_given"] = True + if policy not in self.diff_modify: self.changed_dict[0]["merged"].append(policy) self.diff_modify.append(policy) @@ -760,6 +887,27 @@ def dcnm_policy_get_diff_merge(self): self.changed_dict[0]["merged"].append(policy) self.diff_create.append(policy) policy_id = None + elif rc == "DCNM_POLICY_TEMPLATE_CHANGED": + # A policy exists and the template name is changed + # Remove the existing policy and create a new one + + pinfo = self.dcnm_policy_get_policy_info_from_dcnm(policy["policyId"]) + prev_template_name = policy["templateName"] + if pinfo != []: + prev_template_name = pinfo["templateName"] + + del_payload = self.dcnm_policy_get_delete_payload(policy) + if del_payload not in self.diff_delete: + self.diff_delete.append(del_payload) + self.changed_dict[0]["deleted"].append( + { + "policy": policy["policyId"], + "templateName": prev_template_name, + } + ) + policy.pop("policyId") + self.changed_dict[0]["merged"].append(policy) + self.diff_create.append(policy) # Check the 'deploy' flag and decide if this policy is to be deployed if self.deploy is True: @@ -814,13 +962,22 @@ def dcnm_policy_get_diff_deleted(self): for pl in plist for wp in self.want if ( - (wp["policy_id_given"] is False) - and (pl["templateName"] == wp["templateName"]) - or (wp["policy_id_given"] is True) - and (pl["policyId"] == wp["policyId"]) + not pl["deleted"] + and ( + (wp["policy_id_given"] is False) + and (pl["templateName"] == wp["templateName"]) + and ( + # When use_desc_as_key is True, only add the policy match the description + not self.use_desc_as_key + or (pl.get("description", "") == wp.get("description", "")) + ) + ) + or ( + (wp["policy_id_given"] is True) + and (pl["policyId"] == wp["policyId"]) + ) ) ] - # match_pol contains all the policies which exist and are to be deleted # Build the delete payloads @@ -875,7 +1032,7 @@ def dcnm_policy_get_diff_query(self): self.result["response"].append(pinfo) else: # templateName is given. Note this down - match_templates.append(cfg["name"]) + match_templates.append(cfg) if (get_specific_policies is False) or (match_templates != []): @@ -890,12 +1047,16 @@ def dcnm_policy_get_diff_query(self): match_pol = [ pl for pl in plist - for mt_name in match_templates - if (pl["templateName"] == mt_name) + for mt in match_templates + if (pl["templateName"] == mt["name"]) + # When use_desc_as_key is True, only add the policy match the description + and ( + not self.use_desc_as_key + or pl.get("description", "") == mt["description"] + ) ] else: match_pol = plist - if match_pol: # match_pol contains all the policies which exist and match the given templates self.changed_dict[0]["query"].extend( @@ -943,6 +1104,35 @@ def dcnm_policy_create_policy(self, policy, command): return resp + def dcnm_policy_update_policy(self, policy, command): + + path = self.paths["POLICY_BULK_UPDATE"].format(policy["policyId"]) + + json_payload = json.dumps([policy]) + + retries = 0 + while retries < 3: + resp = dcnm_send(self.module, command, path, json_payload) + + if ( + (resp.get("DATA", None) is not None) + and (isinstance(resp["DATA"], dict)) + and (resp["DATA"].get("failureList", None) is not None) + ): + if isinstance(resp["DATA"]["failureList"], list): + fl = resp["DATA"]["failureList"][0] + else: + fl = resp["DATA"]["failureList"] + + if "is not unique" in fl.get("message", ""): + retries = retries + 1 + continue + break + + self.result["response"].append(resp) + + return resp + def dcnm_policy_delete_policy(self, policy, mark_del): if mark_del is True: @@ -1137,7 +1327,8 @@ def dcnm_policy_send_message_to_dcnm(self): for policy in self.diff_modify: # POP the 'create_additional_policy' object before sending create policy.pop("create_additional_policy") - resp = self.dcnm_policy_create_policy(policy, "PUT") + policy.pop("policy_id_given", "") + resp = self.dcnm_policy_update_policy(policy, "PUT") if isinstance(resp, list): resp = resp[0] if ( @@ -1231,8 +1422,9 @@ def dcnm_translate_config(self, config): cfg["switch"] = [] if sw["ip"] not in cfg["switch"]: cfg["switch"].append(sw["ip"]) - - if config: + # if use_desc_as_key is true, don't override the policy with the same templateName + # the per-switch polices will be simpliy merged with global policies config + if config and not self.use_desc_as_key: updated_config = [] for ovr_cfg in override_config: for cfg in config: @@ -1251,7 +1443,7 @@ def dcnm_translate_config(self, config): updated_config.append(cfg) config = updated_config else: - config = override_config + config += override_config return config @@ -1262,6 +1454,7 @@ def main(): element_spec = dict( fabric=dict(required=True, type="str"), config=dict(required=False, type="list", elements="dict"), + use_desc_as_key=dict(required=False, type="bool", default=False), state=dict( type="str", default="merged", @@ -1310,11 +1503,7 @@ def main(): if module.params["state"] != "query": # Translate the given playbook config to some convenient format. Each policy should # have the switches to be deployed. - - dcnm_policy.config = dcnm_policy.dcnm_translate_config( - dcnm_policy.config - ) - + dcnm_policy.config = dcnm_policy.dcnm_translate_config(dcnm_policy.config) # See if this is required dcnm_policy.dcnm_policy_copy_config() dcnm_policy.dcnm_policy_validate_input() diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_policy_configs.json b/tests/unit/modules/dcnm/fixtures/dcnm_policy_configs.json index e52937e55..be4909828 100644 --- a/tests/unit/modules/dcnm/fixtures/dcnm_policy_configs.json +++ b/tests/unit/modules/dcnm/fixtures/dcnm_policy_configs.json @@ -1,7 +1,7 @@ { "create_policy_125_127_with_vars": [ - { - "create_additional_policy": false, + { + "create_additional_policy": false, "name": "template_125", "description": "125 - policy with vars", "priority": 125, @@ -9,472 +9,543 @@ "OSPF_TAG": 2000, "LOOPBACK_IP": "10.10.10.108" } - }, - { - "create_additional_policy": false, - "name": "template_126", - "description": "126 - policy with vars", - "priority": 126, - "policy_vars": { - "OSPF_TAG": 3000, - "LOOPBACK_IP": "10.10.10.109" - } - }, - { - "create_additional_policy": false, - "name": "template_127", - "description": "127 - policy with vars", - "priority": 127, - "policy_vars": { - "OSPF_TAG": 4000, - "LOOPBACK_IP": "10.10.10.110" - } - }, - { - "switch": [ + }, + { + "create_additional_policy": false, + "name": "template_126", + "description": "126 - policy with vars", + "priority": 126, + "policy_vars": { + "OSPF_TAG": 3000, + "LOOPBACK_IP": "10.10.10.109" + } + }, + { + "create_additional_policy": false, + "name": "template_127", + "description": "127 - policy with vars", + "priority": 127, + "policy_vars": { + "OSPF_TAG": 4000, + "LOOPBACK_IP": "10.10.10.110" + } + }, { - "ip": "10.10.10.224" + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], "create_policy_multi_switch_101_105": [ - { - "create_additional_policy": false, - "name": "template_101", - "priority": 101 - }, - { - "create_additional_policy": false, - "description": "102 - No piority given", - "name": "template_102" - }, - { - "create_additional_policy": false, - "description": "Both description and priority given", - "name": "template_103", - "priority": 500 - }, - { - "switch": [ - { - "ip": "10.10.10.224", - "policies": [ - { - "create_additional_policy": false, - "name": "template_104" - }, - { - "create_additional_policy": false, - "name": "template_105" - } - ] + { + "create_additional_policy": false, + "name": "template_101", + "priority": 101 + }, + { + "create_additional_policy": false, + "description": "102 - No piority given", + "name": "template_102" }, { - "ip": "10.10.10.225" + "create_additional_policy": false, + "description": "Both description and priority given", + "name": "template_103", + "priority": 500 }, { - "ip": "10.10.10.226" + "switch": [ + { + "ip": "10.10.10.224", + "policies": [ + { + "create_additional_policy": false, + "name": "template_104" + }, + { + "create_additional_policy": false, + "name": "template_105" + } + ] + }, + { + "ip": "10.10.10.225" + }, + { + "ip": "10.10.10.226" + } + ] } - ] - }], + ], "create_policy_101_101_5": [ - { - "create_additional_policy": false, - "name": "template_101", - "priority": 101, - "description": "Create again even if it exists" - }, - { - "create_additional_policy": false, - "description": "101 - No piority given", - "name": "template_101" - }, - { - "create_additional_policy": false, - "description": "Both description and priority given", - "name": "template_101", - "priority": 500 - }, - { - "switch": [ - { - "ip": "10.10.10.224", - "policies": [ - { - "create_additional_policy": false, - "name": "template_101", - "description": "description changed" - }, - { - "create_additional_policy": false, - "name": "template_101", - "description": "description changed to verify merge" - } - ] + { + "create_additional_policy": false, + "name": "template_101", + "priority": 101, + "description": "Create again even if it exists" + }, + { + "create_additional_policy": false, + "description": "101 - No piority given", + "name": "template_101" + }, + { + "create_additional_policy": false, + "description": "Both description and priority given", + "name": "template_101", + "priority": 500 }, { - "ip": "10.10.10.224" + "switch": [ + { + "ip": "10.10.10.224", + "policies": [ + { + "create_additional_policy": false, + "name": "template_101", + "description": "description changed" + }, + { + "create_additional_policy": false, + "name": "template_101", + "description": "description changed to verify merge" + } + ] + }, + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], - "modify_policy_125_with_vars" : [ - { - "create_additional_policy": false, - "name": "POLICY-125125", - "priority": 125, - "policy_vars": { - "OSPF_TAG": 2000, - "LOOPBACK_IP": "11.11.11.108" - } - }, - { - "switch": [ + "modify_policy_125_with_vars": [ { - "ip": "10.10.10.224" + "create_additional_policy": false, + "name": "POLICY-125125", + "priority": 125, + "policy_vars": { + "OSPF_TAG": 2000, + "LOOPBACK_IP": "11.11.11.108" + } + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], "create_policy_101_105": [ - { - "create_additional_policy": false, - "name": "template_101", - "priority": 101 - }, - { - "create_additional_policy": false, - "description": "102 - No piority given", - "name": "template_102" - }, - { - "create_additional_policy": false, - "description": "Both description and priority given", - "name": "template_103", - "priority": 500 - }, - { - "switch": [ - { - "ip": "10.10.10.224", - "policies": [ - { - "create_additional_policy": false, - "name": "template_104" - }, - { - "create_additional_policy": false, - "name": "template_105" - } + { + "create_additional_policy": false, + "name": "template_101", + "description": "policy101", + "priority": 101 + }, + { + "create_additional_policy": false, + "description": "policy102", + "name": "template_102" + }, + { + "create_additional_policy": false, + "name": "template_103", + "description": "policy103", + "priority": 500 + }, + { + "switch": [ + { + "ip": "10.10.10.224", + "policies": [ + { + "create_additional_policy": false, + "name": "template_104", + "description": "policy104" + }, + { + "create_additional_policy": false, + "name": "template_105", + "description": "policy105" + } + ] + }, + { + "ip": "10.10.10.224" + } ] + } + ], + + "modify_policy_101_102": [ + { + "create_additional_policy": false, + "name": "template_101_1", + "description": "policy101", + "priority": 101 }, { - "ip": "10.10.10.224" + "create_additional_policy": false, + "description": "policy102", + "name": "template_102_1" + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], "create_policy_101_105_5": [ - { - "create_additional_policy": false, - "name": "template_101", - "priority": 101 - }, - { - "create_additional_policy": false, - "description": "102 - No piority given", - "name": "template_102" - }, - { - "create_additional_policy": false, - "description": "Both description and priority given", - "name": "template_103", - "priority": 500 - }, - { - "switch": [ - { - "ip": "10.10.10.224", - "policies": [ - { - "create_additional_policy": false, - "name": "template_101", - "description": "Description override - 10.10.10.225" - }, - { - "create_additional_policy": false, - "name": "template_102", - "description": "Description override - 10.10.10.225" - } - ] + { + "create_additional_policy": false, + "name": "template_101", + "priority": 101 }, { - "ip": "10.10.10.225", - "policies": [ - { - "create_additional_policy": false, - "name": "template_101", - "description": "Description override - 10.10.10.225" - }, - { - "create_additional_policy": false, - "name": "template_102", - "description": "Description override - 10.10.10.225" - } - ] + "create_additional_policy": false, + "description": "102 - No piority given", + "name": "template_102" }, { - "ip": "10.10.10.226", - "policies": [ - { - "create_additional_policy": false, - "name": "template_104", - "description": "Description override - 10.10.10.225" - }, - { - "create_additional_policy": false, - "name": "template_105", - "description": "Description override - 10.10.10.225" - } - ] + "create_additional_policy": false, + "description": "Both description and priority given", + "name": "template_103", + "priority": 500 }, { - "ip": "10.10.10.226" + "switch": [ + { + "ip": "10.10.10.224", + "policies": [ + { + "create_additional_policy": false, + "name": "template_101", + "description": "Description override - 10.10.10.225" + }, + { + "create_additional_policy": false, + "name": "template_102", + "description": "Description override - 10.10.10.225" + } + ] + }, + { + "ip": "10.10.10.225", + "policies": [ + { + "create_additional_policy": false, + "name": "template_101", + "description": "Description override - 10.10.10.225" + }, + { + "create_additional_policy": false, + "name": "template_102", + "description": "Description override - 10.10.10.225" + } + ] + }, + { + "ip": "10.10.10.226", + "policies": [ + { + "create_additional_policy": false, + "name": "template_104", + "description": "Description override - 10.10.10.225" + }, + { + "create_additional_policy": false, + "name": "template_105", + "description": "Description override - 10.10.10.225" + } + ] + }, + { + "ip": "10.10.10.226" + } + ] } - ] - }], + ], "create_policy_without_state_104_105": [ - { - "switch": [ - { - "ip": "10.10.10.224", - "policies": [ - { - "create_additional_policy": false, - "name": "template_104" - }, - { - "create_additional_policy": false, - "name": "template_105" - } + { + "switch": [ + { + "ip": "10.10.10.224", + "policies": [ + { + "create_additional_policy": false, + "name": "template_104" + }, + { + "create_additional_policy": false, + "name": "template_105" + } + ] + }, + { + "ip": "10.10.10.225" + } ] + } + ], + + "create_policy_additional_flags_104": [ + { + "create_additional_policy": true, + "description": "create template_104", + "name": "template_104", + "priority": 104 + }, + { + "create_additional_policy": true, + "description": "create template_104", + "name": "template_104", + "priority": 104 }, { - "ip": "10.10.10.225" + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], - "create_policy_additional_flags_104" : [ - { - "create_additional_policy": true, - "description": "create template_104", - "name": "template_104", - "priority": 104 - }, - { - "create_additional_policy": true, - "description": "create template_104", - "name": "template_104", - "priority": 104 - }, - { - "switch": [ - { - "ip": "10.10.10.224" + "modify_policy_104_with_policy_id": [ + { + "create_additional_policy": false, + "description": "modifying policy with policy ID", + "name": "POLICY-123840", + "priority": 904 + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], - "modify_policy_104_with_policy_id" : [ - { - "create_additional_policy": false, - "description": "modifying policy with policy ID", - "name": "POLICY-123840", - "priority": 904 - }, - { - "switch": [ - { - "ip": "10.10.10.224" + "modify_policy_104_with_template_name": [ + { + "create_additional_policy": false, + "description": "modifying policy with template name", + "name": "template_104", + "priority": 904 + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], - "modify_policy_104_with_template_name" : [ - { - "create_additional_policy": false, - "description": "modifying policy with template name", - "name": "template_104", - "priority": 904 - }, - { - "switch": [ - { - "ip": "10.10.10.224" + "create_policy_no_deploy_104": [ + { + "create_additional_policy": false, + "description": "create template_104", + "name": "template_104", + "priority": 104 + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], - "create_policy_no_deploy_104" : [ - { - "create_additional_policy": false, - "description": "create template_104", - "name": "template_104", - "priority": 104 - }, - { - "switch": [ - { - "ip": "10.10.10.224" + "create_policy_wrong_state_104": [ + { + "create_additional_policy": false, + "description": "create template_104", + "name": "template_104", + "priority": 104 + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], - "create_policy_wrong_state_104" : [ - { - "create_additional_policy": false, - "description": "create template_104", - "name": "template_104", - "priority": 104 - }, - { - "switch": [ - { - "ip": "10.10.10.224" + "delete_policy_template_name_101_105": [ + { + "name": "template_101" + }, + { + "name": "template_102" + }, + { + "name": "template_103" + }, + { + "name": "template_104" + }, + { + "name": "template_105" + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], - - "delete_policy_template_name_101_105" : [ - { - "name": "template_101" - }, - { - "name": "template_102" - }, - { - "name": "template_103" - }, - { - "name": "template_104" - }, - { - "name": "template_105" - }, - { - "switch": [ - { - "ip": "10.10.10.224" + ], + "delete_policy_template_desc_101_105": [ + { + "name": "template_101", + "description": "policy101" + }, + { + "name": "template_102", + "description": "policy102" + }, + { + "name": "template_103", + "description": "policy103" + }, + { + "name": "template_104", + "description": "policy104" + }, + { + "name": "template_105", + "description": "policy105" + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], - "delete_policy_policy_id_101_105" : [ - { - "name": "POLICY-101101" - }, - { - "name": "POLICY-102102" - }, - { - "name": "POLICY-103103" - }, - { - "name": "POLICY-104104" - }, - { - "name": "POLICY-105105" - }, - { - "switch": [ - { - "ip": "10.10.10.224" + "delete_policy_policy_id_101_105": [ + { + "name": "POLICY-101101" + }, + { + "name": "POLICY-102102" + }, + { + "name": "POLICY-103103" + }, + { + "name": "POLICY-104104" + }, + { + "name": "POLICY-105105" + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], - "delete_policy_template_name_multi" : [ - { - "name": "template_101" - }, - { - "name": "template_102" - }, - { - "name": "template_103" - }, - { - "name": "template_104" - }, - { - "name": "template_105" - }, - { - "switch": [ - { - "ip": "10.10.10.224" + "delete_policy_template_name_multi": [ + { + "name": "template_101" + }, + { + "name": "template_102" + }, + { + "name": "template_103" + }, + { + "name": "template_104" + }, + { + "name": "template_105" + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], - "query_policy_with_switch_info" : [ - { - "switch": [ + "query_policy_with_switch_info": [ { - "ip": "10.10.10.224" + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], - "query_policy_with_policy_id" : [ - { - "name": "POLICY-101101" - }, - { - "name": "POLICY-102102" - }, - { - "name": "POLICY-103103" - }, - { - "name": "POLICY-104104" - }, - { - "name": "POLICY-105105" - }, - { - "switch": [ - { - "ip": "10.10.10.224" + "query_policy_with_policy_id": [ + { + "name": "POLICY-101101" + }, + { + "name": "POLICY-102102" + }, + { + "name": "POLICY-103103" + }, + { + "name": "POLICY-104104" + }, + { + "name": "POLICY-105105" + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }], + ], - "query_policy_with_template_name" : [ - { - "name": "template_101" - }, - { - "name": "template_102" - }, - { - "name": "template_103" - }, - { - "name": "template_104" - }, - { - "name": "template_105" - }, - { - "switch": [ - { - "ip": "10.10.10.224" + "query_policy_with_template_name": [ + { + "name": "template_101" + }, + { + "name": "template_102" + }, + { + "name": "template_103" + }, + { + "name": "template_104" + }, + { + "name": "template_105" + }, + { + "switch": [ + { + "ip": "10.10.10.224" + } + ] } - ] - }] + ] } diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_policy_payloads.json b/tests/unit/modules/dcnm/fixtures/dcnm_policy_payloads.json index 81c4a51ff..d954ded59 100644 --- a/tests/unit/modules/dcnm/fixtures/dcnm_policy_payloads.json +++ b/tests/unit/modules/dcnm/fixtures/dcnm_policy_payloads.json @@ -639,182 +639,183 @@ "REQUEST_PATH": "https://10.64.78.151:443/rest/control/policies/switches?serialNumber=XYZKSJHSMK1", "MESSAGE": "OK", "DATA": [ - { - "id": 101101, - "policyId": "POLICY-101101", - "description": "", - "serialNumber": "XYZKSJHSMK1", - "entityType": "SWITCH", - "entityName": "SWITCH", - "templateName": "template_101", - "templateContentType": "TEMPLATE_CLI", - "nvPairs": { - "FABRIC_NAME": "mmudigon" - }, - "autoGenerated": false, - "deleted": false, - "source": "", - "priority": 101, - "status": "NA", - "statusOn": 1605693124239, - "createdOn": 1605693124239, - "modifiedOn": 1605693124239, - "fabricName": "mmudigon" - }, - { - "id": 101201, - "policyId": "POLICY-101201", - "description": "", - "serialNumber": "XYZKSJHSMK1", - "entityType": "SWITCH", - "entityName": "SWITCH", - "templateName": "template_101", - "templateContentType": "TEMPLATE_CLI", - "nvPairs": { - "FABRIC_NAME": "mmudigon" - }, - "autoGenerated": false, - "deleted": false, - "source": "", - "priority": 101, - "status": "NA", - "statusOn": 1605693124239, - "createdOn": 1605693124239, - "modifiedOn": 1605693124239, - "fabricName": "mmudigon" - }, - { - "id": 101301, - "policyId": "POLICY-101301", - "description": "", - "serialNumber": "XYZKSJHSMK1", - "entityType": "SWITCH", - "entityName": "SWITCH", - "templateName": "template_101", - "templateContentType": "TEMPLATE_CLI", - "nvPairs": { - "FABRIC_NAME": "mmudigon" - }, - "autoGenerated": false, - "deleted": false, - "source": "", - "priority": 101, - "status": "NA", - "statusOn": 1605693124239, - "createdOn": 1605693124239, - "modifiedOn": 1605693124239, - "fabricName": "mmudigon" - }, - { - "id": 102102, - "policyId": "POLICY-102102", - "description": "102 - No piority given", - "serialNumber": "XYZKSJHSMK1", - "entityType": "SWITCH", - "entityName": "SWITCH", - "templateName": "template_102", - "templateContentType": "TEMPLATE_CLI", - "nvPairs": { - "FABRIC_NAME": "mmudigon" - }, - "autoGenerated": false, - "deleted": false, - "source": "", - "priority": 500, - "status": "NA", - "statusOn": 1605693124377, - "createdOn": 1605693124377, - "modifiedOn": 1605693124377, - "fabricName": "mmudigon" - }, - { - "id": 102202, - "policyId": "POLICY-102202", - "description": "102 - No piority given", - "serialNumber": "XYZKSJHSMK1", - "entityType": "SWITCH", - "entityName": "SWITCH", - "templateName": "template_102", - "templateContentType": "TEMPLATE_CLI", - "nvPairs": { - "FABRIC_NAME": "mmudigon" - }, - "autoGenerated": false, - "deleted": false, - "source": "", - "priority": 500, - "status": "NA", - "statusOn": 1605693124377, - "createdOn": 1605693124377, - "modifiedOn": 1605693124377, - "fabricName": "mmudigon" - }, - { - "id": 103103, - "policyId": "POLICY-103103", - "description": "Both description and priority given", - "serialNumber": "XYZKSJHSMK1", - "entityType": "SWITCH", - "entityName": "SWITCH", - "templateName": "template_103", - "templateContentType": "TEMPLATE_CLI", - "nvPairs": { - "FABRIC_NAME": "mmudigon" - }, - "autoGenerated": false, - "deleted": false, - "source": "", - "priority": 500, - "status": "NA", - "statusOn": 1605693124502, - "createdOn": 1605693124502, - "modifiedOn": 1605693124502, - "fabricName": "mmudigon" - }, - { - "id": 104104, - "policyId": "POLICY-104104", - "description": "", - "serialNumber": "XYZKSJHSMK1", - "entityType": "SWITCH", - "entityName": "SWITCH", - "templateName": "template_104", - "templateContentType": "TEMPLATE_CLI", - "nvPairs": { - "FABRIC_NAME": "mmudigon" - }, - "autoGenerated": false, - "deleted": false, - "source": "", - "priority": 500, - "status": "NA", - "statusOn": 1605693124617, - "createdOn": 1605693124617, - "modifiedOn": 1605693124617, - "fabricName": "mmudigon" - }, - { - "id": 105105, - "policyId": "POLICY-105105", - "description": "", - "serialNumber": "XYZKSJHSMK1", - "entityType": "SWITCH", - "entityName": "SWITCH", - "templateName": "template_105", - "templateContentType": "TEMPLATE_CLI", - "nvPairs": { - "FABRIC_NAME": "mmudigon" - }, - "autoGenerated": false, - "deleted": false, - "source": "", - "priority": 500, - "status": "NA", - "statusOn": 1605693124733, - "createdOn": 1605693124733, - "modifiedOn": 1605693124733, - "fabricName": "mmudigon" - }] + { + "id": 101101, + "policyId": "POLICY-101101", + "description": "policy101", + "serialNumber": "XYZKSJHSMK1", + "entityType": "SWITCH", + "entityName": "SWITCH", + "templateName": "template_101", + "templateContentType": "TEMPLATE_CLI", + "nvPairs": { + "FABRIC_NAME": "mmudigon" + }, + "autoGenerated": false, + "deleted": false, + "source": "", + "priority": 101, + "status": "NA", + "statusOn": 1605693124239, + "createdOn": 1605693124239, + "modifiedOn": 1605693124239, + "fabricName": "mmudigon" + }, + { + "id": 101201, + "policyId": "POLICY-101201", + "description": "", + "serialNumber": "XYZKSJHSMK1", + "entityType": "SWITCH", + "entityName": "SWITCH", + "templateName": "template_101", + "templateContentType": "TEMPLATE_CLI", + "nvPairs": { + "FABRIC_NAME": "mmudigon" + }, + "autoGenerated": false, + "deleted": false, + "source": "", + "priority": 101, + "status": "NA", + "statusOn": 1605693124239, + "createdOn": 1605693124239, + "modifiedOn": 1605693124239, + "fabricName": "mmudigon" + }, + { + "id": 101301, + "policyId": "POLICY-101301", + "description": "", + "serialNumber": "XYZKSJHSMK1", + "entityType": "SWITCH", + "entityName": "SWITCH", + "templateName": "template_101", + "templateContentType": "TEMPLATE_CLI", + "nvPairs": { + "FABRIC_NAME": "mmudigon" + }, + "autoGenerated": false, + "deleted": false, + "source": "", + "priority": 101, + "status": "NA", + "statusOn": 1605693124239, + "createdOn": 1605693124239, + "modifiedOn": 1605693124239, + "fabricName": "mmudigon" + }, + { + "id": 102102, + "policyId": "POLICY-102102", + "description": "102 - No piority given", + "serialNumber": "XYZKSJHSMK1", + "entityType": "SWITCH", + "entityName": "SWITCH", + "templateName": "template_102", + "templateContentType": "TEMPLATE_CLI", + "nvPairs": { + "FABRIC_NAME": "mmudigon" + }, + "autoGenerated": false, + "deleted": false, + "source": "", + "priority": 500, + "status": "NA", + "statusOn": 1605693124377, + "createdOn": 1605693124377, + "modifiedOn": 1605693124377, + "fabricName": "mmudigon" + }, + { + "id": 102202, + "policyId": "POLICY-102202", + "description": "102 - No piority given", + "serialNumber": "XYZKSJHSMK1", + "entityType": "SWITCH", + "entityName": "SWITCH", + "templateName": "template_102", + "templateContentType": "TEMPLATE_CLI", + "nvPairs": { + "FABRIC_NAME": "mmudigon" + }, + "autoGenerated": false, + "deleted": false, + "source": "", + "priority": 500, + "status": "NA", + "statusOn": 1605693124377, + "createdOn": 1605693124377, + "modifiedOn": 1605693124377, + "fabricName": "mmudigon" + }, + { + "id": 103103, + "policyId": "POLICY-103103", + "description": "Both description and priority given", + "serialNumber": "XYZKSJHSMK1", + "entityType": "SWITCH", + "entityName": "SWITCH", + "templateName": "template_103", + "templateContentType": "TEMPLATE_CLI", + "nvPairs": { + "FABRIC_NAME": "mmudigon" + }, + "autoGenerated": false, + "deleted": false, + "source": "", + "priority": 500, + "status": "NA", + "statusOn": 1605693124502, + "createdOn": 1605693124502, + "modifiedOn": 1605693124502, + "fabricName": "mmudigon" + }, + { + "id": 104104, + "policyId": "POLICY-104104", + "description": "policy104", + "serialNumber": "XYZKSJHSMK1", + "entityType": "SWITCH", + "entityName": "SWITCH", + "templateName": "template_104", + "templateContentType": "TEMPLATE_CLI", + "nvPairs": { + "FABRIC_NAME": "mmudigon" + }, + "autoGenerated": false, + "deleted": false, + "source": "", + "priority": 500, + "status": "NA", + "statusOn": 1605693124617, + "createdOn": 1605693124617, + "modifiedOn": 1605693124617, + "fabricName": "mmudigon" + }, + { + "id": 105105, + "policyId": "POLICY-105105", + "description": "policy105", + "serialNumber": "XYZKSJHSMK1", + "entityType": "SWITCH", + "entityName": "SWITCH", + "templateName": "template_105", + "templateContentType": "TEMPLATE_CLI", + "nvPairs": { + "FABRIC_NAME": "mmudigon" + }, + "autoGenerated": false, + "deleted": false, + "source": "", + "priority": 500, + "status": "NA", + "statusOn": 1605693124733, + "createdOn": 1605693124733, + "modifiedOn": 1605693124733, + "fabricName": "mmudigon" + } + ] }, "have_response_101_101_5" : { "RETURN_CODE": 200, @@ -942,7 +943,7 @@ { "id": 101101, "policyId": "POLICY-101101", - "description": "", + "description": "policy101", "serialNumber": "XYZKSJHSMK1", "entityType": "SWITCH", "entityName": "SWITCH", @@ -964,7 +965,7 @@ { "id": 102102, "policyId": "POLICY-102102", - "description": "102 - No piority given", + "description": "policy102", "serialNumber": "XYZKSJHSMK1", "entityType": "SWITCH", "entityName": "SWITCH", @@ -986,7 +987,7 @@ { "id": 103103, "policyId": "POLICY-103103", - "description": "Both description and priority given", + "description": "policy103", "serialNumber": "XYZKSJHSMK1", "entityType": "SWITCH", "entityName": "SWITCH", @@ -1008,7 +1009,7 @@ { "id": 104104, "policyId": "POLICY-104104", - "description": "", + "description": "policy104", "serialNumber": "XYZKSJHSMK1", "entityType": "SWITCH", "entityName": "SWITCH", @@ -1030,7 +1031,7 @@ { "id": 105105, "policyId": "POLICY-105105", - "description": "", + "description": "policy105", "serialNumber": "XYZKSJHSMK1", "entityType": "SWITCH", "entityName": "SWITCH", @@ -1204,7 +1205,7 @@ { "id": 123810, "policyId": "POLICY-123810", - "description": "", + "description": "policy101", "serialNumber": "XYZKSJHSMK1", "entityType": "SWITCH", "entityName": "SWITCH", @@ -1226,7 +1227,7 @@ { "id": 123820, "policyId": "POLICY-123820", - "description": "102 - No piority given", + "description": "policy102", "serialNumber": "XYZKSJHSMK1", "entityType": "SWITCH", "entityName": "SWITCH", @@ -1248,7 +1249,7 @@ { "id": 123830, "policyId": "POLICY-123830", - "description": "Both description and priority given", + "description": "policy103", "serialNumber": "XYZKSJHSMK1", "entityType": "SWITCH", "entityName": "SWITCH", diff --git a/tests/unit/modules/dcnm/test_dcnm_policy.py b/tests/unit/modules/dcnm/test_dcnm_policy.py index 8715db19f..e4b6a7a40 100644 --- a/tests/unit/modules/dcnm/test_dcnm_policy.py +++ b/tests/unit/modules/dcnm/test_dcnm_policy.py @@ -163,6 +163,23 @@ def load_policy_fixtures(self): deploy_succ_resp, ] + if ( + "test_dcnm_policy_merged_existing_and_non_exist_desc_as_key" + == self._testMethodName + ): + + have_101_103_resp = self.payloads_data.get("have_response_101_103") + create_succ_resp4 = self.payloads_data.get("success_create_response_104") + create_succ_resp5 = self.payloads_data.get("success_create_response_105") + deploy_succ_resp = self.payloads_data.get("success_deploy_response_101_105") + + self.run_dcnm_send.side_effect = [ + have_101_103_resp, + create_succ_resp4, + create_succ_resp5, + deploy_succ_resp, + ] + if "test_dcnm_policy_without_state" == self._testMethodName: create_succ_resp4 = self.payloads_data.get("success_create_response_104") @@ -316,6 +333,26 @@ def load_policy_fixtures(self): deploy_succ_resp, ] + if ( + "test_dcnm_policy_merged_existing_different_template_desc_as_key" + == self._testMethodName + ): + have_all_resp = self.payloads_data.get("have_response_101_105") + create_succ_resp_101 = self.payloads_data.get("success_create_response_101") + create_succ_resp_102 = self.payloads_data.get("success_create_response_102") + get_resp_101 = self.payloads_data.get("get_response_101") + get_resp_102 = self.payloads_data.get("get_response_102") + mark_delete_resp_101 = self.payloads_data.get("mark_delete_response_101") + mark_delete_resp_102 = self.payloads_data.get("mark_delete_response_102") + self.run_dcnm_send.side_effect = [ + have_all_resp, + get_resp_101, + get_resp_102, + mark_delete_resp_101, + mark_delete_resp_102, + create_succ_resp_101, + create_succ_resp_102, + ] if "test_dcnm_policy_modify_with_policy_id" == self._testMethodName: create_succ_resp4 = self.payloads_data.get("success_create_response_104") @@ -450,6 +487,30 @@ def load_policy_fixtures(self): [], [], ] + if "test_dcnm_policy_delete_with_desc_as_key" == self._testMethodName: + + have_resp_101_105_multi = self.payloads_data.get( + "have_response_101_105_multi" + ) + mark_delete_resp_101 = self.payloads_data.get("mark_delete_response_101") + mark_delete_resp_104 = self.payloads_data.get("mark_delete_response_104") + mark_delete_resp_105 = self.payloads_data.get("mark_delete_response_105") + get_response_101 = self.payloads_data.get("get_response_101") + get_response_104 = self.payloads_data.get("get_response_104") + get_response_105 = self.payloads_data.get("get_response_105") + delete_config_save_resp = self.payloads_data.get( + "delete_config_deploy_response_101_105" + ) + + self.run_dcnm_send.side_effect = [ + have_resp_101_105_multi, + mark_delete_resp_101, + mark_delete_resp_104, + mark_delete_resp_105, + [], + [], + [], + ] if ( "test_dcnm_policy_delete_with_template_name_with_second_delete" @@ -821,6 +882,60 @@ def test_dcnm_policy_merged_existing_and_non_exist(self): ) count = count + 1 + def test_dcnm_policy_merged_existing_and_non_exist_desc_as_key(self): + + self.config_data = loadPlaybookData("dcnm_policy_configs") + self.payloads_data = loadPlaybookData("dcnm_policy_payloads") + + # get mock ip_sn and fabric_inventory_details + self.mock_fab_inv = self.payloads_data.get("mock_fab_inv") + self.mock_ip_sn = self.payloads_data.get("mock_ip_sn") + + # load required config data + self.playbook_config = self.config_data.get("create_policy_101_105") + + set_module_args( + dict( + state="merged", + deploy=True, + fabric="mmudigon", + use_desc_as_key=True, + config=self.playbook_config, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertEqual(len(result["diff"][0]["merged"]), 2) + self.assertEqual(len(result["diff"][0]["deleted"]), 0) + self.assertEqual(len(result["diff"][0]["query"]), 0) + self.assertEqual(len(result["diff"][0]["deploy"]), 5) + + def test_dcnm_policy_merged_existing_different_template_desc_as_key(self): + + self.config_data = loadPlaybookData("dcnm_policy_configs") + self.payloads_data = loadPlaybookData("dcnm_policy_payloads") + + # get mock ip_sn and fabric_inventory_details + self.mock_fab_inv = self.payloads_data.get("mock_fab_inv") + self.mock_ip_sn = self.payloads_data.get("mock_ip_sn") + + # load required config data + self.playbook_config = self.config_data.get("modify_policy_101_102") + + set_module_args( + dict( + state="merged", + deploy=False, + fabric="mmudigon", + use_desc_as_key=True, + config=self.playbook_config, + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertEqual(len(result["diff"][0]["merged"]), 2) + self.assertEqual(len(result["diff"][0]["deleted"]), 2) + self.assertEqual(len(result["diff"][0]["query"]), 0) + self.assertEqual(len(result["diff"][0]["deploy"]), 0) + def test_dcnm_policy_without_state(self): # load the json from playbooks @@ -1360,6 +1475,38 @@ def test_dcnm_policy_delete_with_template_name(self): ) count = count + 1 + def test_dcnm_policy_delete_with_desc_as_key(self): + + # load the json from playbooks + self.config_data = loadPlaybookData("dcnm_policy_configs") + self.payloads_data = loadPlaybookData("dcnm_policy_payloads") + + # get mock ip_sn and fabric_inventory_details + self.mock_fab_inv = self.payloads_data.get("mock_fab_inv") + self.mock_ip_sn = self.payloads_data.get("mock_ip_sn") + + # load required config data + self.playbook_config = self.config_data.get( + "delete_policy_template_desc_101_105" + ) + + set_module_args( + dict( + state="deleted", + deploy=False, + fabric="mmudigon", + use_desc_as_key=True, + config=self.playbook_config, + ) + ) + result = self.execute_module(changed=True, failed=False) + + self.assertEqual(len(result["diff"][0]["merged"]), 0) + self.assertEqual(len(result["diff"][0]["deleted"]), 3) + self.assertEqual(len(result["diff"][0]["query"]), 0) + self.assertEqual(len(result["diff"][0]["deploy"]), 0) + self.assertEqual(len(result["diff"][0]["skipped"]), 0) + def test_dcnm_policy_delete_with_policy_id(self): # load the json from playbooks From 33035b6ae31e7358a40483e06dda1919a064691a Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 26 Jun 2024 06:45:34 -1000 Subject: [PATCH 2/6] dcnm_fabric: Add support for IPFM fabrics (ready for review) (#294) * ApiEndpoints(): endpoints for common controller operations * ControllerFeatures(): Retrieve feature information from the controller * 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" * 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. * Remove debug message * FabricDelete().register_result(): fabric_name needs to be upper-case * dcnm_fabric IT: Add dcnm_fabric_merged_basic_ipfm * dcnm_fabric IT: Add dcnm_tests.yaml with approprate vars Includes all vars required for the test cases listed. * dcnm_fabric IT: Add notes regarding controller config * Update unit tests to reflect addition of IPFM fabric type * 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. * 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. * dcnm_fabric IT: Add dcnm_fabric_replaced_save_deploy_ipfm Also, update comments in other IT regarding nxos credentials. * ControllerFeatures(): run thru black and isort * Run api endpoint classes thru black, isort, pylint * ControllerFeatures(): Add unit tests, 100% coverage * dcnm_fabric: Update docs with IPFM fabric parameters * dcnm_fabric: fix PEP8 and doc errors * Add EXTRA_CONF_LEAF param in EXAMPLES section Just to make the example a bit more interesting... * 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 * Subclasses can define mandatory properties, more Fabrics(): add path property FabricsDelete(): new class for fabric delete endpoint FabricsDetails(): inherit path property from Fabrics() * Rename classes and files * 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 * Consistent docstring structure. 1. Add Endpoint section to all docstrings. 2. Modify all previously-unmodified docstrings for consistency. 3. Run thru black, isort, pylint. * Rename v1_common (V1Common) to common_v1 (CommonV1) * 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() * dcnm_endpoints: Add ImageMgmt endpoints /api/v1/imagemanagement/rest/imagemgnt - ImageMgmt() - EpBootFlashInfo() * image_mgmt.py rename to image_mgnt.py to parallel NDFC * Rename staging_management classes EpStageImage() -> EpImageStage() EpValidateImage() -> EpImageValidate() * dcnm_endpoints: Add policy_mgnt endpoint classes * 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. * 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. * dcnm_endpoints: docstring consistency across classes * 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 * dcnm_endpoints: Add configtemplate endpoints + UT * Fix PEP8 issues, import error test_controller_features.py was trying to import the old name, Features, for renamed class EpFeatures. * Fix PEP8 no line at end of file, and f-string issue * Fabrics().EpFabrics() new endpoint Also modify all usage examples to use "instance" for the instantiated class name. * 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. * FabricDetails(): run through black, isort, pylint * 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. * 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 * 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() * 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. * 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. * 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 * 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 * 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. * Fix empy-init errors * 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. * 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. * 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. * FabricUpdateCommon(): Use EpFabricUpdate() 1. FabricUpdateCommon(): use EpFabricUpdate() for endpoint resolution. 2. test_fabric_updatee_bulk.py: Update unit tests to reflect 1 above. * 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 * 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. * dcnm_fabric: IPFM, update FabricTypes() unit tests for IPFM. * 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. * FabricReplacedCommon().update_replaced_payload(): Further logic simplification * FabricReplacedCommon().update_replaced_payload(): docstring update Modify the docstring to remove mention of raising ValueError since this method no longer raises ValueError. * EpFabricConfigDeploy(): add switch_id property This will be useful for the dcnm_maintenance_mode module. - Add switch_id property - Update docstrings * 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/* --- docs/cisco.dcnm.dcnm_fabric_module.rst | 1156 ++++++++++++++++- playbooks/roles/dcnm_fabric/dcnm_tests.yaml | 62 +- plugins/module_utils/common/api/__init__.py | 0 plugins/module_utils/common/api/api.py | 65 + .../module_utils/common/api/v1/__init__.py | 0 .../common/api/v1/configtemplate/__init__.py | 0 .../api/v1/configtemplate/configtemplate.py | 42 + .../api/v1/configtemplate/rest/__init__.py | 0 .../v1/configtemplate/rest/config/__init__.py | 0 .../v1/configtemplate/rest/config/config.py | 49 + .../rest/config/templates/__init__.py | 0 .../rest/config/templates/templates.py | 193 +++ .../common/api/v1/configtemplate/rest/rest.py | 49 + .../module_utils/common/api/v1/fm/__init__.py | 0 plugins/module_utils/common/api/v1/fm/fm.py | 128 ++ .../common/api/v1/imagemanagement/__init__.py | 0 .../api/v1/imagemanagement/imagemanagement.py | 42 + .../api/v1/imagemanagement/rest/__init__.py | 0 .../rest/imagemgnt/__init__.py | 0 .../rest/imagemgnt/imagemgnt.py | 85 ++ .../rest/imageupgrade/__init__.py | 0 .../rest/imageupgrade/imageupgrade.py | 151 +++ .../rest/policymgnt/__init__.py | 0 .../rest/policymgnt/policymgnt.py | 336 +++++ .../api/v1/imagemanagement/rest/rest.py | 49 + .../rest/stagingmanagement/__init__.py | 0 .../stagingmanagement/stagingmanagement.py | 179 +++ .../common/api/v1/lan_fabric/__init__.py | 0 .../common/api/v1/lan_fabric/lan_fabric.py | 42 + .../common/api/v1/lan_fabric/rest/__init__.py | 0 .../v1/lan_fabric/rest/control/__init__.py | 0 .../api/v1/lan_fabric/rest/control/control.py | 43 + .../rest/control/fabrics/__init__.py | 0 .../rest/control/fabrics/fabrics.py | 717 ++++++++++ .../rest/control/switches/__init__.py | 0 .../rest/control/switches/switches.py | 141 ++ .../common/api/v1/lan_fabric/rest/rest.py | 43 + plugins/module_utils/common/api/v1/v1.py | 41 + .../common/controller_features.py | 324 +++++ .../module_utils/common/controller_version.py | 35 +- plugins/module_utils/fabric/config_deploy.py | 44 +- plugins/module_utils/fabric/config_save.py | 44 +- plugins/module_utils/fabric/create.py | 25 +- plugins/module_utils/fabric/delete.py | 19 +- plugins/module_utils/fabric/endpoints.py | 300 ----- plugins/module_utils/fabric/fabric_details.py | 12 +- plugins/module_utils/fabric/fabric_summary.py | 12 +- plugins/module_utils/fabric/fabric_types.py | 34 + plugins/module_utils/fabric/replaced.py | 45 +- plugins/module_utils/fabric/template_get.py | 54 +- .../module_utils/fabric/template_get_all.py | 67 +- plugins/module_utils/fabric/update.py | 20 +- plugins/modules/dcnm_fabric.py | 464 ++++++- .../tests/dcnm_fabric_deleted_basic_ipfm.yaml | 250 ++++ .../tests/dcnm_fabric_merged_basic_ipfm.yaml | 409 ++++++ .../tests/dcnm_fabric_merged_save_deploy.yaml | 13 +- .../dcnm_fabric_merged_save_deploy_ipfm.yaml | 470 +++++++ .../tests/dcnm_fabric_replaced_basic.yaml | 2 +- .../dcnm_fabric_replaced_basic_ipfm.yaml | 413 ++++++ .../dcnm_fabric_replaced_save_deploy.yaml | 5 +- ...dcnm_fabric_replaced_save_deploy_ipfm.yaml | 471 +++++++ .../unit/module_utils/common/api/__init__.py | 0 .../common/api/test_v1_api_fabrics.py | 609 +++++++++ .../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 + .../common/api/test_v1_api_switches.py | 79 ++ .../common/api/test_v1_api_templates.py | 93 ++ .../unit/module_utils/common/common_utils.py | 65 +- .../responses_ControllerFeatures.json | 632 +++++++++ .../common/test_controller_features.py | 345 +++++ .../common/test_controller_version.py | 1 - .../fixtures/payloads_FabricCreateCommon.json | 27 + .../dcnm/dcnm_fabric/test_endpoints.py | 544 -------- .../dcnm/dcnm_fabric/test_fabric_common.py | 2 +- .../dcnm_fabric/test_fabric_config_deploy.py | 62 +- .../dcnm_fabric/test_fabric_config_save.py | 57 +- .../dcnm_fabric/test_fabric_create_common.py | 182 ++- .../dcnm/dcnm_fabric/test_fabric_delete.py | 44 +- .../dcnm/dcnm_fabric/test_fabric_details.py | 6 +- .../test_fabric_details_by_name.py | 8 +- .../test_fabric_details_by_nv_pair.py | 6 +- .../dcnm_fabric/test_fabric_replaced_bulk.py | 14 +- .../dcnm/dcnm_fabric/test_fabric_summary.py | 33 +- .../dcnm/dcnm_fabric/test_fabric_types.py | 9 +- .../dcnm_fabric/test_fabric_update_bulk.py | 29 +- .../dcnm/dcnm_fabric/test_template_get.py | 40 +- .../dcnm/dcnm_fabric/test_template_get_all.py | 60 +- 89 files changed, 9042 insertions(+), 1338 deletions(-) create mode 100644 plugins/module_utils/common/api/__init__.py create mode 100644 plugins/module_utils/common/api/api.py create mode 100644 plugins/module_utils/common/api/v1/__init__.py create mode 100644 plugins/module_utils/common/api/v1/configtemplate/__init__.py create mode 100644 plugins/module_utils/common/api/v1/configtemplate/configtemplate.py 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 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/__init__.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/__init__.py create mode 100644 plugins/module_utils/common/api/v1/fm/fm.py create mode 100644 plugins/module_utils/common/api/v1/imagemanagement/__init__.py create mode 100644 plugins/module_utils/common/api/v1/imagemanagement/imagemanagement.py create mode 100644 plugins/module_utils/common/api/v1/imagemanagement/rest/__init__.py create mode 100644 plugins/module_utils/common/api/v1/imagemanagement/rest/imagemgnt/__init__.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/__init__.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/__init__.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/__init__.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/__init__.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/__init__.py 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/control.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/fabrics/fabrics.py create mode 100644 plugins/module_utils/common/api/v1/lan_fabric/rest/control/switches/__init__.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/v1.py create mode 100644 plugins/module_utils/common/controller_features.py delete mode 100644 plugins/module_utils/fabric/endpoints.py create mode 100644 tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_deleted_basic_ipfm.yaml create mode 100644 tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_basic_ipfm.yaml create mode 100644 tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy_ipfm.yaml create mode 100644 tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_basic_ipfm.yaml create mode 100644 tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_replaced_save_deploy_ipfm.yaml create mode 100644 tests/unit/module_utils/common/api/__init__.py create mode 100644 tests/unit/module_utils/common/api/test_v1_api_fabrics.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 create mode 100644 tests/unit/module_utils/common/api/test_v1_api_switches.py create mode 100644 tests/unit/module_utils/common/api/test_v1_api_templates.py create mode 100644 tests/unit/module_utils/common/fixtures/responses_ControllerFeatures.json create mode 100644 tests/unit/module_utils/common/test_controller_features.py delete mode 100644 tests/unit/modules/dcnm/dcnm_fabric/test_endpoints.py diff --git a/docs/cisco.dcnm.dcnm_fabric_module.rst b/docs/cisco.dcnm.dcnm_fabric_module.rst index 8dc3e27eb..8c012612c 100644 --- a/docs/cisco.dcnm.dcnm_fabric_module.rst +++ b/docs/cisco.dcnm.dcnm_fabric_module.rst @@ -112,11 +112,1163 @@ 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 +
+ + + + + +
Enable only, when IP Authorization is enabled in the AAA Server
+ + + + + + +
+ AAA_SERVER_CONF + +
+ string +
+ + + Default:
""
+ + +
AAA Configurations
+ + + + + + +
+ ASM_GROUP_RANGES
list - / elements=dictionary + / elements=string +
+ + + 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 +
+ + + + + +
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 +
+ + + + + +
Enable CDP on management interface
+ + + + + + +
+ DHCP_ENABLE + +
+ boolean +
+ + + + + +
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 +
+ + + + + +
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 +
+ + + + + +
Include AAA configs from Manageability tab during device bootup
+ + + + + + +
+ ENABLE_ASM + +
+ boolean +
+ + + + + +
Enable groups with receivers sending (*,G) joins
+ + + + + + +
+ ENABLE_NBM_PASSIVE + +
+ boolean +
+ + + + + +
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 +
+ + + + + +
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 +
+ + + + + +
No description available
+ + + + + + +
+ ISIS_AUTH_ENABLE + +
+ boolean +
+ + + + + +
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 +
+ + + + + +
Supported IS types: level-1, level-2
+ + + + + + +
+ ISIS_P2P_ENABLE + +
+ boolean +
+ + + + + +
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 +
+ + + + + +
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 +
+ + + + + +
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 +
+ + + + + +
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 +
+ + + + + +
No description available
+ + + + + + +
+ PIM_HELLO_AUTH_KEY + +
+ string +
+ + + Default:
""
+ + +
3DES Encrypted
+ + + + + + +
+ PM_ENABLE + +
+ boolean +
+ + + + + +
No description available
+ + + + + + +
+ POWER_REDUNDANCY_MODE + +
+ string +
+ + + + + +
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 +
+ + + + + +
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 +
+ + + + + +
Configure NDFC as a receiver for SNMP traps
+ + + + + + +
+ STATIC_UNDERLAY_IP_ALLOC + +
+ boolean +
+ + + + + +
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 +
+ + + + + +
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/playbooks/roles/dcnm_fabric/dcnm_tests.yaml b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml index ec82ee92a..a3cc72d88 100644 --- a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml +++ b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml @@ -1,44 +1,44 @@ --- # 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 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_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_replaced_save_deploy_ipfm + 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_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 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/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 new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/module_utils/common/api/v1/configtemplate/__init__.py b/plugins/module_utils/common/api/v1/configtemplate/__init__.py new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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..e69de29bb 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 new file mode 100644 index 000000000..e69de29bb 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..d87433cb3 --- /dev/null +++ b/plugins/module_utils/common/api/v1/lan_fabric/rest/control/fabrics/fabrics.py @@ -0,0 +1,717 @@ +# 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 + or fabric_name + switch_id. + + ### 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}`` + - ``/fabrics/{fabric_name}/config-deploy/{switch_id}`` + - ``/fabrics/{fabric_name}/config-deploy/{switch_id}/?forceShowRun={force_show_run}`` + + ### Verb + - POST + + ### Parameters + - 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 + 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["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 + - Optional + """ + 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 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"] + + @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" + 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): + """ + ## 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..e69de29bb 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/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 new file mode 100644 index 000000000..ba87a9c59 --- /dev/null +++ b/plugins/module_utils/common/controller_features.py @@ -0,0 +1,324 @@ +""" +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 copy +import inspect +import logging + +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.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.api_features = EpFeatures() + self._init_properties() + + def _init_properties(self): + self.properties = {} + 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): + """ + - 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 refresh()." + raise ValueError(msg) + + 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 + # 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 + + 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 == {}: + 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) + + 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 response(self): + """ + Return the GET response from the Controller + """ + return self.properties.get("response") + + @property + def response_data(self): + """ + Return the data retrieved from the request + """ + return self.properties.get("response_data") + + @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.get("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 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/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/plugins/module_utils/fabric/config_deploy.py b/plugins/module_utils/fabric/config_deploy.py index c89299c92..7f0bd6e30 100644 --- a/plugins/module_utils/fabric/config_deploy.py +++ b/plugins/module_utils/fabric/config_deploy.py @@ -22,18 +22,12 @@ 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 from ansible_collections.cisco.dcnm.plugins.module_utils.common.conversion import \ 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 -from ansible_collections.cisco.dcnm.plugins.module_utils.fabric.endpoints import \ - ApiEndpoints class FabricConfigDeploy: @@ -91,7 +85,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 +248,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 @@ -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 eb45b563c..6e4b232ea 100644 --- a/plugins/module_utils/fabric/config_save.py +++ b/plugins/module_utils/fabric/config_save.py @@ -22,16 +22,10 @@ import logging from typing import Dict +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 -# 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 FabricConfigSave: @@ -87,7 +81,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 +156,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 @@ -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/plugins/module_utils/fabric/create.py b/plugins/module_utils/fabric/create.py index 1d1f785a2..cdf4cb43f 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.lan_fabric.rest.control.fabrics.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/plugins/module_utils/fabric/delete.py b/plugins/module_utils/fabric/delete.py index e802a2dc9..8958720ee 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.lan_fabric.rest.control.fabrics.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): """ @@ -289,7 +284,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) 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/module_utils/fabric/fabric_details.py b/plugins/module_utils/fabric/fabric_details.py index 3590c2d80..f7cfc6007 100644 --- a/plugins/module_utils/fabric/fabric_details.py +++ b/plugins/module_utils/fabric/fabric_details.py @@ -22,14 +22,14 @@ 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 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.fabric.endpoints import \ - ApiEndpoints class FabricDetails(FabricCommon): @@ -52,9 +52,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 +82,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/plugins/module_utils/fabric/fabric_summary.py b/plugins/module_utils/fabric/fabric_summary.py index c54a58808..7d8ae01c1 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.lan_fabric.rest.control.switches.switches 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/plugins/module_utils/fabric/fabric_types.py b/plugins/module_utils/fabric/fabric_types.py index 9cd8f9dfa..4592a1d3a 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["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._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): """ @@ -161,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()) diff --git a/plugins/module_utils/fabric/replaced.py b/plugins/module_utils/fabric/replaced.py index a6ddc8053..6bdc2d0f3 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.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 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() @@ -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 @@ -149,32 +148,17 @@ def update_replaced_payload(self, parameter, playbook, controller, default): payload_to_send_to_controller.update(result) ``` """ - raise_value_error = False 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 playbook is not None: - if playbook == controller: + if controller is None or 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 " - msg += f"parameter {parameter}, " - msg += f"playbook: {playbook}, " - msg += f"controller: {controller}, " - msg += f"default: {default}" - raise ValueError(msg) + return {parameter: default} + if playbook == controller: + return None + return {parameter: playbook} def _verify_value_types_for_comparison( self, fabric_name, parameter, user_value, controller_value, default_value @@ -484,26 +468,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/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/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/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/plugins/modules/dcnm_fabric.py b/plugins/modules/dcnm_fabric.py index 81b39ded3..c2a141815 100644 --- a/plugins/modules/dcnm_fabric.py +++ b/plugins/modules/dcnm_fabric.py @@ -1545,15 +1545,408 @@ - 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 + elements: str + 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 @@ -1851,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 @@ -1920,6 +2316,8 @@ from os import environ from ansible.module_utils.basic import AnsibleModule +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 @@ -1933,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 \ @@ -1980,7 +2376,8 @@ 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 = {} self._implemented_states = set() @@ -2049,6 +2446,31 @@ 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 = {} + 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 +2589,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 +2688,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 +2853,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 +2887,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() 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/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 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 new file mode 100644 index 000000000..d799f900c --- /dev/null +++ b/tests/integration/targets/dcnm_fabric/tests/dcnm_fabric_merged_save_deploy_ipfm.yaml @@ -0,0 +1,470 @@ +################################################################################ +# 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 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 +################################################################################ +# 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: "{{ nxos_username}}" + password: "{{ nxos_password }}" + 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 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_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/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 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_fabrics.py b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py new file mode 100644 index 000000000..5ed96bd84 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_v1_api_fabrics.py @@ -0,0 +1,609 @@ +# 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.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 \ + 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(): + """ + ### 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 + - 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 + + ### 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_00440(): + """ + ### 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_00450(): + """ + ### 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_00500(): + """ + ### 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_00540(): + """ + ### 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_00550(): + """ + ### 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_00600(): + """ + ### 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_00640(): + """ + ### 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_00650(): + """ + ### 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 + + +# 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_image_mgnt.py b/tests/unit/module_utils/common/api/test_v1_api_image_mgnt.py new file mode 100644 index 000000000..ab0785d15 --- /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.imagemanagement.rest.imagemgnt.imagemgnt 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..1e49fd61f --- /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.imagemanagement.rest.imageupgrade.imageupgrade 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..ff66de1b3 --- /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.imagemanagement.rest.policymgnt.policymgnt 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..8bb951c05 --- /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.imagemanagement.rest.stagingmanagement.stagingmanagement 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" 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..a654f846d --- /dev/null +++ b/tests/unit/module_utils/common/api/test_v1_api_switches.py @@ -0,0 +1,79 @@ +# 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.lan_fabric.rest.control.switches.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 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..bdedf18f9 --- /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.configtemplate.rest.config.templates.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" 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..0a932aba4 --- /dev/null +++ b/tests/unit/module_utils/common/test_controller_features.py @@ -0,0 +1,345 @@ +# 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.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, controller_features_fixture, + does_not_raise, params, responses_controller_features) + + +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, EpFeatures) + 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 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) 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_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_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_config_deploy.py b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_config_deploy.py index 66f0a3823..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 @@ -32,6 +32,8 @@ import inspect import pytest +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 from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send import \ @@ -40,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, @@ -76,7 +72,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: @@ -178,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( @@ -218,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( @@ -420,57 +416,33 @@ 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 MockEpFabricConfigDeploy: # 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_DCNM_SEND = "ansible_collections.cisco.dcnm.plugins." PATCH_DCNM_SEND += "module_utils.common.rest_send.dcnm_send" 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 +450,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 +461,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", MockEpFabricConfigDeploy()) instance.fabric_details = fabric_details_by_name instance.fabric_details.rest_send = RestSend(MockAnsibleModule()) instance.payload = payload @@ -499,6 +469,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() @@ -531,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 @@ -654,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..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 @@ -32,6 +32,8 @@ import inspect import pytest +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 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: @@ -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( @@ -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."} 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..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,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.lan_fabric.rest.control.fabrics.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() 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..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,12 +32,12 @@ import inspect import pytest +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 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" @@ -350,7 +352,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 @@ -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() 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..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,14 +32,14 @@ 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.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..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,14 +32,14 @@ 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.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..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,14 +32,14 @@ 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.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) 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..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 @@ -32,12 +32,12 @@ import inspect import pytest +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 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) @@ -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( 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..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,6 +32,8 @@ import inspect import pytest +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 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): 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..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 @@ -58,12 +58,13 @@ 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( "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()), 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) 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..176cdaae2 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_template_get.py @@ -32,14 +32,14 @@ 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 \ 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) @@ -58,9 +58,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 +70,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 +109,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 +388,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() 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..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 @@ -32,14 +32,14 @@ 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 \ 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) @@ -58,9 +58,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 +69,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 +108,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 +312,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 6275a19b2a41b1f7d427b6f3fac6c629faab355e Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Wed, 26 Jun 2024 11:42:53 -1000 Subject: [PATCH 3/6] Replaced(): Create fabrics if they do not exist. closes #301 (#303) * Replaced(): Create fabrics if they do not exist. Replaced(): If fabrics in the playbook config for replaced state do not exist, instantiate and configure Merged() in Replaced().send_need_replaced(), and call Merged().send_need_create(). * Fix PEP8 too-many-blank-lines * Replaced().send_need_replaced(): remove redundant line. The following line was called twice. Removed duplicate. self.merged.ansible_module = self.ansible_module * Fix merge breakage fabric_name and fabric_type were deleted in fixing a prior merge conflict. Adding them back. * Replaced().get_need(): changes for easier merge. Replaced().get_need(): Changes to make resolving merge conflict with dcnm_maintenance_mode branch easier. --- plugins/modules/dcnm_fabric.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/plugins/modules/dcnm_fabric.py b/plugins/modules/dcnm_fabric.py index c2a141815..30997ed50 100644 --- a/plugins/modules/dcnm_fabric.py +++ b/plugins/modules/dcnm_fabric.py @@ -2836,17 +2836,17 @@ def __init__(self, params): self.fabric_replaced = FabricReplacedBulk(self.params) self.fabric_summary = FabricSummary(self.params) self.fabric_types = FabricTypes() + self.merged = None + self.need_create = [] + self.need_replaced = [] self.template = TemplateGet() + self._implemented_states.add("replaced") msg = f"ENTERED Replaced.{method_name}: " msg += f"state: {self.state}, " msg += f"check_mode: {self.check_mode}" self.log.debug(msg) - self.need_replaced = [] - - self._implemented_states.add("replaced") - def get_need(self): """ Caller: commit() @@ -2860,8 +2860,11 @@ def get_need(self): fabric_name = want.get("FABRIC_NAME", None) fabric_type = want.get("FABRIC_TYPE", None) - # Skip fabrics that do not exist on the controller + # If fabrics do not exist on the controller, add them to + # need_create. These will be created by Merged() in + # Replaced.send_need_replaced() if fabric_name not in self.have.all_data: + self.need_create.append(want) continue if self.features[fabric_type] is False: @@ -2907,6 +2910,16 @@ def send_need_replaced(self) -> None: msg += f"{json_pretty(self.need_replaced)}" self.log.debug(msg) + if len(self.need_create) != 0: + self.merged = Merged(self.params) + self.merged.ansible_module = self.ansible_module + self.merged.rest_send = self.rest_send + self.merged.fabric_details.rest_send = self.rest_send + self.merged.fabric_summary.rest_send = self.rest_send + self.merged.results = self.results + self.merged.need_create = self.need_create + self.merged.send_need_create() + if len(self.need_replaced) == 0: msg = f"{self.class_name}.{method_name}: " msg += "No fabrics to update for replaced state." From cc922757141efb4b5485e5c5c6f52ad3a514d399 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Thu, 27 Jun 2024 07:25:13 -1000 Subject: [PATCH 4/6] Fix for issue #305 (#306) 1. FabricReplacedCommon().update_replaced_payload() If the playbook value for a parameter is None, then the only thing we need to ensure is that the controller value is set to the default value. If the default value is null, we change it to the empty string "" since NDFC throws a 500 error for null parameter values. 2. Update unit tests to reflect the above change. The following test was modified. Specifically two cases where playbook is None (PARAM_8 and PARAM_15). - test_fabric_replaced_bulk_00040 --- plugins/module_utils/fabric/replaced.py | 13 ++++++------- .../dcnm/dcnm_fabric/test_fabric_replaced_bulk.py | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/plugins/module_utils/fabric/replaced.py b/plugins/module_utils/fabric/replaced.py index 6bdc2d0f3..c317df080 100644 --- a/plugins/module_utils/fabric/replaced.py +++ b/plugins/module_utils/fabric/replaced.py @@ -149,13 +149,12 @@ def update_replaced_payload(self, parameter, playbook, controller, default): ``` """ if playbook is None: - if default is None: - return None - if controller == default: - return None - if controller is None or controller == "": - return None - return {parameter: default} + if controller != default: + if default is None: + # The controller prefers empty string over null. + return {parameter: ""} + return {parameter: default} + return None if playbook == controller: return None return {parameter: playbook} 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..2cb473afe 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 @@ -390,14 +390,14 @@ def test_fabric_replaced_bulk_00031( ("PARAM_5", "c", "c", "c", None), ("PARAM_6", None, "c", "c", None), ("PARAM_7", None, "b", "c", {"PARAM_7": "c"}), - ("PARAM_8", None, "b", None, None), + ("PARAM_8", None, "b", None, {"PARAM_8": ""}), ("PARAM_9", None, None, None, None), ("PARAM_10", "a", None, None, {"PARAM_10": "a"}), ("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), + ("PARAM_15", None, None, "c", {"PARAM_15": "c"}), ], ) def test_fabric_replaced_bulk_00040( From 71fb3a400bbddb36c55d01b0cf4d96f4547d8cbe Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 26 Jul 2024 05:22:03 -1000 Subject: [PATCH 5/6] dcnm_maintenance_mode: Ready for review (#296) * ApiEndpoints(): endpoints for common controller operations * ControllerFeatures(): Retrieve feature information from the controller * 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" * 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. * Remove debug message * FabricDelete().register_result(): fabric_name needs to be upper-case * dcnm_fabric IT: Add dcnm_fabric_merged_basic_ipfm * dcnm_fabric IT: Add dcnm_tests.yaml with approprate vars Includes all vars required for the test cases listed. * dcnm_fabric IT: Add notes regarding controller config * Update unit tests to reflect addition of IPFM fabric type * 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. * 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. * dcnm_fabric IT: Add dcnm_fabric_replaced_save_deploy_ipfm Also, update comments in other IT regarding nxos credentials. * ControllerFeatures(): run thru black and isort * Run api endpoint classes thru black, isort, pylint * ControllerFeatures(): Add unit tests, 100% coverage * dcnm_fabric: Update docs with IPFM fabric parameters * dcnm_fabric: fix PEP8 and doc errors * Add EXTRA_CONF_LEAF param in EXAMPLES section Just to make the example a bit more interesting... * 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 * Subclasses can define mandatory properties, more Fabrics(): add path property FabricsDelete(): new class for fabric delete endpoint FabricsDetails(): inherit path property from Fabrics() * Rename classes and files * 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 * Consistent docstring structure. 1. Add Endpoint section to all docstrings. 2. Modify all previously-unmodified docstrings for consistency. 3. Run thru black, isort, pylint. * Rename v1_common (V1Common) to common_v1 (CommonV1) * 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() * dcnm_endpoints: Add ImageMgmt endpoints /api/v1/imagemanagement/rest/imagemgnt - ImageMgmt() - EpBootFlashInfo() * image_mgmt.py rename to image_mgnt.py to parallel NDFC * Rename staging_management classes EpStageImage() -> EpImageStage() EpValidateImage() -> EpImageValidate() * dcnm_endpoints: Add policy_mgnt endpoint classes * 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. * 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. * dcnm_endpoints: docstring consistency across classes * 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 * dcnm_endpoints: Add configtemplate endpoints + UT * Fix PEP8 issues, import error test_controller_features.py was trying to import the old name, Features, for renamed class EpFeatures. * Fix PEP8 no line at end of file, and f-string issue * Fabrics().EpFabrics() new endpoint Also modify all usage examples to use "instance" for the instantiated class name. * 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. * FabricDetails(): run through black, isort, pylint * 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. * 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 * 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() * 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. * 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. * 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 * 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 * 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. * Fix empy-init errors * 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. * 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. * 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. * FabricUpdateCommon(): Use EpFabricUpdate() 1. FabricUpdateCommon(): use EpFabricUpdate() for endpoint resolution. 2. test_fabric_updatee_bulk.py: Update unit tests to reflect 1 above. * 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 * 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. * dcnm_fabric: IPFM, update FabricTypes() unit tests for IPFM. * 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. * FabricReplacedCommon().update_replaced_payload(): Further logic simplification * FabricReplacedCommon().update_replaced_payload(): docstring update Modify the docstring to remove mention of raising ValueError since this method no longer raises ValueError. * EpFabricConfigDeploy(): add switch_id property This will be useful for the dcnm_maintenance_mode module. - Add switch_id property - Update docstrings * 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/* * 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 * dcnm_maintenance_mode: Add to test/sanity/ignore-* * Fix PEP8 errors, more... 1. Query() was treating self.want as a dict instead of as a list. * 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 } ``` * 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) * SwitchDetails(): compare values using lower() * 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. * 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. * RestSend(): improve docstrings * Handle "inconsistent" and "migration" states. * Handle non-existent switch case. * 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. * 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. * 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. * 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 * 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. * 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. * 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. * 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. * SwitchDetails(): Fix PEP8 whitespace in blank line * api: Add unit tests Add unit tests for the following classes: - EpFabricConfigDeploy - EpFabrics - EpMaintenanceModeDisable - EpMaintenanceModeEnable - Fabrics * Fix PEP8 errors, more... Unit tests failed because I forgot to add modified api fabrics.py that the UT were testing :-( * 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. * 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. * 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. * MaintenanceMode: initial unit tests * test_maintenance_mode_00120: update, more... 1. test_maintenance_mode_00120: update missing checks 2. MaintenanceMode(): remove temporary debug statements * 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. * Fix PEP8 errors, more... 1. Fix PEP8 errors from the last commit. 2. Common().get_want(): Add try-except block around Want(). * 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. * Want().want: Fix type hint * 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 * 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() * 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 * 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(). * 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. * ResultHandler(): Update unit tests 1. Update unit tests to expect TypeError when input to result.setter and response.setter is not a dict. * RestSend() v2: Update usage sections of docstring * 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() * 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. * 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. * 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. * Log() v2: 96% unit test coverage * 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. * MaintenanceMode(): Add unit test test_maintenance_mode_00700: - Verify MaintenanceMode().change_system_mode() raises ``ValueError`` when ``EpMaintenanceModeEnable`` or ``EpMaintenanceModeDisable`` raise any of: - ``TypeError`` - ``ValueError`` * 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. * 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. * 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. * 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() * 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. * 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. * 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 * 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 * 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. * 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. * Fix several item assignments MaintenanceModeInfo(): In converting this class to use _var rather than properties["var"], I forgot a few occurances. Cleaned this up. * 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. * 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. * FabricDetails() v2: Fix PEP8 too many blank lines Also, add the following to elide no-member error reporting: # pylint: disable=no-member * 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. * 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. * 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 * 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. * 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. * 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. * 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. * Forgot to add the mocks in the last commit. Test cases were failing due to missing mocks. * MockSwitchDetails(): Remove pylint not-callable Missed this one in the last commit. * 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. * 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. * 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. * 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. * 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 * 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. * MockFabricDetailsByName: Mock exceptions only. 1. MockFabricDetailsByName(): Modified to mock ONLY the exceptions raised by MockFabricDetailsByName. It no longer duplicates the functionality of Sender(). * 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. * 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. * 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. * 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``. * 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`` * 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. * 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``. * 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``. * 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. * FabricDetails: 78% unit test coverage 1. test_fabric_details_by_name_v2_00000 Verify that refresh() raises ``ValueError`` if ``refresh_super()`` raises ``ValueError`` * 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. * 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. * 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 * 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. * 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. * 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. * 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. * 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. * 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). * 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. * 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. * 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. * RestSend() v2: 81% unit test coverage Add the following test case: - test_rest_send_v2_00200 Verify ``commit_check_mode()`` happy path. * 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. * 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. * 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. * 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. * 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. * 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``. * sender_file.py: Add ability to simulate exceptions * 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``. * RestSend().send_interval: Need to check for bool RestSend().send_interval: In validating the input, we need to check for bool type first. * Fix invalid escape. * 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. * 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) * RestSend() v2: property assignment modifications RestSend() v2: remove self.properties in favor of _property. * RestSend().commit_normal_mode(): remove unneeded try-except * 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. * RestSend() remove unneeded method RestSend()._strip_invalid_json_from_response_data() didn't work and wasn't all that useful. Removing it for now. * 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. * 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". * 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. * 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. * 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. * 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. * 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()``. * 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. * 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. * Initial integration test DESCRIPTION - merged_normal_to_maintenance State: merged Test: Change normal mode switches to maintenance mode with config-deploy. * 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 * 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. * 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() * Fix validate-modules DOCUMENTATION error. * Same fix as last commit, but for switch-level. * Merged().get_need(): Update docstring Merged().get_need(): Update docstring to include wait_for_mode_change in example JSON structure. * 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. * 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. * Fix missing import dcnm_maintenance_mode/utils.py: needed import for FabricDetailsByNameV2 * 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. * Fix PEP8 trailing-whitespace * 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. * Fix pylint no-method-argument in MockMergeDicts() * dcnm_maintenance_mode.py: 77% unit test coverage. Added testcases for Want(), Merged(). * dcnm_maintenance_mode.py: 87% unit test coverage. 1. Query(): Add initial unit tests. 2. Query(): Update error messages for consistency with Merged() * 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``. * 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. * 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. * Complete integration tests. * 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. --- playbooks/roles/dcnm_fabric/dcnm_tests.yaml | 3 +- .../dcnm_maintenance_mode/dcnm_hosts.yaml | 20 + .../dcnm_maintenance_mode/dcnm_tests.yaml | 42 + .../rest/control/fabrics/fabrics.py | 372 ++++- .../v1/lan_fabric/rest/inventory/__init__.py | 0 .../v1/lan_fabric/rest/inventory/inventory.py | 98 ++ plugins/module_utils/common/exceptions.py | 4 + plugins/module_utils/common/log_v2.py | 383 +++++ .../module_utils/common/maintenance_mode.py | 664 ++++++++ .../common/maintenance_mode_info.py | 605 ++++++++ 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 | 36 +- .../module_utils/common/params_validate_v2.py | 706 +++++++++ plugins/module_utils/common/properties.py | 123 ++ .../module_utils/common/response_handler.py | 179 ++- plugins/module_utils/common/rest_send.py | 183 ++- plugins/module_utils/common/rest_send_v2.py | 828 ++++++++++ plugins/module_utils/common/results.py | 264 +++- plugins/module_utils/common/sender_dcnm.py | 268 ++++ plugins/module_utils/common/sender_file.py | 282 ++++ plugins/module_utils/common/switch_details.py | 705 +++++++++ .../module_utils/fabric/fabric_details_v2.py | 822 ++++++++++ plugins/modules/dcnm_maintenance_mode.py | 1358 +++++++++++++++++ .../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_1x_rw.yaml | 94 ++ .../tests/00_setup_fabrics_2x_rw.yaml | 123 ++ ...ance_mode_deploy_no_wait_switch_level.yaml | 173 +++ ...rmal_mode_deploy_no_wait_switch_level.yaml | 173 +++ ...tenance_mode_deploy_no_wait_top_level.yaml | 165 ++ ..._normal_mode_deploy_no_wait_top_level.yaml | 167 ++ ...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 | 397 +++++ .../dcnm_maintenance_mode/tests/README.md | 55 + 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.15.txt | 1 + tests/sanity/ignore-2.16.txt | 1 + tests/sanity/ignore-2.9.txt | 1 + tests/unit/mocks/__init__.py | 0 .../unit/mocks/mock_fabric_details_by_name.py | 189 +++ tests/unit/mocks/mock_switch_details.py | 394 +++++ ...v1_configtemplate_rest_config_templates.py | 93 ++ ...t_api_v1_imagemanagement_rest_imagemgnt.py | 39 + ...pi_v1_imagemanagement_rest_imageupgrade.py | 53 + ..._api_v1_imagemanagement_rest_policymgnt.py | 129 ++ ..._imagemanagement_rest_stagingmanagement.py | 67 + ..._api_v1_lan_fabric_rest_control_fabrics.py | 968 ++++++++++++ ...api_v1_lan_fabric_rest_control_switches.py | 79 + .../unit/module_utils/common/common_utils.py | 160 +- .../common/fixtures/merge_dicts_v2.json | 147 ++ .../responses_DeployMaintenanceMode.json | 11 + .../responses_FabricDetailsByName.json | 143 ++ .../fixtures/responses_MaintenanceMode.json | 22 + .../common/fixtures/responses_SenderDcnm.json | 22 + .../fixtures/responses_SwitchDetails.json | 680 +++++++++ tests/unit/module_utils/common/test_log.py | 2 +- tests/unit/module_utils/common/test_log_v2.py | 442 ++++++ .../common/test_maintenance_mode.py | 1198 +++++++++++++++ .../common/test_maintenance_mode_info.py | 1358 +++++++++++++++++ .../common/test_merge_dicts_v2.py | 375 +++++ .../common/test_params_validate_v2.py | 880 +++++++++++ .../common/test_response_handler.py | 12 +- .../module_utils/common/test_rest_send_v2.py | 1329 ++++++++++++++++ .../module_utils/common/test_sender_dcnm.py | 394 +++++ .../module_utils/common/test_sender_file.py | 270 ++++ .../common/test_switch_details.py | 905 +++++++++++ .../responses_FabricDetailsByName_V2.json | 448 ++++++ .../responses_FabricDetailsByNvPair_V2.json | 186 +++ .../fixtures/responses_FabricDetails_V2.json | 380 +++++ .../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 | 578 +++++++ .../test_fabric_details_by_nv_pair.py | 6 +- .../test_fabric_details_by_nv_pair_v2.py | 381 +++++ .../dcnm_fabric/test_fabric_details_v2.py | 638 ++++++++ .../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 | 102 +- .../test_image_policy_common.py | 50 +- .../dcnm/dcnm_maintenance_mode/__init__.py | 0 .../dcnm/dcnm_maintenance_mode/fixture.py | 50 + .../fixtures/configs_Common.json | 142 ++ .../fixtures/configs_Merged.json | 119 ++ .../fixtures/configs_Query.json | 31 + .../fixtures/configs_Want.json | 124 ++ .../fixtures/responses_EpAllSwitches.json | 286 ++++ .../fixtures/responses_EpFabrics.json | 165 ++ .../responses_EpMaintenanceModeDeploy.json | 42 + .../responses_EpMaintenanceModeDisable.json | 24 + .../responses_EpMaintenanceModeEnable.json | 33 + .../test_dcnm_maintenance_mode_common.py | 428 ++++++ .../test_dcnm_maintenance_mode_merged.py | 925 +++++++++++ .../test_dcnm_maintenance_mode_params_spec.py | 208 +++ .../test_dcnm_maintenance_mode_query.py | 373 +++++ .../test_dcnm_maintenance_mode_want.py | 601 ++++++++ .../dcnm/dcnm_maintenance_mode/utils.py | 328 ++++ 117 files changed, 27157 insertions(+), 346 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 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/log_v2.py create mode 100644 plugins/module_utils/common/maintenance_mode.py create mode 100644 plugins/module_utils/common/maintenance_mode_info.py 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 plugins/module_utils/common/properties.py create mode 100644 plugins/module_utils/common/rest_send_v2.py create mode 100644 plugins/module_utils/common/sender_dcnm.py create mode 100644 plugins/module_utils/common/sender_file.py create mode 100644 plugins/module_utils/common/switch_details.py create mode 100644 plugins/module_utils/fabric/fabric_details_v2.py create mode 100644 plugins/modules/dcnm_maintenance_mode.py 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_1x_rw.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_2x_rw.yaml 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 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 create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/09_merged_maintenance_mode_no_deploy.yaml create mode 100644 tests/integration/targets/dcnm_maintenance_mode/tests/README.md 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 create mode 100644 tests/unit/module_utils/common/api/test_api_v1_configtemplate_rest_config_templates.py create mode 100644 tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_imagemgnt.py create mode 100644 tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_imageupgrade.py create mode 100644 tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_policymgnt.py create mode 100644 tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_stagingmanagement.py create mode 100644 tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_fabrics.py create mode 100644 tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_switches.py create mode 100644 tests/unit/module_utils/common/fixtures/merge_dicts_v2.json create mode 100644 tests/unit/module_utils/common/fixtures/responses_DeployMaintenanceMode.json create mode 100644 tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json create mode 100644 tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json create mode 100644 tests/unit/module_utils/common/fixtures/responses_SenderDcnm.json create mode 100644 tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json create mode 100644 tests/unit/module_utils/common/test_log_v2.py create mode 100644 tests/unit/module_utils/common/test_maintenance_mode.py create mode 100644 tests/unit/module_utils/common/test_maintenance_mode_info.py 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 create mode 100644 tests/unit/module_utils/common/test_rest_send_v2.py create mode 100644 tests/unit/module_utils/common/test_sender_dcnm.py create mode 100644 tests/unit/module_utils/common/test_sender_file.py create mode 100644 tests/unit/module_utils/common/test_switch_details.py create mode 100644 tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json create mode 100644 tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByNvPair_V2.json 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_by_name_v2.py create mode 100644 tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py create mode 100644 tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py 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/fixtures/configs_Merged.json create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Query.json create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Want.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_common.py create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_params_spec.py create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_query.py create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py create mode 100644 tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py diff --git a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml index a3cc72d88..32e02f670 100644 --- a/playbooks/roles/dcnm_fabric/dcnm_tests.yaml +++ b/playbooks/roles/dcnm_fabric/dcnm_tests.yaml @@ -1,5 +1,6 @@ --- -# 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: # # Modify the vars section with details for testing setup. # 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 d87433cb3..461e00ee4 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 ``TypeError`` 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 TypeError(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): """ @@ -158,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 @@ -171,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: @@ -184,6 +247,9 @@ class EpFabricConfigDeploy(Fabrics): ```python instance = EpFabricConfigDeploy() instance.fabric_name = "MyFabric" + instance.switch_id = ["CHM1234567", "CHM7654321"] + # or instance.switch_id = "CHM1234567" + # or instance.switch_id = "CHM7654321,CHM1234567" instance.force_show_run = True instance.include_all_msd_switches = True path = instance.path @@ -275,23 +341,36 @@ 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 ``TypeError`` 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): + + def error(param, param_type): 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) + 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: + error(value, type(value).__name__) self.properties["switch_id"] = value @@ -346,28 +425,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): @@ -715,3 +772,256 @@ def _build_properties(self): @property 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 ``TypeError`` 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 TypeError(msg) + self._wait_for_mode_change = value + + +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): + """ + - 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. + """ + _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 verb for the endpoint. + - verb: POST + """ + 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): + """ + - 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" 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/log_v2.py b/plugins/module_utils/common/log_v2.py new file mode 100644 index 000000000..5fd8212db --- /dev/null +++ b/plugins/module_utils/common/log_v2.py @@ -0,0 +1,383 @@ +# 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 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. + - 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. + - ``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.") + ``` + + ### 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.valid_handlers = set() + self.valid_handlers.add("file") + + self._build_properties() + + def _build_properties(self) -> None: + self.properties = {} + self.properties["config"] = environ.get("NDFC_LOGGING_CONFIG", None) + self.properties["develop"] = False + + 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 no handlers. + - The logging config file contains a handler other than + the handlers listed in self.valid_handlers (see class + docstring). + + ### Usage + ```python + log = Log() + log.config = "/path/to/logging_config.json" + 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 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): + """ + ### 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): + 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/plugins/module_utils/common/maintenance_mode.py b/plugins/module_utils/common/maintenance_mode.py new file mode 100644 index 000000000..180c059d1 --- /dev/null +++ b/plugins/module_utils/common/maintenance_mode.py @@ -0,0 +1,664 @@ +# 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.api.v1.lan_fabric.rest.control.fabrics.fabrics import ( + 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 \ + ControllerResponseError +from ansible_collections.cisco.dcnm.plugins.module_utils.common.properties import \ + Properties + + +@Properties.add_rest_send +@Properties.add_results +class MaintenanceMode: + """ + ### Summary + - Modify the maintenance mode state of switches. + - Optionally deploy the changes. + + ### 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. + - ``commit`` if config, rest_send, or results are not set. + - ``commit`` if ``EpMaintenanceModeEnable`` or + ``EpMaintenanceModeDisable`` raise ``ValueError``. + - ``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. + - ``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, initiates a per-fabric + bulk switch 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. + + ```python + instance = MaintenanceMode(params) + try: + instance.config = config + except ValueError as error: + raise ValueError(error) from error + instance.rest_send = RestSend(ansible_module) + instance.results = Results() + try: + instance.commit() + except ValueError as error: + raise ValueError(error) from error + ``` + """ + + 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.endpoints = [] + + self.check_mode = self.params.get("check_mode", None) + if self.check_mode is None: + 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}.{method_name}: " + msg += "params is missing mandatory parameter: state." + raise ValueError(msg) + + # Populated in build_deploy_dict() + self.deploy_dict = {} + self.serial_number_to_ip_address = {} + + self.valid_modes = ["maintenance", "normal"] + + self.conversion = ConversionUtils() + 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 + 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 verify_config_parameters(self, value) -> None: + """ + ### Summary + Verify that required parameters are present in config. + + ### Raises + - ``TypeError`` 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 TypeError(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) + self.verify_wait_for_mode_change(item) + except (TypeError, ValueError) as error: + raise ValueError(error) from error + + def verify_deploy(self, item) -> None: + """ + ### Summary + Verify the ``deploy`` parameter. + + ### 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 += "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 += "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: + """ + ### 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: + msg = f"{self.class_name}.{method_name}: " + msg += "config is missing mandatory key: fabric_name." + 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) -> None: + """ + ### 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: + msg = f"{self.class_name}.{method_name}: " + msg += "config is missing mandatory key: ip_address." + raise ValueError(msg) + + def verify_mode(self, item) -> None: + """ + ### Summary + Validate the ``mode`` parameter. + + ### Raises + - ``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: + msg = f"{self.class_name}.{method_name}: " + 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}: " + 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) -> None: + """ + ### 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 += "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 + Verify that required parameters are present before calling commit. + + ### 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 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) + + def commit(self) -> None: + """ + ### Summary + Initiates the maintenance mode change on the controller. + + ### Raises + - ``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() + except (TypeError, ValueError) as error: + raise ValueError(error) from error + + try: + self.change_system_mode() + self.deploy_switches() + except (ControllerResponseError, ValueError, TypeError) as error: + raise ValueError(error) from error + + 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. + - endpoint cannot be resolved. + - ``Results()`` raises an exception. + - ``TypeError`` if: + - ``serial_number`` is not a string. + """ + 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": + endpoint = self.ep_maintenance_mode_disable + else: + 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 = endpoint.path + self.rest_send.verb = endpoint.verb + self.rest_send.payload = None + self.rest_send.commit() + + # Update diff + result = self.rest_send.result_current["success"] + if result is False: + self.results.diff_current = {} + else: + self.results.diff_current = { + "fabric_name": fabric_name, + "ip_address": ip_address, + "maintenance_mode": mode, + "serial_number": serial_number, + } + + # register 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}: " + 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) -> None: + """ + ### Summary + - Build the deploy_dict + + ### Raises + None + + ### Structure + - key: fabric_name + - value: list of dict + - each dict contains ``serial_number`` and ``wait_for_mode_change keys`` + + ### Example + ```json + { + "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 = {} + for item in self.config: + 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: + 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: + """ + ### 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 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, 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 = 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"] = self.ep_maintenance_mode_deploy.serial_number + endpoint["fabric_name"] = fabric_name + endpoints.append(copy.copy(endpoint)) + self.endpoints = copy.copy(endpoints) + + 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. + """ + method_name = inspect.stack()[0][3] + self.build_deploy_dict() + self.build_serial_number_to_ip_address() + 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.payload = None + self.rest_send.commit() + + # Register the result + 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}) + 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 + 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() + + if self.results.response_current["RETURN_CODE"] != 200: + msg = f"{self.class_name}.{method_name}: " + 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) + + @property + def config(self) -> list: + """ + ### Summary + The maintenance mode configurations to be sent to the controller. + + ### 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: + - ``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._config + + @config.setter + def config(self, value): + try: + self.verify_config_parameters(value) + except (TypeError, ValueError) as error: + raise ValueError(error) from error + self._config = value 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..a59c8d4e8 --- /dev/null +++ b/plugins/module_utils/common/maintenance_mode_info.py @@ -0,0 +1,605 @@ +# 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 ``sender_dcnm.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.conversion = ConversionUtils() + self.fabric_details = FabricDetailsByName(self.params) + self.switch_details = SwitchDetails() + + self._config = None + self._filter = 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, or is missing its " + msg += "serialNumber key." + raise ValueError(msg) + + 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 + 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}) + 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): + """ + Return the value of the item from the filtered switch. + + ### Raises + - ``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] + + 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) + + return self.conversion.make_boolean( + self.conversion.make_none(self._info[self.filter].get(item)) + ) + + @property + def filter(self): + """ + ### Summary + Set the query filter (switch IP address) + + ### 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 += "config must be a list of strings " + msg += "containing ip addresses. " + msg += "value contains element of type " + msg += f"{type(item).__name__}. " + msg += f"value: {value}." + 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_read_only") + + @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/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 a38a07836..4e18dd472 100644 --- a/plugins/module_utils/common/params_validate.py +++ b/plugins/module_utils/common/params_validate.py @@ -29,23 +29,30 @@ class ParamsValidate: """ + ## DEPRECATED + Use ``ParamsValidate`` from ``params_validate_v2.py`` for all new development. + + ### 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 +69,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/params_validate_v2.py b/plugins/module_utils/common/params_validate_v2.py new file mode 100644 index 000000000..71300cd01 --- /dev/null +++ b/plugins/module_utils/common/params_validate_v2.py @@ -0,0 +1,706 @@ +# 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: + 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 + ) + + 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. + - ``ValueError`` if a parameter is missing. + - ``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.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) + 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).__name__} 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).__name__}." + 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/module_utils/common/properties.py b/plugins/module_utils/common/properties.py new file mode 100644 index 000000000..1ae51c292 --- /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/plugins/module_utils/common/response_handler.py b/plugins/module_utils/common/response_handler.py index 1953de773..08efc5aaf 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,20 +92,25 @@ 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}") - 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"} + 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 +119,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 +153,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 +185,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}: " @@ -159,18 +210,42 @@ def commit(self): raise ValueError(msg) self._handle_response() + @property + def implements(self): + """ + ### Summary + The interface implemented by this class. + + ### Raises + None + """ + return self._implements + @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) + return self._response @response.setter def response(self, value): @@ -179,7 +254,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. " @@ -190,16 +265,16 @@ 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): """ - 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) + return self._result @result.setter def result(self, value): @@ -208,17 +283,27 @@ 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) - self._properties["result"] = value + raise TypeError(msg) + self._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) + return self._verb @verb.setter def verb(self, value): @@ -229,4 +314,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/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") 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..19404230f --- /dev/null +++ b/plugins/module_utils/common/rest_send_v2.py @@ -0,0 +1,828 @@ +# +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Allen Robel" + +import copy +import inspect +import json +import logging +from time import sleep + +# Using only for its failed_result property +from ansible_collections.cisco.dcnm.plugins.module_utils.common.results import \ + Results + + +class RestSend: + """ + ### Summary + - 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/sender_dcnm.py`` + - Accepts a ``ResponseHandler()`` class that implements the response + handler interface. + - 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 + requests to the controller. + - 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 + response dict and returns a result dict. + - See ``module_utils/common/response_handler.py`` for details + about implementing ``ResponseHandler()`` classes. + + ### 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(params) + 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 + # 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._implements = "rest_send_v2" + + 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._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 + 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 + - ``response_handler`` 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.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()." + 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()`` + + ### NOTES + - ``check_mode`` is not saved 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 + if self.timeout is not None: + self.saved_timeout = self.timeout + + 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`` + + """ + method_name = inspect.stack()[0][3] + msg = f"{self.class_name}.{method_name}: " + msg += f"check_mode: {self.check_mode}." + self.log.debug(msg) + + 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): + """ + ### Summary + Simulate a controller request for check_mode. + + ### Raises + - ``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 + - ``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() + + 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]" + + 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: + msg = f"{self.class_name}.{method_name}: " + msg += "Error building response/result. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error + + def commit_normal_mode(self): + """ + Call dcnm_send() with retries until successful response or timeout is exceeded. + + ### Raises + - ``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) + - ``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] + + try: + self._verify_commit_parameters() + except ValueError as error: + raise ValueError(error) from error + + timeout = copy.copy(self.timeout) + + 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}" + + 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 + self.response_handler.verb = self.verb + self.response_handler.commit() + self.result_current = self.response_handler.result + except (TypeError, ValueError) as 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}. " + msg += f"result_current: {json.dumps(self.result_current, indent=4, sort_keys=True)}." + self.log.debug(msg) + + success = self.result_current["success"] + if success is False and self.unit_test is False: + sleep(self.send_interval) + timeout -= self.send_interval + + msg = f"{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) + + @property + def check_mode(self): + """ + ### Summary + Determines if changes should be made on the controller. + + ### Raises + - ``TypeError`` if value is not a ``bool`` + + ### Default + ``False`` + + - 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, SwitchDetails + 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._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 boolean. Got {value}." + raise TypeError(msg) + self._check_mode = value + + @property + def failed_result(self): + """ + Return a result for a failed task with no changes + """ + return Results().failed_result + + @property + def implements(self): + """ + ### Summary + The interface implemented by this class. + + ### Raises + None + """ + return self._implements + + @property + def path(self): + """ + Endpoint path for the REST request. + + ### 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): + """ + Return the payload to send to the controller + + ### Raises + None + """ + return self._payload + + @payload.setter + def payload(self, value): + self._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._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 += f"{method_name} must be a dict. " + msg += f"Got type {type(value).__name__}, " + msg += f"Value: {value}." + raise TypeError(msg) + self._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._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 += f"{method_name} must be a dict. " + msg += f"Got type {type(value).__name__}, " + msg += f"Value: {value}." + raise TypeError(msg) + self._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._response_handler + + @response_handler.setter + def response_handler(self, value): + method_name = inspect.stack()[0][3] + _implements_need = "response_handler_v1" + _implements_have = None + msg = f"{self.class_name}.{method_name}: " + msg += f"{method_name} must implement {_implements_need}. " + msg += f"Got type {type(value).__name__}, " + msg += f"implementing {_implements_have}. " + try: + _implements_have = value.implements + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _implements_have != _implements_need: + raise TypeError(msg) + self._response_handler = 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._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 += f"{method_name} must be a dict. " + msg += f"Got type {type(value).__name__}, " + msg += f"Value: {value}." + raise TypeError(msg) + self._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._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 += f"{method_name} must be a dict. " + msg += f"Got {value}." + raise TypeError(msg) + self._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._send_interval + + @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): + raise TypeError(msg) + self._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._sender + + @sender.setter + def sender(self, value): + method_name = inspect.stack()[0][3] + _implements_have = None + _implements_need = "sender_v1" + + msg = f"{self.class_name}.{method_name}: " + msg += f"value must be a class that implements {_implements_need}. " + msg += f"Got type {type(value).__name__}, " + msg += f"value {value}. " + try: + _implements_have = value.implements + except AttributeError as error: + msg += f"Error detail: {error}." + raise TypeError(msg) from error + if _implements_have != _implements_need: + raise TypeError(msg) + self._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._timeout + + @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): + raise TypeError(msg) + self._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._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 boolean. " + msg += f"Got type {type(value).__name__}, " + msg += f"value {value}." + raise TypeError(msg) + self._unit_test = value + + @property + 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 + ``GET``, ``POST``, ``PUT``, ``DELETE`` + """ + return self._verb + + @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: + raise ValueError(msg) + self._verb = value diff --git a/plugins/module_utils/common/results.py b/plugins/module_utils/common/results.py index 9ef9e8115..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): - An examplke 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): @@ -218,16 +235,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 @@ -243,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) @@ -301,7 +318,28 @@ def register_task_result(self): def build_final_result(self): """ - Build the final result + ### Summary + Build the final result. + + ### Description + The final result 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}, " @@ -350,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"] @@ -361,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) @@ -370,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"] @@ -382,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"] @@ -399,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"] @@ -417,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): """ - Return the current diff + ### Summary + - getter: Return the current diff + - setter: Set the current diff - This is a dict of the current diff set by the handler. + ### Raises + - setter: ``TypeError`` if value is not a dict. """ value = self.properties.get("diff_current") value["sequence_number"] = self.task_sequence_number @@ -439,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"] @@ -463,16 +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. - raise ValueError if value is not a dict + ### Raises + - setter: ``TypeError`` if value is not a dict. """ return self.properties["metadata"] @@ -482,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): """ - Return the current metadata which is comprised of the - properties action, check_mode, and state. + ### Summary + - getter: Return the current metadata which is comprised of the + properties action, check_mode, and state. + + ### Raises + None """ value = {} value["action"] = self.action @@ -502,10 +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. + ### 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. - This is a dict of the current response from the controller. + ### Raises + - setter: ``TypeError`` if value is not a dict. """ value = self.properties.get("response_current") value["sequence_number"] = self.task_sequence_number @@ -518,16 +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. + ### Raises + - setter: ``TypeError``: if value is not a dict. """ return self.properties.get("response") @@ -538,14 +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): """ - Return the contents of the DATA key within current_response. + ### 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") @@ -556,10 +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. + ### Raises + - setter: ``TypeError`` if value is not a dict """ return self.properties.get("result") @@ -570,17 +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. + ### Summary + - The current result. + - getter: Return the current result. + - setter: Set the current result. - This is a dict containing the current result. + ### Raises + - setter: ``TypeError`` if value is not a dict """ value = self.properties.get("result_current") value["sequence_number"] = self.task_sequence_number @@ -593,13 +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): """ - Ansible state + ### Summary + - The Ansible state + - getter: Return the state. + - setter: Set the state. + + ### Raises + - setter: ``TypeError`` if value is not a string """ return self.properties["state"] @@ -610,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/plugins/module_utils/common/sender_dcnm.py b/plugins/module_utils/common/sender_dcnm.py new file mode 100644 index 000000000..bc98f1841 --- /dev/null +++ b/plugins/module_utils/common/sender_dcnm.py @@ -0,0 +1,268 @@ +# +# 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. Responses are retrieved using dcnm_send. + + ### Raises + - ``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() + 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. + ``` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + self._implements = "sender_v1" + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + self.params = None + self._ansible_module = None + self._dcnm_send = dcnm_send + self._path = None + self._payload = None + self._response = None + self._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 + """ + method_name = inspect.stack()[0][3] + if self.ansible_module is None: + 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}.{method_name}: " + msg += "path must be set before calling commit()." + raise ValueError(msg) + if self.verb is None: + msg = f"{self.class_name}.{method_name}: " + msg += "verb must be set before calling commit()." + raise ValueError(msg) + + def commit(self): + """ + Send the REST request to the controller + + ### Raises + - ``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 + - ``payload`` Optional HTTP payload + + ## Properties written + - ``response``: raw response from the controller + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][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 + 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 = 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 = self._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._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 += 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 + self._ansible_module = value + + @property + def implements(self): + """ + ### Summary + The interface implemented by this class. + + ### Raises + None + """ + return self._implements + + @property + def path(self): + """ + Endpoint path for the REST request. + + ### 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): + """ + Return the payload to send to the controller + + ### Raises + - ``TypeError`` if value is not a ``dict``. + """ + return self._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 += f"{method_name} must be a dict. " + msg += f"Got type {type(value).__name__}, " + msg += f"value {value}." + raise TypeError(msg) + self._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._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 += f"{method_name} must be a dict. " + msg += f"Got type {type(value).__name__}, " + msg += f"value {value}." + raise TypeError(msg) + self._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._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._verb = value diff --git a/plugins/module_utils/common/sender_file.py b/plugins/module_utils/common/sender_file.py new file mode 100644 index 000000000..35b804c3f --- /dev/null +++ b/plugins/module_utils/common/sender_file.py @@ -0,0 +1,282 @@ +# +# 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._implements = "sender_v1" + self._path = None + self._payload = None + self._response = None + self._verb = None + + self._raise_method = None + self._raise_exception = 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 + """ + method_name = inspect.stack()[0][3] + if self.gen is None: + msg = f"{self.class_name}.{method_name}: " + 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.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: + 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] + 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): + """ + ### 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 + def implements(self): + """ + ### Summary + The interface implemented by this class. + + ### Raises + None + """ + return self._implements + + @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 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): + """ + ### Summary + The simulated response from a file. + + ### Raises + None + + - getter: Return a copy of ``response`` + - setter: Set ``response`` + """ + return self.gen.next + + @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/module_utils/common/switch_details.py b/plugins/module_utils/common/switch_details.py new file mode 100644 index 000000000..33f410be5 --- /dev/null +++ b/plugins/module_utils/common/switch_details.py @@ -0,0 +1,705 @@ +# +# 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 +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 +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 + 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 + - 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 = rest_send + 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... + ``` + + """ + + 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.action = "switch_details" + self.conversion = ConversionUtils() + self.ep_all_switches = EpAllSwitches() + self.path = self.ep_all_switches.path + self.verb = self.ep_all_switches.verb + + self._filter = None + self._info = None + self._rest_send = None + self._results = None + + 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 send_request(self) -> None: + """ + ### Summary + Send the request to the controller. + + ### Raises + - ``ValueError`` if the RestSend object raises + ``TypeError`` or ``ValueError``. + """ + # Send request + 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: + """ + ### Summary + Update and register the results. + + ### Raises + - ``ControllerResponseError`` if: + - The controller RETURN_CODE is not 200. + - ``ValueError`` if: + - ``Results()`` raises ``TypeError``. + """ + method_name = inspect.stack()[0][3] + # Update and register results + 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: + 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}: " + 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 + - ``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: + self.validate_refresh_parameters() + except ValueError as error: + msg = f"{self.class_name}.{method_name}: " + msg += "Mandatory parameters need review. " + msg += f"Error detail: {error}" + raise ValueError(msg) from error + + 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() + except (ControllerResponseError, ValueError) as 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 = {} + for switch in data: + if switch.get("ipAddress", None) is None: + continue + self._info[switch["ipAddress"]] = switch + + 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.conversion.make_boolean( + self.conversion.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 + a non-existent switch, ``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 fabric_name(self): + """ + ### 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): + """ + ### 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): + """ + ### 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 + """ + return self._get("hostName") + + @property + def info(self): + """ + ### Summary + Parsed data from the GET request. + + ### Raises + None + + ### Returns + - Parsed data from the GET request, if it exists. + - ``None`` otherwise + + ### NOTES + - Keyed on ip_address + """ + return self._info + + @property + def is_non_nexus(self): + """ + ### 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): + """ + ### 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") + + @property + def maintenance_mode(self): + """ + ### Summary + - Return a synthesized value for ``maintenanceMode`` status of the + filtered switch, if it exists. + - Return ``mode`` otherwise. + - 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 ``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 + + @property + def managable(self): + """ + ### 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): + """ + ### Summary + The ``mode`` value of the filtered switch. + + ### Raises + - ``ValueError`` (potentially). See ``filter`` setter + and ``_get`` method. + + ### Returns + - The ``mode`` value of the filtered switch, if it exists. + - ``None`` otherwise. + - Example: maintenance, migration, normal, inconsistent + """ + mode = self._get("mode") + if mode is None: + return None + return mode.lower() + + @property + def model(self): + """ + ### 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): + """ + ### 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") + + @property + def platform(self): + """ + ### 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. + """ + model = self._get("model") + if model is None: + return None + return model.split("-")[0] + + @property + def release(self): + """ + ### 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 role(self): + """ + ### 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. + - Example: spine + + ### NOTES + - ``role`` is an alias of ``switch_role``. + """ + return self._get("switchRole") + + @property + def serial_number(self): + """ + ### Summary + The ``serialNumber`` value of the filtered switch. + + ### 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): + """ + ### 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): + """ + ### 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): + """ + ### 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): + """ + ### 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): + """ + ### 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): + """ + ### 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): + """ + ### 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): + """ + ### 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") 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..04d596a43 --- /dev/null +++ b/plugins/module_utils/fabric/fabric_details_v2.py @@ -0,0 +1,822 @@ +# +# 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.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.properties import \ + Properties + + +@Properties.add_rest_send +@Properties.add_results +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__ + 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}.{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}.{method_name}: " + msg += "state is missing from 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)" + 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._rest_send = None + self._results = None + + def register_result(self): + """ + ### Summary + Update the results object with the current state of the fabric + details and register the result. + + ### Raises + - ``ValueError``if: + - ``Results()`` raises ``TypeError`` + """ + method_name = inspect.stack()[0][3] + try: + 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: + 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: + 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: + """ + ### Summary + Validate that mandatory parameters are set before calling refresh(). + + ### Raises + - ``ValueError``if: + - ``rest_send`` is not set. + - ``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: + - ``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 + 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 + + 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. + + This is an alias of BGP_AS. + + ### 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 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): + """ + ### Summary + The nvPairs.DEPLOYMENT_FREEZE of the fabric specified with filter. + + ### Raises + None + + ### Type + boolean + + ### Returns + - False + - True + - None + """ + 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): + """ + ### Summary + The PBR enable state of the fabric specified with filter. + + ### Raises + None + + ### Type + boolean + + ### Returns + - False + - True + - 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): + """ + ### Summary + The ``fabricId`` value of the fabric specified with filter. + + ### Raises + None + + ### Type + string + + ### Returns + - 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): + """ + ### Summary + The ``nvPairs.FABRIC_TYPE`` value of the fabric specified with filter. + + ### Raises + None + + ### Type + string + + ### Returns + - e.g. 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): + """ + ### Summary + The ``nvPairs.IS_READ_ONLY`` value of the fabric specified with filter. + + ### Raises + None + + ### Type + boolean + + ### Returns + - True + - False + - None + """ + 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): + """ + ### Summary + The ``nvPairs.REPLICATION_MODE`` value of the fabric specified + with filter. + + ### Raises + None + + ### Type + boolean + + ### Returns + - 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 template_name(self): + """ + ### Summary + The ``templateName`` value of the fabric specified + with filter. + + ### Raises + None + + ### Type + string + + ### Returns + - e.g. Easy_Fabric + - 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): + """ + ### Summary + Retrieve fabric details from the controller and provide + property accessors for the fabric attributes. + + ### 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() + sender.ansible_module = ansible_module + + rest_send = RestSend(params) + 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 + 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(params) + 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): + 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() " + msg += f"params {params}." + self.log.debug(msg) + + self.data_subclass = {} + self._filter = None + + def refresh(self): + """ + ### Refresh fabric_name current details from the controller + + ### Raises + - ``ValueError`` if: + - Mandatory properties are not set. + """ + 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): + """ + 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): + """ + ### Summary + Retrieve the value of the nvPair item for fabric_name. + + ### 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()`` + """ + 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): + """ + ### 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: + msg = f"{self.class_name}.{method_name}: " + 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) + + @property + def filter(self): + """ + ### Summary + Set the fabric_name of the fabric to query. + + ### Raises + None + + ### NOTES + ``filter`` must be set before accessing this class's properties. + """ + return self._filter + + @filter.setter + def filter(self, value): + self._filter = value + + +class FabricDetailsByNvPair(FabricDetails): + """ + ### 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``. + + ### 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() + sender.ansible_module = ansible_module + + rest_send = RestSend(params) + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + + instance = FabricDetailsNvPair(params) + instance.filter_key = "DCI_SUBNET_RANGE" + instance.filter_value = "10.33.0.0/16" + instance.refresh() + fabrics = instance.filtered_data + ``` + """ + + def __init__(self, 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}") + + msg = "ENTERED FabricDetailsByNvPair() " + self.log.debug(msg) + + self.data_subclass = {} + self._filter_key = None + self._filter_value = None + + def refresh(self): + """ + ### Summary + Refresh fabric_name current details from the controller. + + ### Raises + - ``ValueError`` if: + - ``filter_key`` has not been set. + - ``filter_value`` 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) + + try: + self.refresh_super() + except ValueError as error: + msg = "Failed to refresh fabric details: " + 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 + + @property + def filtered_data(self): + """ + ### 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): + """ + ### Summary + The ``nvPairs`` key on which to filter. + + ### Raises + None + + ### Notes + ``filter_key``should be an exact match for the key in the ``nvPairs`` + dictionary for the fabric. + """ + return self._filter_key + + @filter_key.setter + def filter_key(self, value): + self._filter_key = value + + @property + def filter_value(self): + """ + ### Summary + The ``nvPairs`` value on which to filter. + + ### Raises + None + + ### Notes + ``filter_value`` should be an exact match for the value in the ``nvPairs`` + dictionary for the fabric. + """ + return self._filter_value + + @filter_value.setter + def filter_value(self, value): + self._filter_value = value diff --git a/plugins/modules/dcnm_maintenance_mode.py b/plugins/modules/dcnm_maintenance_mode.py new file mode 100644 index 000000000..45176ce2e --- /dev/null +++ b/plugins/modules/dcnm_maintenance_mode.py @@ -0,0 +1,1358 @@ +#!/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: + default: false + description: + - Whether to deploy the switch configurations. + 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. + required: false + type: bool + mode: + choices: + - maintenance + - normal + default: normal + 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: true + type: list + elements: dict + suboptions: + ip_address: + description: + - The IP address of the switch. + required: true + type: str + mode: + choices: + - maintenance + - normal + default: normal + description: + - Enable maintenance or normal mode for the switch. + required: false + type: str + deploy: + 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. + 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: true + wait_for_mode_change: true + 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 + wait_for_mode_change: 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 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.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 \ + 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 \ + 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): + """ + 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() + try: + params_spec.params = ansible_module.params + except ValueError as error: + ansible_module.fail_json(error) + 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._params = None + self._params_spec: dict = {} + + self.valid_states = ["merged", "query"] + + def commit(self): + """ + Build the parameter specification based on the state + + ## Raises + - ``ValueError`` if params is not set + """ + 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": + 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) -> None: + """ + Build the parameter specifications for ``merged`` state. + """ + self._params_spec: dict = {} + 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"]["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["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" + + def _build_params_spec_for_query_state(self) -> None: + """ + Build the parameter specifications for ``query`` state. + """ + 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: + """ + return the parameter specification + """ + return self._params_spec + + @property + def params(self) -> dict: + """ + ### 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 + """ + 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 + + +class Want: + """ + ### Summary + Build self.want, a list of validated playbook configurations. + + ### Raises + - ``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. + 2. Validate the merged configs from step 1 against the param spec. + 3. Populate self.want with the validated configs. + + ### Usage + ```python + try: + instance = Want() + instance.config = playbook_config + instance.params = ansible_module.params + instance.params_spec = ParamsSpec() + instance.items_key = "switches" + instance.validator = ParamsValidate() + instance.commit() + want = instance.want + except (TypeError, ValueError) as error: + handle_error(error) + ``` + ### self.want structure + + ```json + [ + { + "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 + } + ] + ``` + """ + + def __init__(self): + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + self.log.debug("ENTERED Want()") + + self._config = None + self._items_key = None + self._params = None + self._params_spec = None + self._validator = None + self._want = [] + + self.merge_dicts = MergeDicts() + self.merged_configs = [] + self.item_configs = [] + + 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 += "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 += "params_spec is not set, and is required." + raise ValueError(msg) + + try: + self.params_spec.params = self.params + except ValueError as error: + raise ValueError(error) from error + + self.params_spec.commit() + + def validate_configs(self) -> None: + """ + ### Summary + Validate the merged configs against the param spec + and populate self.want with the validated configs. + + ### Raises + None + + ### Notes + - validator is already verified in commit()s + """ + 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. + + ### Raises + None + """ + 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.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. + + ### 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: + 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: + 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: + 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: + """ + ### 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 += "config is not set, and is required." + raise ValueError(msg) + if self.items_key is None: + msg = f"{self.class_name}.{method_name}: " + 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}." + 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) + + try: + 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: + 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: " + 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._config + + @config.setter + def config(self, value) -> None: + if not isinstance(value, dict): + msg = f"{self.class_name}.config.setter: " + msg += "expected dict but got " + msg += f"{type(value).__name__}, value {value}." + raise TypeError(msg) + self._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._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 but got " + msg += f"{type(value).__name__}, value {value}." + raise TypeError(msg) + self._items_key = value + + @property + def want(self) -> list: + """ + ### Summary + Return the want list. See class docstring for structure details. + """ + return self._want + + @property + def params(self) -> dict: + """ + ### Summary + The return value of ``AnsibleModule.params`` property + (or equivalent dict). This is passed to ``params_spec`` + and used in playbook config validation. + + ### Raises + - setter: raise ``ValueError`` if value is not a ``dict``. + + ### getter + Return params + + ### setter + Set params + """ + return self._params + + @params.setter + def params(self, value: dict) -> None: + """ + - setter: set the params + """ + if not isinstance(value, dict): + msg = f"{self.class_name}.params.setter: " + msg += "expected dict but got " + msg += f"{type(value).__name__}, value {value}." + raise TypeError(msg) + self._params = value + + @property + def params_spec(self): + """ + ### Summary + 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. + + ### Raises + - setter: raise ``TypeError`` if value is not an instance + of ParamsSpec() + + ### getter + Return params_spec + + ### setter + Set params_spec + """ + return self._params_spec + + @params_spec.setter + def params_spec(self, value) -> None: + 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 type {type(value).__name__}, " + msg += f"value {value}. " + 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._params_spec = value + + @property + def validator(self): + """ + ### 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._validator + + @validator.setter + 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 type {type(value).__name__}, " + msg += f"value {value}. " + 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._validator = value + + +@Properties.add_rest_send +class Common: + """ + Common methods, properties, and resources for all states. + """ + + def __init__(self, params): + """ + ### Raises + - ``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] + + self.params = params + 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}: " + 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." + 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." + 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._rest_send = None + + 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 = [] + + 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: + """ + ### 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 (TypeError, ValueError) as error: + raise ValueError(error) from error + + +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__ + 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.maintenance_mode = MaintenanceMode(params) + + msg = f"ENTERED Merged.{method_name}: " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + + self.need = [] + + 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 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. + 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" + } + } + ``` + """ + 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 = [ + 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: + """ + ### Summary + Handle the following cases: + - switch migration mode is ``migration`` + - fabric is in read-only mode (IS_READ_ONLY is True) + - fabric is in freeze mode (Deployment Disable) + + ### 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 == "migration": + msg = f"{self.class_name}.{method_name}: " + msg += "Switch maintenance mode is in migration state for the " + 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 " + 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) + + 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): + """ + ### Summary + Build self.need for merged state. + + ### Raises + - ``ValueError`` if the switch is not found on the controller. + + ### self.need structure + ```json + [ + { + "deploy": false, + "fabric_name": "MyFabric", + "ip_address": "172.22.150.2", + "mode": "maintenance", + "serial_number": "FCI1234567" + "wait_for_mode_change": true + }, + { + "deploy": true, + "fabric_name": "YourFabric", + "ip_address": "172.22.150.3", + "mode": "normal", + "serial_number": "HMD2345678" + "wait_for_mode_change": true + } + ] + """ + 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: + msg = f"{self.class_name}.{method_name}: " + msg += f"Switch {ip_address} 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"]: + 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}) + need.update({"wait_for_mode_change": want.get("wait_for_mode_change")}) + self.need.append(copy.copy(need)) + + def commit(self): + """ + ### Summary + Commit the merged state request + + ### 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] + 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: + 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 + + try: + self.get_have() + except ValueError as error: + raise ValueError(error) from error + + self.fabric_deployment_disabled() + + self.get_need() + + try: + self.send_need() + except ValueError as 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: + """ + ### Summary + Build and send the payload to modify maintenance mode. + + ### Raises + - ``ValueError`` if MaintenanceMode() raises either + ``TypeError`` or ``ValueError`` + + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + if len(self.need) == 0: + msg = f"{self.class_name}.{method_name}: " + msg += "No switches to modify." + self.log.debug(msg) + return + + try: + 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 + + +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__ + 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.maintenance_mode_info = MaintenanceModeInfo(self.params) + + msg = "ENTERED Query(): " + msg += f"state: {self.state}, " + msg += f"check_mode: {self.check_mode}" + self.log.debug(msg) + + def get_have(self): + """ + ### Summary + Build self.have, a dict containing the current mode of all switches. + + ### Raises + - ``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. + 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" + } + } + ``` + """ + method_name = inspect.stack()[0][3] # pylint: disable=unused-variable + + try: + 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", {}) + ] + 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 = self.maintenance_mode_info.info + + 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: + - ``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: + 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 + + try: + self.get_have() + except ValueError as 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" + self.results.changed = False + 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() + + +def main(): + """main entry point for module execution""" + + 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 = copy.deepcopy(ansible_module.params) + params["check_mode"] = ansible_module.check_mode + + # Logging setup + try: + log = Log() + log.commit() + except ValueError as error: + ansible_module.fail_json(str(error)) + + sender = Sender() + sender.ansible_module = ansible_module + rest_send = RestSend(params) + rest_send.response_handler = ResponseHandler() + rest_send.sender = sender + + if params["state"] == "merged": + try: + 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 params["state"] == "query": + try: + 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) + + else: + # We should never get here since the state parameter has + # already been validated. + msg = f"Unknown state {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() 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_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_2x_rw.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_2x_rw.yaml new file mode 100644 index 000000000..ebde6e6f6 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/00_setup_fabrics_2x_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 add 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_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..a1899e21f --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/02_merged_normal_mode_deploy_no_wait_switch_level.yaml @@ -0,0 +1,173 @@ +--- +################################################################################ +# 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/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/09_merged_maintenance_mode_no_deploy.yaml b/tests/integration/targets/dcnm_maintenance_mode/tests/09_merged_maintenance_mode_no_deploy.yaml new file mode 100644 index 000000000..c452e5f13 --- /dev/null +++ b/tests/integration/targets/dcnm_maintenance_mode/tests/09_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 switch mode is normal +# TEST +# GLOBAL CONFIG +# 2. Change switch mode to maintenance (global config) +# 3. Verify switch mode is inconsistent (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 inconsistent (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: 07_merged_maintenance_mode_no_deploy +# 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 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: 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: 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: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "inconsistent" + - result.diff[2][leaf_2].mode == "inconsistent" + +################################################################################ +# 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: false + mode: normal + 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" + +- 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 + +################################################################################ +# 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: false + switches: + - ip_address: "{{ leaf_1 }}" + mode: maintenance + - ip_address: "{{ leaf_2 }}" + mode: maintenance + register: result_maintenance_mode +- debug: + var: result_maintenance_mode + +################################################################################ +# 7. 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: 60 + delay: 10 + until: + - result.diff[2][leaf_1].mode == "inconsistent" + - result.diff[2][leaf_2].mode == "inconsistent" + +################################################################################ +# 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: 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 + +################################################################################ +# 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" + +- 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/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 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.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 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 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..01ab992a0 --- /dev/null +++ b/tests/unit/mocks/mock_fabric_details_by_name.py @@ -0,0 +1,189 @@ +# 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" + + +class MockFabricDetailsByName: + """ + ### 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: + + 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._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 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): + """ + 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 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.setter" + ): + raise self.mock_exception(self.mock_message) + self._rest_send = value + + @property + 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.setter" + ): + raise self.mock_exception(self.mock_message) + self._results = value + + @property + def is_read_only(self): + """ + Mocked is_read_only property + """ + 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 diff --git a/tests/unit/mocks/mock_switch_details.py b/tests/unit/mocks/mock_switch_details.py new file mode 100644 index 000000000..93e4607aa --- /dev/null +++ b/tests/unit/mocks/mock_switch_details.py @@ -0,0 +1,394 @@ +# 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" + + +class MockSwitchDetails: + """ + ### 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: + + 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._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) + + @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 filter(self): + """ + Mocked filter + """ + 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.setter" + ): + raise self.mock_exception(self.mock_message) + self._rest_send = value + + @property + 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.setter" + ): + raise self.mock_exception(self.mock_message) + self._results = value + + @property + def fabric_name(self): + """ + Mocked fabric_name property + """ + 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 + def freeze_mode(self): + """ + Mocked freeze_mode property + """ + 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 + def maintenance_mode(self): + """ + Mocked maintenance_mode property + """ + if ( + self.mock_class == self.class_name + and self.mock_property == "maintenance_mode.getter" + ): + raise self.mock_exception(self.mock_message) + return self._maintenance_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 + def mode(self): + """ + Mocked mode property + """ + if self.mock_class == self.class_name and self.mock_property == "mode.getter": + raise self.mock_exception(self.mock_message) + return self._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 + 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 + """ + if ( + self.mock_class == self.class_name + and self.mock_property == "switch_role.getter" + ): + raise self.mock_exception(self.mock_message) + return self.switch_role + + @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 + def system_mode(self): + """ + Mocked switch_role property + """ + if ( + self.mock_class == self.class_name + and self.mock_property == "system_mode.getter" + ): + raise self.mock_exception(self.mock_message) + return self.system_mode + + @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/api/test_api_v1_configtemplate_rest_config_templates.py b/tests/unit/module_utils/common/api/test_api_v1_configtemplate_rest_config_templates.py new file mode 100644 index 000000000..bdedf18f9 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_api_v1_configtemplate_rest_config_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.configtemplate.rest.config.templates.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" diff --git a/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_imagemgnt.py b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_imagemgnt.py new file mode 100644 index 000000000..ab0785d15 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_imagemgnt.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.imagemanagement.rest.imagemgnt.imagemgnt 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_api_v1_imagemanagement_rest_imageupgrade.py b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_imageupgrade.py new file mode 100644 index 000000000..1e49fd61f --- /dev/null +++ b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_imageupgrade.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.imagemanagement.rest.imageupgrade.imageupgrade 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_api_v1_imagemanagement_rest_policymgnt.py b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_policymgnt.py new file mode 100644 index 000000000..ff66de1b3 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_policymgnt.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.imagemanagement.rest.policymgnt.policymgnt 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_api_v1_imagemanagement_rest_stagingmanagement.py b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_stagingmanagement.py new file mode 100644 index 000000000..8bb951c05 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_api_v1_imagemanagement_rest_stagingmanagement.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.imagemanagement.rest.stagingmanagement.stagingmanagement 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" diff --git a/tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_fabrics.py b/tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_fabrics.py new file mode 100644 index 000000000..3a019ad91 --- /dev/null +++ b/tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_fabrics.py @@ -0,0 +1,968 @@ +# 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.lan_fabric.rest.control.fabrics.fabrics import ( + EpFabricConfigDeploy, EpFabricConfigSave, EpFabricCreate, EpFabricDelete, + 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(): + """ + ### 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 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``. + + """ + 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_00060(): + """ + ### 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_00070(): + """ + ### 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_00080(): + """ + ### 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 + ) + + +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 + - 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 = TICKET_ID + ticket_id_path = f"{PATH_PREFIX}/{FABRIC_NAME}/config-save" + ticket_id_path += f"?ticketId={TICKET_ID}" + 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 = TICKET_ID + ticket_id_path = f"{PATH_PREFIX}/{FABRIC_NAME}/config-save" + ticket_id_path += f"?ticketId={TICKET_ID}" + 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 + - 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 + + ### 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_00440(): + """ + ### 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_00450(): + """ + ### 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_00500(): + """ + ### 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_00540(): + """ + ### 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_00550(): + """ + ### 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_00600(): + """ + ### 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_00640(): + """ + ### 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_00650(): + """ + ### 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 + + +# 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 + + +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 diff --git a/tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_switches.py b/tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_switches.py new file mode 100644 index 000000000..a654f846d --- /dev/null +++ b/tests/unit/module_utils/common/api/test_api_v1_lan_fabric_rest_control_switches.py @@ -0,0 +1,79 @@ +# 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.lan_fabric.rest.control.switches.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 diff --git a/tests/unit/module_utils/common/common_utils.py b/tests/unit/module_utils/common/common_utils.py index 70db881ce..56c28d2fe 100644 --- a/tests/unit/module_utils/common/common_utils.py +++ b/tests/unit/module_utils/common/common_utils.py @@ -28,10 +28,22 @@ 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.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 \ + 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 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 @@ -44,8 +56,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 @@ -73,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 @@ -95,7 +117,7 @@ class MockAnsibleModule: supports_check_mode = True @staticmethod - def fail_json(msg) -> AnsibleFailJson: + def fail_json(msg, **kwargs) -> AnsibleFailJson: """ mock the fail_json method """ @@ -127,6 +149,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(): """ @@ -135,6 +181,22 @@ def log_fixture(): return Log(MockAnsibleModule) +@pytest.fixture(name="maintenance_mode") +def maintenance_mode_fixture(): + """ + return MaintenanceMode + """ + 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(): """ @@ -143,6 +205,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(): """ @@ -151,6 +221,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(): """ @@ -161,7 +239,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) @@ -169,9 +247,29 @@ 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_deploy_maintenance_mode(key: str) -> Dict[str, str]: + """ + Return data in responses_DeployMaintenanceMode.json + """ + response_file = "responses_DeployMaintenanceMode" + response = load_fixture(response_file).get(key) + print(f"responses_deploy_maintenance_mode: {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 +278,59 @@ 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_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 + """ + response_file = "responses_MaintenanceMode" + response = load_fixture(response_file).get(key) + print(f"responses_maintenance_mode: {key} : {response}") + 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 + """ + 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/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/fixtures/responses_DeployMaintenanceMode.json b/tests/unit/module_utils/common/fixtures/responses_DeployMaintenanceMode.json new file mode 100644 index 000000000..8fbbd2578 --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/responses_DeployMaintenanceMode.json @@ -0,0 +1,11 @@ +{ + "test_maintenance_mode_00220a": { + "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/FDO211218HH/deploy-maintenance-mode?waitForModeChange=true", + "RETURN_CODE": 200 + } +} \ No newline at end of file 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..217198f3e --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/responses_FabricDetailsByName.json @@ -0,0 +1,143 @@ +{ + "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_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", + "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_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", + "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 + }, + "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 + }, + "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 + }, + "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_MaintenanceMode.json b/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json new file mode 100644 index 000000000..20da8f3d6 --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/responses_MaintenanceMode.json @@ -0,0 +1,22 @@ +{ + "test_maintenance_mode_00220a": { + "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 + }, + "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/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/fixtures/responses_SwitchDetails.json b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json new file mode 100644 index 000000000..0212edebb --- /dev/null +++ b/tests/unit/module_utils/common/fixtures/responses_SwitchDetails.json @@ -0,0 +1,680 @@ +{ + "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 + }, + "test_maintenance_mode_info_00210a": { + "TEST_NOTES": [ + "No switches exist on the controller", + "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 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": [ + { + "fabricName": "VXLAN_Fabric", + "freezeMode": null, + "ipAddress": "192.168.1.1", + "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 + }, + "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", + "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": "VXLAN_Fabric", + "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 + }, + "test_maintenance_mode_info_00500a": { + "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: leaf", + "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": "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_00510a": { + "TEST_NOTES": [ + "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": [ + { + "fabricName": "VXLAN_Fabric", + "freezeMode": true, + "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 + }, + "test_maintenance_mode_info_00520a": { + "TEST_NOTES": [ + "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, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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_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/module_utils/common/test_log_v2.py b/tests/unit/module_utils/common/test_log_v2.py new file mode 100644 index 000000000..120203855 --- /dev/null +++ b/tests/unit/module_utils/common/test_log_v2.py @@ -0,0 +1,442 @@ +# 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 logging.config.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 += 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_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 + handler(s) that emit to non-file destinations. + """ + 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 = 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\.\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() + + +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 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..c18cd0793 --- /dev/null +++ b/tests/unit/module_utils/common/test_maintenance_mode.py @@ -0,0 +1,1198 @@ +# 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.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.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.tests.unit.module_utils.common.common_utils import ( + ResponseGenerator, does_not_raise, maintenance_mode_fixture, params, + responses_deploy_maintenance_mode, responses_maintenance_mode) + +FABRIC_NAME = "VXLAN_Fabric" +CONFIG = [ + { + "deploy": False, + "fabric_name": f"{FABRIC_NAME}", + "ip_address": "192.168.1.2", + "mode": "maintenance", + "wait_for_mode_change": False, + "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._config is None + assert instance._rest_send is None + assert instance._results 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.deploy_dict == {} + 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) + + +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__() + - verify_commit_parameters() + - 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({}) + 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__() + - verify_commit_parameters() + - 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__() + - verify_commit_parameters() + - 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({}) + 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_00200( + 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({}) + 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_00210( + monkeypatch, maintenance_mode, mock_exception, expected_exception, mock_message +) -> None: + """ + Classes and Methods + - MaintenanceMode() + - __init__() + - commit() + + Summary + - Verify MaintenanceMode().commit() raises ``ValueError`` when + ``MaintenanceMode().deploy_switches`` 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({}) + 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() + + +@pytest.mark.parametrize( + "mode, deploy", + [ + ("maintenance", True), + ("maintenance", False), + ("normal", True), + ("normal", False), + ], +) +def test_maintenance_mode_00220(maintenance_mode, mode, deploy) -> 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 + - Sender() is mocked to return expected 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" + + def responses(): + yield responses_maintenance_mode(key) + yield responses_deploy_maintenance_mode(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 + + config = copy.deepcopy(CONFIG[0]) + config["mode"] = mode + config["deploy"] = deploy + + with does_not_raise(): + instance = maintenance_mode + instance.rest_send = rest_send + instance.results = Results() + instance.config = [config] + 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 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.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") == "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" + + assert instance.results.result[0].get("changed", None) is True + assert instance.results.result[0].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( + "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) + + 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(): + instance = maintenance_mode + instance.rest_send = rest_send + 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) == "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_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"), + ("wait_for_mode_change"), + ], +) +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 + - wait_for_mode_change 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] + + +@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 + + +@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", + [ + ("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_00800( + 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 + - 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. + """ + + 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() + + +@pytest.mark.parametrize( + "endpoint_instance, mock_exception, expected_exception, mock_message", + [ + ("ep_maintenance_mode_deploy", TypeError, ValueError, "Bad type"), + ("ep_maintenance_mode_deploy", ValueError, ValueError, "Bad value"), + ], +) +def test_maintenance_mode_00900( + 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 ``EpMaintenanceModeDeploy`` raises any of: + - ``TypeError`` + - ``ValueError`` + + + Code Flow - Setup + - MaintenanceMode() is instantiated + - Required attributes are set + - EpMaintenanceModeDeploy() 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 EpMaintenanceModeDeploy() class + """ + + 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): + """ + Mock fabric_name getter/setter to raise an exception + in the setter. + """ + return self._fabric_name + + @fabric_name.setter + def fabric_name(self, *args): + 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 + + @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()) + 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["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", + [ + (TypeError, ValueError, r"Converted TypeError to ValueError"), + (ValueError, ValueError, r"Converted ValueError to ValueError"), + ], +) +def test_maintenance_mode_01000( + 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 to raise an exception in the setter. + """ + + @response_current.setter + def response_current(self, *args): + raise mock_exception(mock_message) + + def responses(): + yield {"RETURN_CODE": 200, "MESSAGE": "OK", "DATA": {"status": "Success"}} + + 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 = maintenance_mode + instance.rest_send = rest_send + instance.config = CONFIG + instance.results = MockResults() + + with pytest.raises(expected_exception, match=mock_message): + instance.commit() + + +def test_maintenance_mode_01100(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"}, + } + + 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["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 switch:\s+" + match += r"fabric_name VXLAN_Fabric,\s+" + match += r"serial_number FDO22180ASJ\.\s+" + match += r"Got response.*\." + with pytest.raises(ValueError, match=match): + instance.commit() 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..6d20cc9f3 --- /dev/null +++ b/tests/unit/module_utils/common/test_maintenance_mode_info.py @@ -0,0 +1,1358 @@ +# 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.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 ( + 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"] +PARAMS = {"state": "query", "check_mode": False} + + +def test_maintenance_mode_info_00000(maintenance_mode_info) -> None: + """ + ### 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. + + """ + 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()`` + - ``verify_refresh_parameters()`` + - ``refresh()`` + + ### Summary + - Verify MaintenanceModeInfo().refresh() raises ``ValueError`` when + ``config`` is not set. + + ### Setup - Data + - None + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated. + - Other required attributes are set. + + ### Trigger + - ``refresh()`` is called without having first set ``config``. + + ### Expected Result + - ``ValueError`` is raised. + - Exception message matches expectations. + """ + 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()`` + - ``verify_refresh_parameters()`` + - ``refresh()`` + + ### Summary + - Verify ``refresh()`` raises ``ValueError`` when ``rest_send`` + is not set. + + ### Setup - Data + - None + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated. + - Other required attributes are set. + + ### Trigger + - ``refresh()`` is called without having first set ``rest_send``. + + ### Expected Result + - ``ValueError`` is raised. + - Exception message matches expectations. + """ + 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()`` + - ``verify_refresh_parameters()`` + - ``refresh()`` + + ### Summary + - Verify ``refresh()`` raises ``ValueError`` when ``results`` is not set. + + ### Setup - Data + - None + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated. + - Other required attributes are set. + + ### Trigger + - ``refresh()`` is called without having first set ``results``. + + ### Expected Result + - ``ValueError`` is raised. + - Exception message matches expectations. + """ + 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", + "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: + """ + ### Classes and Methods + - ``MaintenanceModeInfo()`` + - ``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``. + + ### 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``. + + ### 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) + + 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) + + 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() + + +@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() + - refresh() + + ### Summary + - Verify ``refresh()`` raises ``ValueError`` when + ``switch_details.serial_number`` raises ``ValueError``. + + ### Setup - Data + - ``responses_SwitchDetails.json``: + - RETURN_CODE: 200 + - MESSAGE: OK + + ### Setup - Code + - ``MaintenanceModeInfo()`` is instantiated + - Required attributes are set + - ``SwitchDetails()`` is mocked to conditionally raise + ``ValueError`` in the ``serial_number.getter`` property. + + ### 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) + + 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) + + 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, "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() -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + + ### Summary + Verify ``refresh()`` raises ``ValueError`` when + ``switch_details._get()`` raises ``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 + - ``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 + + ### 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"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): + 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", + [ + ( + "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() + - refresh() + + ### Summary + - 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. + - ``FabricDetailsByName().filter`` is mocked to conditionally raise + ``ValueError``. + + ### 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) + + 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) + + 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 + + monkeypatch.setattr(instance, "fabric_details", mock_fabric_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_00500() -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - refresh() + + ### Summary + - 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 + + ### Trigger + - ``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_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_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() -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + + ### Summary + - Verify happy path with: + - switch_details: freezeMode is True + + ### 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 + + ### Trigger + - ``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_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_name == FABRIC_NAME + assert instance.fabric_freeze_mode is True + assert instance.fabric_read_only is False + assert instance.fabric_deployment_disabled is True + assert instance.mode == "normal" + assert instance.role == "leaf" + + +def test_maintenance_mode_info_00520() -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - __init__() + - refresh() + + ### Summary + - 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 + + ### 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.mode == "inconsistent" + 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 + + +def test_maintenance_mode_info_00600() -> None: + """ + ### Classes and Methods + - MaintenanceModeInfo() + - 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 + + +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 + + +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() + + +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"] + + +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" 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..db2de0786 --- /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"list\." + + 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).__name__} 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() diff --git a/tests/unit/module_utils/common/test_response_handler.py b/tests/unit/module_utils/common/test_response_handler.py index 0b2964b8c..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"} @@ -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" 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..308bea088 --- /dev/null +++ b/tests/unit/module_utils/common/test_rest_send_v2.py @@ -0,0 +1,1329 @@ +# 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.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 responses(): + """ + Dummy coroutine for ResponseGenerator() + + See e.g. test_rest_send_v2_00800 + """ + yield {} + + +def test_rest_send_v2_00000() -> None: + """ + ### Classes and Methods + - RestSend() + - __init__() + + ### 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._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 + 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 + - RestSend().commit() is called. + + ### Expected Result + - RestSend()._verify_commit_parameters() raises ``ValueError``. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + instance.sender = Sender() + instance.response_handler = ResponseHandler() + instance.verb = "GET" + + 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() + + +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 + - RestSend().commit() is called. + + ### Expected Result + - RestSend()._verify_commit_parameters() raises ``ValueError``. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + instance.path = "/foo/path" + instance.sender = Sender() + instance.verb = "GET" + + 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() + + +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 ``sender`` 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 + - RestSend().commit() is called. + + ### Expected Result + - RestSend()._verify_commit_parameters() raises ``ValueError``. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + instance.path = "/foo/path" + instance.response_handler = ResponseHandler() + instance.verb = "GET" + + 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() + + +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 ``verb`` 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 + - RestSend().commit() is called. + + ### Expected Result + - RestSend()._verify_commit_parameters() raises ``ValueError``. + """ + with does_not_raise(): + instance = RestSend(PARAMS) + instance.path = "/foo/path" + 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\(\)\." + 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 when + ``verb`` is "GET". + + ### 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["found"] 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 = "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] + + +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] + + +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:\s+" + match += r"RestSend\.commit_check_mode:\s+" + match += r"Error building response\/result\." + with pytest.raises(ValueError, match=match): + 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() + + +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.*\." + + +@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 + + +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 + + +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\.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_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() + - 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_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_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_01000(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] + + +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 + + +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" + + +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" + + +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 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..f563fe07d --- /dev/null +++ b/tests/unit/module_utils/common/test_sender_dcnm.py @@ -0,0 +1,394 @@ +# 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.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 ( + ResponseGenerator, does_not_raise, 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 + """ + with does_not_raise(): + 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``. + + + """ + 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+" + 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``. + """ + with does_not_raise(): + 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``. + """ + 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+" + 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() + + +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 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() 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..4a9da7b39 --- /dev/null +++ b/tests/unit/module_utils/common/test_switch_details.py @@ -0,0 +1,905 @@ +# 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 inspect + +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.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 \ + 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, responses_switch_details) + +PARAMS = {"state": "merged", "check_mode": False} + + +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() + - refresh() + + ### 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() + - refresh() + + ### 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() + + +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() + 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 + 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] + + +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() + + +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() + - _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 # 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 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..ed53309b9 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByName_V2.json @@ -0,0 +1,448 @@ +{ + "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 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_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 + }, + "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 + }, + "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/fixtures/responses_FabricDetailsByNvPair_V2.json b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByNvPair_V2.json new file mode 100644 index 000000000..14bd8e3fd --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetailsByNvPair_V2.json @@ -0,0 +1,186 @@ +{ + "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_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.", + "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/fixtures/responses_FabricDetails_V2.json b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetails_V2.json new file mode 100644 index 000000000..b5a2eb17a --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/fixtures/responses_FabricDetails_V2.json @@ -0,0 +1,380 @@ +{ + "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 + }, + "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_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 new file mode 100644 index 000000000..70d8b03bf --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_name_v2.py @@ -0,0 +1,578 @@ +# 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 \ + 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 ( + 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_00000(monkeypatch) -> None: + """ + ### Classes and Methods + - FabricDetailsByName() + - __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 + - FabricDetailsByName() is instantiated. + + ### Expected Result + - 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+" + 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 + - FabricDetailsByName() + - __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 + - FabricDetailsByName() + - __init__() + - refresh() + + ### Summary + - Verify properties missing in the controller response return ``None``. + + ### 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 + + ### 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_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 + + +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() + + +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 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. + - 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 + + +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" + + +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 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 new file mode 100644 index 000000000..8d3e97700 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_by_nv_pair_v2.py @@ -0,0 +1,381 @@ +# 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.module_utils.common.common_utils import \ + ResponseGenerator +from ansible_collections.cisco.dcnm.tests.unit.modules.dcnm.dcnm_fabric.utils import ( + 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_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() 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..9cff01137 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_fabric/test_fabric_details_v2.py @@ -0,0 +1,638 @@ +# 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 \ + 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 ( + does_not_raise, fabric_details_v2_fixture, responses_fabric_details_v2) + + +def test_fabric_details_v2_00000(fabric_details_v2) -> None: + """ + ### Classes and Methods + - 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 + - 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 + - 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( # pylint: disable=unused-variable + {"check_mode": False} + ) + + +def test_fabric_details_v2_00100(fabric_details_v2) -> None: + """ + ### Classes and Methods + - 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) + + 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) == 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(fabric_details_v2) -> None: + """ + ### Classes and Methods + - 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) + + 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) == 0 + assert len(instance.results.result) == 0 + assert len(instance.results.response) == 0 + + +def test_fabric_details_v2_00120(fabric_details_v2) -> None: + """ + ### Classes and Methods + - 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) + + 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) == 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_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_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 + - 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 + - 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 + - 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"} + + +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 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 2cb473afe..58de6533d 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 2b3ba4dd1..20a52ea6a 100644 --- a/tests/unit/modules/dcnm/dcnm_fabric/utils.py +++ b/tests/unit/modules/dcnm/dcnm_fabric/utils.py @@ -37,6 +37,12 @@ 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_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 \ @@ -61,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 @@ -227,6 +196,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(): """ @@ -237,6 +214,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(): """ @@ -247,6 +235,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(): """ @@ -497,6 +495,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 @@ -507,6 +515,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 version 2 + """ + 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 @@ -517,6 +535,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 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 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..e6cd1b333 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Common.json @@ -0,0 +1,142 @@ +{ + "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 + } + ] + }, + "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/fixtures/configs_Merged.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Merged.json new file mode 100644 index 000000000..8b65e469c --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Merged.json @@ -0,0 +1,119 @@ +{ + "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" + } + ] + }, + "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/configs_Query.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Query.json new file mode 100644 index 000000000..f1b80929c --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/configs_Query.json @@ -0,0 +1,31 @@ +{ + "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": { + "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/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/fixtures/responses_EpAllSwitches.json b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json new file mode 100644 index 000000000..10f1debd5 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpAllSwitches.json @@ -0,0 +1,286 @@ +{ + "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 + }, + "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": [ + { + "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 new file mode 100644 index 000000000..80706b13e --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/fixtures/responses_EpFabrics.json @@ -0,0 +1,165 @@ +{ + "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 + }, + "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": [ + { + "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/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_common.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py new file mode 100644 index 000000000..734cc5826 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_common.py @@ -0,0 +1,428 @@ +# 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 + +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 \ + 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) + + +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() + + +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"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_merged.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py new file mode 100644 index 000000000..c4b43a40c --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_merged.py @@ -0,0 +1,925 @@ +# 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.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 + + +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() + - 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_test) + 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) + + 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 + + +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 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_query.py b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_query.py new file mode 100644 index 000000000..934912b82 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_query.py @@ -0,0 +1,373 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# 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 + + +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() 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..31d79f753 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/test_dcnm_maintenance_mode_want.py @@ -0,0 +1,601 @@ +# 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. + """ + 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 ``items_key`` 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.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: # 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(): + 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`` + 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" + + 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() + + +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() 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..7ce1e6082 --- /dev/null +++ b/tests/unit/modules/dcnm/dcnm_maintenance_mode/utils.py @@ -0,0 +1,328 @@ +# 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.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 \ + 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"}]}, + "check_mode": False, +} + + +class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + + check_mode = False + + params = params + 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 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 + """ + data_file = "configs_Want" + data = load_fixture(data_file).get(key) + print(f"{data_file}: {key} : {data}") + 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 + """ + 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_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 EpFabrics(). + """ + data_file = "responses_EpFabrics" + 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 8574383a58e5bc7f2a698fb713b57ad4dd0cff14 Mon Sep 17 00:00:00 2001 From: mmudigon <62759545+mmudigon@users.noreply.github.com> Date: Fri, 26 Jul 2024 20:53:27 +0530 Subject: [PATCH 6/6] Case agnostic checks for interface names in dcnm_links module (#300) --- plugins/modules/dcnm_links.py | 267 ++++++++++++------ .../dcnm/fixtures/dcnm_links_payloads.json | 66 ++--- 2 files changed, 211 insertions(+), 122 deletions(-) diff --git a/plugins/modules/dcnm_links.py b/plugins/modules/dcnm_links.py index 139421bc4..cb27a35c4 100644 --- a/plugins/modules/dcnm_links.py +++ b/plugins/modules/dcnm_links.py @@ -899,8 +899,7 @@ class DcnmLinks: "ext_multisite_underlay_setup": "ext_multisite_underlay_setup_11_1", "ext_evpn_multisite_overlay_setup": "ext_evpn_multisite_overlay_setup", "ext_vxlan_mpls_overlay_setup": "ext_vxlan_mpls_overlay_setup", - "ext_vxlan_mpls_underlay_setup": "ext_vxlan_mpls_underlay_setup" - + "ext_vxlan_mpls_underlay_setup": "ext_vxlan_mpls_underlay_setup", }, 12: { "int_intra_fabric_ipv6_link_local": "int_intra_fabric_ipv6_link_local", @@ -913,7 +912,7 @@ class DcnmLinks: "ext_multisite_underlay_setup": "ext_multisite_underlay_setup", "ext_evpn_multisite_overlay_setup": "ext_evpn_multisite_overlay_setup", "ext_vxlan_mpls_overlay_setup": "ext_vxlan_mpls_overlay_setup", - "ext_vxlan_mpls_underlay_setup": "ext_vxlan_mpls_underlay_setup" + "ext_vxlan_mpls_underlay_setup": "ext_vxlan_mpls_underlay_setup", }, } @@ -929,7 +928,7 @@ class DcnmLinks: "ext_multisite_underlay_setup_11_1", "ext_evpn_multisite_overlay_setup", "ext_vxlan_mpls_overlay_setup", - "ext_vxlan_mpls_underlay_setup" + "ext_vxlan_mpls_underlay_setup", ], 12: [ "int_intra_fabric_ipv6_link_local", @@ -942,7 +941,7 @@ class DcnmLinks: "ext_multisite_underlay_setup", "ext_evpn_multisite_overlay_setup", "ext_vxlan_mpls_overlay_setup", - "ext_vxlan_mpls_underlay_setup" + "ext_vxlan_mpls_underlay_setup", ], } @@ -1229,7 +1228,9 @@ def dcnm_links_get_intra_fabric_link_spec(self, cfg): ) ): if ( - self.src_fabric_info["nvPairs"]["UNDERLAY_IS_V6"].lower() + self.src_fabric_info["nvPairs"] + .get("UNDERLAY_IS_V6", "false") + .lower() == "false" ): link_spec["profile"]["peer1_ipv4_addr"] = dict( @@ -1325,12 +1326,18 @@ def dcnm_links_get_inter_fabric_link_spec(self, cfg): self.module.fail_json(msg="Required parameter not found: template") if ( - cfg[0].get("template", "") - == self.templates["ext_multisite_underlay_setup"] - ) or ( - cfg[0].get("template", "") == self.templates["ext_fabric_setup"] - ) or ( - cfg[0].get("template", "") == self.templates["ext_vxlan_mpls_underlay_setup"] + ( + cfg[0].get("template", "") + == self.templates["ext_multisite_underlay_setup"] + ) + or ( + cfg[0].get("template", "") + == self.templates["ext_fabric_setup"] + ) + or ( + cfg[0].get("template", "") + == self.templates["ext_vxlan_mpls_underlay_setup"] + ) ): link_spec["profile"]["ipv4_subnet"] = dict( required=True, type="ipv4_subnet" @@ -1344,7 +1351,10 @@ def dcnm_links_get_inter_fabric_link_spec(self, cfg): ) link_spec["profile"]["peer1_cmds"] = dict(type="list", default=[]) link_spec["profile"]["peer2_cmds"] = dict(type="list", default=[]) - elif cfg[0].get("template") != self.templates["ext_vxlan_mpls_overlay_setup"]: + elif ( + cfg[0].get("template") + != self.templates["ext_vxlan_mpls_overlay_setup"] + ): link_spec["profile"]["ipv4_addr"] = dict( required=True, type="ipv4" ) @@ -1352,12 +1362,18 @@ def dcnm_links_get_inter_fabric_link_spec(self, cfg): link_spec["profile"]["neighbor_ip"] = dict(required=True, type="ipv4") # src_asn and dst_asn are not common parameters if ( - cfg[0].get("template", "") - == self.templates["ext_multisite_underlay_setup"] - ) or ( - cfg[0].get("template", "") == self.templates["ext_fabric_setup"] - ) or ( - cfg[0].get("template", "") == self.templates["ext_vxlan_mpls_overlay_setup"] + ( + cfg[0].get("template", "") + == self.templates["ext_multisite_underlay_setup"] + ) + or ( + cfg[0].get("template", "") + == self.templates["ext_fabric_setup"] + ) + or ( + cfg[0].get("template", "") + == self.templates["ext_vxlan_mpls_overlay_setup"] + ) ): link_spec["profile"]["src_asn"] = dict(required=True, type="int") link_spec["profile"]["dst_asn"] = dict(required=True, type="int") @@ -1610,10 +1626,16 @@ def dcnm_links_get_inter_fabric_links_payload(self, link, link_payload): """ link_payload["nvPairs"] = {} if ( - link["template"] == self.templates["ext_multisite_underlay_setup"] - ) or ( - link["template"] == self.templates["ext_fabric_setup"] - ) or (link["template"] == self.templates["ext_vxlan_mpls_underlay_setup"]): + ( + link["template"] + == self.templates["ext_multisite_underlay_setup"] + ) + or (link["template"] == self.templates["ext_fabric_setup"]) + or ( + link["template"] + == self.templates["ext_vxlan_mpls_underlay_setup"] + ) + ): ip_prefix = link["profile"]["ipv4_subnet"].split("/") link_payload["nvPairs"]["IP_MASK"] = ( @@ -1640,7 +1662,9 @@ def dcnm_links_get_inter_fabric_links_payload(self, link, link_payload): link_payload["nvPairs"]["PEER2_CONF"] = "\n".join( link["profile"].get("peer2_cmds") ) - elif link["template"] != self.templates["ext_vxlan_mpls_overlay_setup"]: + elif ( + link["template"] != self.templates["ext_vxlan_mpls_overlay_setup"] + ): link_payload["nvPairs"]["SOURCE_IP"] = str( ipaddress.ip_address(link["profile"]["ipv4_addr"]) ) @@ -1649,12 +1673,20 @@ def dcnm_links_get_inter_fabric_links_payload(self, link, link_payload): ipaddress.ip_address(link["profile"]["neighbor_ip"]) ) if ( - link["template"] == self.templates["ext_multisite_underlay_setup"] - ) or ( - link["template"] == self.templates["ext_fabric_setup"] - ) or (link["template"] == self.templates["ext_vxlan_mpls_overlay_setup"]): + ( + link["template"] + == self.templates["ext_multisite_underlay_setup"] + ) + or (link["template"] == self.templates["ext_fabric_setup"]) + or ( + link["template"] + == self.templates["ext_vxlan_mpls_overlay_setup"] + ) + ): link_payload["nvPairs"]["asn"] = link["profile"]["src_asn"] - link_payload["nvPairs"]["NEIGHBOR_ASN"] = link["profile"]["dst_asn"] + link_payload["nvPairs"]["NEIGHBOR_ASN"] = link["profile"][ + "dst_asn" + ] if link["template"] == self.templates["ext_fabric_setup"]: link_payload["nvPairs"]["AUTO_VRF_LITE_FLAG"] = link["profile"][ @@ -1707,13 +1739,27 @@ def dcnm_links_get_inter_fabric_links_payload(self, link, link_payload): "profile" ]["ebgp_auth_key_type"] if link["template"] == self.templates["ext_vxlan_mpls_underlay_setup"]: - link_payload["nvPairs"]["MPLS_FABRIC"] = link["profile"]["mpls_fabric"] - link_payload["nvPairs"]["DCI_ROUTING_PROTO"] = link["profile"]["dci_routing_proto"] - link_payload["nvPairs"]["DCI_ROUTING_TAG"] = link["profile"]["dci_routing_tag"] - link_payload["nvPairs"]["PEER1_SR_MPLS_INDEX"] = link["profile"]["peer1_sr_mpls_index"] - link_payload["nvPairs"]["PEER2_SR_MPLS_INDEX"] = link["profile"]["peer2_sr_mpls_index"] - link_payload["nvPairs"]["GB_BLOCK_RANGE"] = link["profile"]["global_block_range"] - link_payload["nvPairs"]["OSPF_AREA_ID"] = link["profile"]["ospf_area_id"] + link_payload["nvPairs"]["MPLS_FABRIC"] = link["profile"][ + "mpls_fabric" + ] + link_payload["nvPairs"]["DCI_ROUTING_PROTO"] = link["profile"][ + "dci_routing_proto" + ] + link_payload["nvPairs"]["DCI_ROUTING_TAG"] = link["profile"][ + "dci_routing_tag" + ] + link_payload["nvPairs"]["PEER1_SR_MPLS_INDEX"] = link["profile"][ + "peer1_sr_mpls_index" + ] + link_payload["nvPairs"]["PEER2_SR_MPLS_INDEX"] = link["profile"][ + "peer2_sr_mpls_index" + ] + link_payload["nvPairs"]["GB_BLOCK_RANGE"] = link["profile"][ + "global_block_range" + ] + link_payload["nvPairs"]["OSPF_AREA_ID"] = link["profile"][ + "ospf_area_id" + ] def dcnm_links_get_intra_fabric_links_payload(self, link, link_payload): @@ -1771,7 +1817,9 @@ def dcnm_links_get_intra_fabric_links_payload(self, link, link_payload): ) ): if ( - self.src_fabric_info["nvPairs"]["UNDERLAY_IS_V6"].lower() + self.src_fabric_info["nvPairs"] + .get("UNDERLAY_IS_V6", "false") + .lower() == "false" ): link_payload["nvPairs"]["PEER1_IP"] = str( @@ -1869,12 +1917,15 @@ def dcnm_links_update_inter_fabric_links_information( return if ( - wlink["templateName"] - == self.templates["ext_multisite_underlay_setup"] - ) or ( - wlink["templateName"] == self.templates["ext_fabric_setup"] - ) or ( - wlink["templateName"] == self.templates["ext_vxlan_mpls_underlay_setup"] + ( + wlink["templateName"] + == self.templates["ext_multisite_underlay_setup"] + ) + or (wlink["templateName"] == self.templates["ext_fabric_setup"]) + or ( + wlink["templateName"] + == self.templates["ext_vxlan_mpls_underlay_setup"] + ) ): if cfg["profile"].get("ipv4_subnet", None) is None: wlink["nvPairs"]["IP_MASK"] = hlink["nvPairs"]["IP_MASK"] @@ -1895,7 +1946,10 @@ def dcnm_links_update_inter_fabric_links_information( if cfg["profile"].get("peer2_cmds", None) is None: wlink["nvPairs"]["PEER2_CONF"] = hlink["nvPairs"]["PEER2_CONF"] wlink["peer2_conf_defaulted"] = True - elif wlink["templateName"] != self.templates["ext_vxlan_mpls_overlay_setup"]: + elif ( + wlink["templateName"] + != self.templates["ext_vxlan_mpls_overlay_setup"] + ): if cfg["profile"].get("ipv4_addr", None) is None: wlink["nvPairs"]["SOURCE_IP"] = hlink["nvPairs"]["SOURCE_IP"] @@ -1908,16 +1962,22 @@ def dcnm_links_update_inter_fabric_links_information( wlink["nvPairs"]["NEIGHBOR_IP"] = hlink["nvPairs"]["NEIGHBOR_IP"] if ( - wlink["templateName"] == self.templates["ext_multisite_underlay_setup"] - ) or ( - wlink["templateName"] == self.templates["ext_fabric_setup"] - ) or ( - wlink["templateName"] == self.templates["ext_vxlan_mpls_overlay_setup"] + ( + wlink["templateName"] + == self.templates["ext_multisite_underlay_setup"] + ) + or (wlink["templateName"] == self.templates["ext_fabric_setup"]) + or ( + wlink["templateName"] + == self.templates["ext_vxlan_mpls_overlay_setup"] + ) ): if cfg["profile"].get("src_asn", None) is None: wlink["nvPairs"]["asn"] = hlink["nvPairs"]["asn"] if cfg["profile"].get("dst_asn", None) is None: - wlink["nvPairs"]["NEIGHBOR_ASN"] = hlink["nvPairs"]["NEIGHBOR_ASN"] + wlink["nvPairs"]["NEIGHBOR_ASN"] = hlink["nvPairs"][ + "NEIGHBOR_ASN" + ] if wlink["templateName"] == self.templates["ext_fabric_setup"]: if cfg["profile"].get("auto_deploy", None) is None: @@ -2026,7 +2086,9 @@ def dcnm_links_update_intra_fabric_links_information( ) ): if ( - self.src_fabric_info["nvPairs"]["UNDERLAY_IS_V6"].lower() + self.src_fabric_info["nvPairs"] + .get("UNDERLAY_IS_V6", "false") + .lower() == "false" ): if cfg["profile"].get("peer1_ipv4_addr", None) is None: @@ -2112,11 +2174,12 @@ def dcnm_links_update_want(self): == want["destinationFabric"] ) and ( - have["sw1-info"]["if-name"] == want["sourceInterface"] + have["sw1-info"]["if-name"].lower() + == want["sourceInterface"].lower() ) and ( - have["sw2-info"]["if-name"] - == want["destinationInterface"] + have["sw2-info"]["if-name"].lower() + == want["destinationInterface"].lower() ) and ( have["sw1-info"]["sw-serial-number"] @@ -2280,12 +2343,12 @@ def dcnm_links_get_links_info_from_dcnm(self, link): ) ) and ( - link["sourceInterface"] - == link_elem["sw1-info"]["if-name"] + link["sourceInterface"].lower() + == link_elem["sw1-info"]["if-name"].lower() ) and ( - link["destinationInterface"] - == link_elem["sw2-info"]["if-name"] + link["destinationInterface"].lower() + == link_elem["sw2-info"]["if-name"].lower() ) ) ] @@ -2354,11 +2417,16 @@ def dcnm_links_compare_inter_fabric_link_params(self, wlink, hlink): return "DCNM_LINK_EXIST", [], [] if ( - wlink["templateName"] - == self.templates["ext_multisite_underlay_setup"] - ) or ( - wlink["templateName"] == self.templates["ext_fabric_setup"] - ) or (wlink["templateName"] == self.templates["ext_vxlan_mpls_underlay_setup"]): + ( + wlink["templateName"] + == self.templates["ext_multisite_underlay_setup"] + ) + or (wlink["templateName"] == self.templates["ext_fabric_setup"]) + or ( + wlink["templateName"] + == self.templates["ext_vxlan_mpls_underlay_setup"] + ) + ): if ( self.dcnm_links_compare_ip_addresses( wlink["nvPairs"]["IP_MASK"], hlink["nvPairs"]["IP_MASK"] @@ -2434,7 +2502,10 @@ def dcnm_links_compare_inter_fabric_link_params(self, wlink, hlink): ] } ) - elif wlink["templateName"] != self.templates["ext_vxlan_mpls_overlay_setup"]: + elif ( + wlink["templateName"] + != self.templates["ext_vxlan_mpls_overlay_setup"] + ): if ( self.dcnm_links_compare_ip_addresses( wlink["nvPairs"]["SOURCE_IP"], @@ -2467,11 +2538,16 @@ def dcnm_links_compare_inter_fabric_link_params(self, wlink, hlink): } ) if ( - wlink["templateName"] - == self.templates["ext_multisite_underlay_setup"] - ) or ( - wlink["templateName"] == self.templates["ext_fabric_setup"] - ) or (wlink["templateName"] == self.templates["ext_vxlan_mpls_overlay_setup"]): + ( + wlink["templateName"] + == self.templates["ext_multisite_underlay_setup"] + ) + or (wlink["templateName"] == self.templates["ext_fabric_setup"]) + or ( + wlink["templateName"] + == self.templates["ext_vxlan_mpls_overlay_setup"] + ) + ): if ( str(wlink["nvPairs"]["asn"]).lower() != str(hlink["nvPairs"]["asn"]).lower() @@ -2699,7 +2775,8 @@ def dcnm_links_compare_inter_fabric_link_params(self, wlink, hlink): "PEER1_SR_MPLS_INDEX", "PEER2_SR_MPLS_INDEX", "GB_BLOCK_RANGE", - "OSPF_AREA_ID"] + "OSPF_AREA_ID", + ] for nv_pair in mpls_underlay_spec_nvpairs: if ( str(wlink["nvPairs"][nv_pair]).lower() @@ -2708,12 +2785,8 @@ def dcnm_links_compare_inter_fabric_link_params(self, wlink, hlink): mismatch_reasons.append( { f"{nv_pair}_MISMATCH": [ - str( - wlink["nvPairs"][nv_pair] - ).lower(), - str( - hlink["nvPairs"][nv_pair] - ).lower(), + str(wlink["nvPairs"][nv_pair]).lower(), + str(hlink["nvPairs"][nv_pair]).lower(), ] } ) @@ -2835,7 +2908,9 @@ def dcnm_links_compare_intra_fabric_link_params(self, wlink, hlink): ) ): if ( - self.src_fabric_info["nvPairs"]["UNDERLAY_IS_V6"].lower() + self.src_fabric_info["nvPairs"] + .get("UNDERLAY_IS_V6", "false") + .lower() == "false" ): if ( @@ -3041,15 +3116,21 @@ def dcnm_links_compare_Links(self, want): have for have in self.have if ( - (have.get("templateName") is not None) # if templateName is empty, link is autodicovered, consider link is new + ( + have.get("templateName") is not None + ) # if templateName is empty, link is autodicovered, consider link is new and (have["sw1-info"]["fabric-name"] == want["sourceFabric"]) and ( have["sw2-info"]["fabric-name"] == want["destinationFabric"] ) - and (have["sw1-info"]["if-name"] == want["sourceInterface"]) and ( - have["sw2-info"]["if-name"] == want["destinationInterface"] + have["sw1-info"]["if-name"].lower() + == want["sourceInterface"].lower() + ) + and ( + have["sw2-info"]["if-name"].lower() + == want["destinationInterface"].lower() ) and ( have["sw1-info"]["sw-serial-number"] @@ -3057,9 +3138,11 @@ def dcnm_links_compare_Links(self, want): ) and ( have["sw2-info"]["sw-serial-number"] - == want["destinationDevice"] or - have["sw2-info"]["sw-serial-number"] - == want["destinationSwitchName"] + "-" + want["destinationFabric"] + == want["destinationDevice"] + or have["sw2-info"]["sw-serial-number"] + == want["destinationSwitchName"] + + "-" + + want["destinationFabric"] ) ) ] @@ -3238,8 +3321,14 @@ def dcnm_links_get_diff_deleted(self): if ( (have["sw1-info"]["fabric-name"] == self.fabric) and (have["sw2-info"]["fabric-name"] == link["dst_fabric"]) - and (have["sw1-info"]["if-name"] == link["src_interface"]) - and (have["sw2-info"]["if-name"] == link["dst_interface"]) + and ( + have["sw1-info"]["if-name"].lower() + == link["src_interface"].lower() + ) + and ( + have["sw2-info"]["if-name"].lower() + == link["dst_interface"].lower() + ) and ( link["src_device"] in self.ip_sn and have["sw1-info"]["sw-serial-number"] @@ -3343,15 +3432,15 @@ def dcnm_links_get_diff_query(self): and ( (link["src_interface"] == "") or ( - rlink["sw1-info"]["if-name"] - == link["src_interface"] + rlink["sw1-info"]["if-name"].lower() + == link["src_interface"].lower() ) ) and ( (link["dst_interface"] == "") or ( - rlink["sw2-info"]["if-name"] - == link["dst_interface"] + rlink["sw2-info"]["if-name"].lower() + == link["dst_interface"].lower() ) ) and ( diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_links_payloads.json b/tests/unit/modules/dcnm/fixtures/dcnm_links_payloads.json index a02222aaf..fec270c3e 100644 --- a/tests/unit/modules/dcnm/fixtures/dcnm_links_payloads.json +++ b/tests/unit/modules/dcnm/fixtures/dcnm_links_payloads.json @@ -328,7 +328,7 @@ "link-uuid": "LINK-UUID-1446600", "sw2-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/1", + "if-name": "ethernet1/1", "sw-sys-name": "n9kv-num-2", "sw-serial-number": "9FX7O3TU2QM" }, @@ -388,7 +388,7 @@ "templateName": "int_pre_provision_intra_fabric_link", "sw1-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/2", + "if-name": "ethernet1/2", "sw-sys-name": "n9kv-num-1", "sw-serial-number": "9IF87L089SZ" }, @@ -406,7 +406,7 @@ "link-uuid": "LINK-UUID-1419160", "sw2-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/3", + "if-name": "ethernet1/3", "sw-sys-name": "n9kv-num-2", "sw-serial-number": "9FX7O3TU2QM" }, @@ -431,7 +431,7 @@ "templateName": "ios_xe_int_intra_fabric_num_link", "sw1-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/3", + "if-name": "ethernet1/3", "sw-sys-name": "n9kv-num-1", "sw-serial-number": "9IF87L089SZ" }, @@ -449,7 +449,7 @@ "link-uuid": "LINK-UUID-1435300", "sw2-info": { "fabric-name": "mmudigon-unnumbered", - "if-name": "Ethernet1/1", + "if-name": "ethernet1/1", "sw-sys-name": "n9kv-unnum-2", "sw-serial-number": "9AF3VNZYAKS" }, @@ -502,7 +502,7 @@ "templateName": "int_pre_provision_intra_fabric_link", "sw1-info": { "fabric-name": "mmudigon-unnumbered", - "if-name": "Ethernet1/2", + "if-name": "ethernet1/2", "sw-sys-name": "n9kv-unnum-1", "sw-serial-number": "9EFX823RUL3" }, @@ -520,7 +520,7 @@ "link-uuid": "LINK-UUID-1419050", "sw2-info": { "fabric-name": "mmudigon-ipv6-underlay", - "if-name": "Ethernet1/1", + "if-name": "ethernet1/1", "sw-sys-name": "n9kv-ipv6-2", "sw-serial-number": "9ITWBH9OIAH" }, @@ -564,7 +564,7 @@ "link-uuid": "LINK-UUID-1419110", "sw2-info": { "fabric-name": "mmudigon-ipv6-underlay", - "if-name": "Ethernet1/2", + "if-name": "ethernet1/2", "sw-sys-name": "n9kv-ipv6-2", "sw-serial-number": "9ITWBH9OIAH" }, @@ -574,7 +574,7 @@ "templateName": "int_pre_provision_intra_fabric_link", "sw1-info": { "fabric-name": "mmudigon-ipv6-underlay", - "if-name": "Ethernet1/2", + "if-name": "ethernet1/2", "sw-sys-name": "n9kv-ipv6-1", "sw-serial-number": "9BH0813WFWT" }, @@ -592,7 +592,7 @@ "link-uuid": "LINK-UUID-1419180", "sw2-info": { "fabric-name": "mmudigon-ipv6-underlay", - "if-name": "Ethernet1/3", + "if-name": "ethernet1/3", "sw-sys-name": "n9kv-ipv6-2", "sw-serial-number": "9ITWBH9OIAH" }, @@ -669,7 +669,7 @@ "templateName": "int_intra_vpc_peer_keep_alive_link", "sw1-info": { "fabric-name": "mmudigon", - "if-name": "Ethernet1/4", + "if-name": "ethernet1/4", "sw-sys-name": "n9kv-100", "sw-serial-number": "9M99N34RDED" }, @@ -687,14 +687,14 @@ "link-uuid": "LINK-UUID-1446600", "templateName": "ext_fabric_setup", "sw2-info": { - "if-name": "Ethernet1/3", + "if-name": "ethernet1/3", "sw-sys-name": "test22", "sw-serial-number": "9E15XSEM5MS", "fabric-name": "test_net" }, "sw1-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/3", + "if-name": "ethernet1/3", "sw-sys-name": "n9kv-227", "sw-serial-number": "953E68OKK1L" }, @@ -742,7 +742,7 @@ "link-dbid": 1467700, "sw1-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/4", + "if-name": "ethernet1/4", "sw-serial-number": "953E68OKK1L", "sw-sys-name": "n9kv-test3" }, @@ -785,7 +785,7 @@ "link-uuid": "LINK-UUID-1446600", "templateName": "ext_evpn_multisite_overlay_setup", "sw2-info": { - "if-name": "Ethernet1/5", + "if-name": "ethernet1/5", "sw-serial-number": "9E15XSEM5MS", "fabric-name": "test_net", "sw-sys-name": "test22" @@ -837,7 +837,7 @@ }, "sw1-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/6", + "if-name": "ethernet1/6", "sw-serial-number": "953E68OKK1L", "sw-sys-name": "n9kv-test3" }, @@ -876,7 +876,7 @@ "link-uuid": "LINK-UUID-1446600", "templateName": "ext_multisite_underlay_setup", "sw2-info": { - "if-name": "Ethernet1/7", + "if-name": "ethernet1/7", "sw-serial-number": "98YWRN9WCSC", "fabric-name": "test_net1", "sw-sys-name": "n9kv-test1" @@ -934,7 +934,7 @@ }, "sw1-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/8", + "if-name": "ethernet1/8", "sw-serial-number": "953E68OKK1L", "sw-sys-name": "n9kv-test3" }, @@ -970,14 +970,14 @@ "DATA": { "link-uuid": "LINK-UUID-1446600", "sw2-info": { - "if-name": "Ethernet1/9", + "if-name": "ethernet1/9", "sw-serial-number": "98YWRN9WCSC", "fabric-name": "test_net1", "sw-sys-name": "n9kv-test1" }, "sw1-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/9", + "if-name": "ethernet1/9", "sw-serial-number": "953E68OKK1L", "sw-sys-name": "n9kv-test3" } @@ -1112,7 +1112,7 @@ }, "sw1-info": { "fabric-name": "mmudigon-unnumbered", - "if-name": "Ethernet1/1", + "if-name": "ethernet1/1", "sw-serial-number": "9EFX823RUL3" }, "sw2-info": { @@ -1134,7 +1134,7 @@ }, "sw2-info": { "fabric-name": "mmudigon-unnumbered", - "if-name": "Ethernet1/2", + "if-name": "ethernet1/2", "sw-serial-number": "9AF3VNZYAKS" }, "templateName": "int_pre_provision_intra_fabric_link" @@ -1173,13 +1173,13 @@ }, "sw1-info": { "fabric-name": "mmudigon-ipv6-underlay", - "if-name": "Ethernet1/1", + "if-name": "ethernet1/1", "if-op-reason": "Link not connected", "sw-serial-number": "9BH0813WFWT" }, "sw2-info": { "fabric-name": "mmudigon-ipv6-underlay", - "if-name": "Ethernet1/1", + "if-name": "ethernet1/1", "sw-serial-number": "9ITWBH9OIAH" }, "templateName": "int_intra_fabric_ipv6_link_local" @@ -1230,7 +1230,7 @@ }, "sw1-info": { "fabric-name": "mmudigon-ipv6-underlay", - "if-name": "Ethernet1/3", + "if-name": "ethernet1/3", "sw-serial-number": "9BH0813WFWT" }, "sw2-info": { @@ -1273,12 +1273,12 @@ }, "sw1-info": { "fabric-name": "mmudigon", - "if-name": "Ethernet1/4", + "if-name": "ethernet1/4", "sw-serial-number": "9M99N34RDED" }, "sw2-info": { "fabric-name": "mmudigon", - "if-name": "Ethernet1/4", + "if-name": "ethernet1/4", "sw-serial-number": "9NXHSNTEO6C" }, "templateName": "int_intra_vpc_peer_keep_alive_link" @@ -1320,7 +1320,7 @@ }, "sw2-info": { "fabric-name": "test_net1", - "if-name": "Ethernet1/8", + "if-name": "ethernet1/8", "sw-serial-number": "98YWRN9WCSC" }, "templateName": "ext_evpn_multisite_overlay_setup" @@ -1355,7 +1355,7 @@ }, "sw1-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/7", + "if-name": "ethernet1/7", "sw-serial-number": "953E68OKK1L" }, "sw2-info": { @@ -1424,12 +1424,12 @@ }, "sw1-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/5", + "if-name": "ethernet1/5", "sw-serial-number": "953E68OKK1L" }, "sw2-info": { "fabric-name": "test_net", - "if-name": "Ethernet1/5", + "if-name": "ethernet1/5", "sw-serial-number": "9E15XSEM5MS" }, "templateName": "ext_evpn_multisite_overlay_setup" @@ -1463,7 +1463,7 @@ }, "sw1-info": { "fabric-name": "mmudigon-numbered", - "if-name": "Ethernet1/4", + "if-name": "ethernet1/4", "sw-serial-number": "953E68OKK1L" }, "sw2-info": { @@ -1504,7 +1504,7 @@ }, "sw2-info": { "fabric-name": "test_net", - "if-name": "Ethernet1/3", + "if-name": "ethernet1/3", "sw-serial-number": "9E15XSEM5MS" }, "templateName": "ext_fabric_setup"