diff --git a/README.md b/README.md index 88e70db6dc..537e39bee7 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Other versions of this collection have support for previous Cisco DNA Center ver | 2.1.1 | 3.0.0 | 2.2.5 | | 2.2.2.3 | 3.3.1 | 2.3.3 | | 2.2.3.3 | 6.4.0 | 2.4.11 | -| 2.3.3.0 | 6.6.1 | 2.5.5 | +| 2.3.3.0 | 6.6.2 | 2.5.5 | *Notes*: diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index ae0cc4fcb6..b5ef906714 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -607,4 +607,12 @@ releases: release_summary: Added the ability to export DNA Center credentials as environment variables and rennamed some vars. bugfixes: - DNA Center credentials can now be exported and used as env vars. - - sda_virtual_network_ip_pool - Now pass the site_name_hierarchy correctly in get method. \ No newline at end of file + - sda_virtual_network_ip_pool - Now pass the site_name_hierarchy correctly in get method. + 6.6.2: + release_date: "2022-12-19" + changes: + release_summary: + bugfixes: + - sda_fabric_border_device - fix Create example at EXAMPLES block + - site_intent - fix Case_1 return example at RETURN block + - swim_intent - fix functionality and tests \ No newline at end of file diff --git a/galaxy.yml b/galaxy.yml index c0de720977..7cb92c09c4 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.6.1 +version: 6.6.2 readme: README.md authors: - Rafael Campos diff --git a/plugins/modules/sda_fabric_border_device.py b/plugins/modules/sda_fabric_border_device.py index 1f1ef5d25c..3099cd0ec8 100644 --- a/plugins/modules/sda_fabric_border_device.py +++ b/plugins/modules/sda_fabric_border_device.py @@ -165,7 +165,7 @@ vlanName: string l3Handoff: - virtualNetwork: - - virtualNetworkName: string + virtualNetworkName: string vlanId: string externalDomainRoutingProtocolName: string internalAutonomouSystemNumber: string diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index eab7335702..a9dbb109e6 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -149,12 +149,13 @@ "endTime": String, "endTimeEpoch": 0, "runtimeInstanceId": String, + "siteId": String, "startTime": String, "startTimeEpoch": 0, "status": String, "timeDuration": 0 - } + }, "msg": "string" } @@ -165,7 +166,12 @@ type: dict sample: > { - "response": {}, + "response": + { + "site": {}, + "siteId": String, + "type": String + }, "msg": String } @@ -190,7 +196,7 @@ "status": String, "timeDuration": 0 - } + }, "msg": "string" } @@ -301,7 +307,7 @@ def get_current_site(self, site): building=dict( name=site[0].get("name"), parentName=site[0].get("siteNameHierarchy").split("/" + site[0].get("name"))[0], - address=location.get("attributes").get("address"), + address=location.get("attributes").get("address", ""), latitude=location.get("attributes").get("latitude"), longitude=location.get("attributes").get("longitude"), ) @@ -326,7 +332,7 @@ def get_current_site(self, site): current_site = dict( type=typeinfo, site=site_info, - site_id=site[0].get("id") + siteId=site[0].get("id") ) if self.log: @@ -430,7 +436,7 @@ def get_have(self): log("Site Exists: " + str(site_exists) + "\n Current Site:" + str(current_site)) if site_exists: - have["site_id"] = current_site.get("site_id") + have["site_id"] = current_site.get("siteId") have["site_exists"] = site_exists have["current_site"] = current_site @@ -501,6 +507,7 @@ def get_diff_merge(self): if site_updated: log("Site Updated Successfully") self.result['msg'] = "Site Updated Successfully" + self.result['response'].update({"siteId": self.have.get("site_id")}) else: # Get the site id of the newly created site. @@ -510,6 +517,7 @@ def get_diff_merge(self): log("Site Created Successfully") log("Current site:" + str(current_site)) self.result['msg'] = "Site Created Successfully" + self.result['response'].update({"siteId": current_site.get('site_id')}) def get_diff_delete(self): site_exists = self.have.get("site_exists") @@ -528,6 +536,7 @@ def get_diff_delete(self): if execution_details.get("status") == "SUCCESS": self.result['changed'] = True self.result['response'] = execution_details + self.result['response'].update({"siteId": self.have.get("site_id")}) self.result['msg'] = "Site deleted successfully" break diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index e518d1460d..ce9cb4f733 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -393,6 +393,8 @@ def get_device_family_identifier(self, family_name): family="software_image_management_swim", function='get_device_family_identifiers', ) + if self.log: + log(str(response)) device_family_db = response.get("response") if device_family_db: device_family_details = get_dict_result(device_family_db, 'deviceFamily', family_name) @@ -402,7 +404,7 @@ def get_device_family_identifier(self, family_name): if self.log: log("Family device indentifier:" + str(device_family_identifier)) else: - self.module.fail_json(msg="Family Device Name not found") + self.module.fail_json(msg="Family Device Name not found", response=[]) self.have.update(have) def get_have(self): @@ -535,13 +537,17 @@ def get_diff_import(self): if task_details and task_details.get("isError"): if "Image already exists" in task_details.get("failureReason"): + self.result['msg'] = "Image already exists." break else: self.module.fail_json(msg=task_details.get("failureReason"), response=task_details) self.result['response'] = task_details if task_details else response - # Fetch image_id if the imported image for further use + if not (self.want.get("tagging_details") or self.want.get("distribution_details") + or self.want.get("activation_details")): + return + # Fetch image_id for the imported image for further use image_name = self.want.get("url_import_details").get("payload")[0].get("sourceURL") image_name = image_name.split('/')[-1] image_id = self.get_image_id(image_name) @@ -571,7 +577,7 @@ def get_diff_tagging(self): else: image_params = dict( image_id=self.have.get("tagging_image_id"), - ite_id=self.have.get("site_id"), + site_id=self.have.get("site_id"), device_family_identifier=self.have.get("device_family_identifier"), device_role=tagging_details.get("deviceRole") ) diff --git a/tests/unit/modules/dnac/fixtures/swim_intent.json b/tests/unit/modules/dnac/fixtures/swim_intent.json new file mode 100644 index 0000000000..d10d077be9 --- /dev/null +++ b/tests/unit/modules/dnac/fixtures/swim_intent.json @@ -0,0 +1,211 @@ +{ + "playbook_config": [{ + "importImageDetails": { + "type": "url", + "urlDetails": { + "payload": [{ + "sourceURL":"http://10.104.118.10/swim/17.9/cat9k_iosxe.BLD_V179_THROTTLE_LATEST_20220429_033422.SSA.bin", + "isThirdParty": 0 + }] + } + }, + "taggingDetails": { + "deviceRole": "ALL", + "deviceFamilyName": "Cisco Catalyst 9606R Switch-Cisco Catalyst 9600 Series Supervisor Engine 1", + "tagging": true + }, + "imageDistributionDetails": { + "deviceSerialNumber": "FXS2325Q01C" + }, + "imageActivationDetails": { + "scehduleValidate": false, + "activateLowerImageVersion": true, + "deviceSerialNumber": "FXS2325Q01C" + } + }], + "playbook_config_image_import": [{ + "importImageDetails": { + "type": "url", + "urlDetails": { + "payload": [{ + "sourceURL":"http://10.104.118.10/swim/17.9/cat9k_iosxe.BLD_V179_THROTTLE_LATEST_20220429_033422.SSA.bin", + "isThirdParty": 0 + }] + } + } + }], + "playbook_config_untag_golden_image":[{ + "taggingDetails": { + "imageName": "cat9k_iosxe.BLD_V179_THROTTLE_LATEST_20220429_033422.SSA.bin", + "deviceRole": "ALL", + "deviceFamilyName": "Cisco Catalyst 9606R Switch-Cisco Catalyst 9600 Series Supervisor Engine 1", + "tagging": false, + "siteName": "Global/Bangalore/Trill" + } + }], + "playbook_config_tag_golden_image_missing_param":[{ + "taggingDetails": { + "deviceRole": "ALL", + "deviceFamilyName": "Cisco Catalyst 9606R Switch-Cisco Catalyst 9600 Series Supervisor Engine 1", + "tagging": true, + "siteName": "Global/Bangalore/Trill" + } + }], + "playbook_config_tag_golden_image_incorrect_family_name":[{ + "taggingDetails": { + "imageName": "cat9k_iosxe.BLD_V179_THROTTLE_LATEST_20220429_033422.SSA.bin", + "deviceRole": "ALL", + "deviceFamilyName": "Cisco Catalyst 9600 Series Supervisor Engine 1", + "tagging": true, + "siteName": "Global/Bangalore/Trill" + } + }], + "playbook_config_activation_missing_param":[{ + "imageActivationDetails": { + "scehduleValidate": false, + "activateLowerImageVersion": true, + "deviceSerialNumber": "FXS2325Q01C" + } + }], + "playbook_config_distribution":[{ + "imageDistributionDetails": { + "deviceSerialNumber": "FXS2325Q01C", + "imageName": "cat9k_iosxe.BLD_V179_THROTTLE_LATEST_20220429_033422.SSA.bin" + } + }], + "playbook_config_activation":[{ + "imageActivationDetails": { + "scehduleValidate": false, + "activateLowerImageVersion": true, + "deviceSerialNumber": "FXS2325Q01C", + "imageName": "cat9k_iosxe.BLD_V179_THROTTLE_LATEST_20220429_033422.SSA.bin" + } + }], + "playbook_config_distribution_missing_param":[{ + "imageDistributionDetails": { + "deviceSerialNumber": "FXS2325Q01C" + } + }], + "task_info_response": { + "response": { + "taskId": "0df54111-93ed-41e9-9c6d-2590e395dd45" + } + }, + "image_imported_successfully_response": { + "response": { + "data": "import", + "progress": "completed successfully. Success = 1, Failure = 0, Running = 0, Pending = 0, Total = 1", + "version": 1663130446725, + "endTime": 1663130659133, + "startTime": 1663130446717, + "serviceType": "Swim Service", + "isError": false, + "rootId": "0df54111-93ed-41e9-9c6d-2590e395dd45", + "additionalStatusURL": "/api/v1/image/task?taskUuid=0df54111-93ed-41e9-9c6d-2590e395dd45", + "lastUpdate": 1663130446725, + "instanceTenantId": "626aafc1ceaf922ba8951626", + "id": "0df54111-93ed-41e9-9c6d-2590e395dd45" + }, + "version": "1.0" + }, + "image_id_fetched_successfully_response": { + "response": [{ + "imageUuid": "b03146ee-f93d-457c-a1c1-87f31219f902", + "name": "cat9k_iosxe.BLD_V179_THROTTLE_LATEST_20220429_033422.SSA.bin", + "family": "CAT9K", + "version": "17.09.01.0.29", + "displayVersion": "17.09.01", + "imageName": "cat9k_iosxe.BLD_V179_THROTTLE_LATEST_20220429_033422.SSA.bin", + "isTaggedGolden": false + }] + }, + "image_already_exists_response":{ + "response": { + "data": "import", + "progress": "Workflow Image Import failed. Success = 0, Failure = 1, Running = 0, Pending = 0, Total = 1", + "errorCode": "400", + "serviceType": "Swim Service", + "isError": true, + "failureReason": "NCSW10042: Image already exists" + }, + "version": "1.0" + }, + "image_doesnot_exist_response": { + "response": [], + "version": "1.0" + }, + "device_family_fetched_successfully": { + "response": [{ + "deviceFamily": "Cisco Catalyst 9606R Switch-Cisco Catalyst 9600 Series Supervisor Engine 1", + "deviceFamilyIdentifier": "286322137-286323141" + }] + }, + "device_id_fetched_successfully_response": { + "response": [{ + "type": "Cisco Catalyst 9606R Switch", + "macAddress": "2c:4f:52:05:a9:40", + "serialNumber": "FXS2325Q01C", + "family": "Switches and Hubs", + "platformId": "C9606R", + "series": "Cisco Catalyst 9600 Series Switches", + "instanceUuid": "108d726f-be89-4d7a-8234-44e4808cca4f", + "id": "108d726f-be89-4d7a-8234-44e4808cca4f" + }] + }, + "device_doesnot_exist_response":{ + "response": [], + "version": "1.0" + }, + "tagging_image_successful_response": { + "response": { + "data": "Golden-Tagging", + "progress": "Tagging image as Golden.", + "serviceType": "Swim Service", + "isError": false, + "id": "a98068db-1867-49a5-a470-5c3d2b3c514f" + } + }, + "untagging_image_successful_response": { + "response": { + "data": "Golden-Tagging", + "progress": "Un-tagging image as Golden.", + "serviceType": "Swim Service", + "isError": false, + "id": "a98068db-1867-49a5-a470-5c3d2b3c514f" + } + }, + "image_distribution_successful_response": { + "response": { + "data": "distribute", + "progress": "completed successfully. Success = 1, Failure = 0, Running = 0, Pending = 0, Total = 1", "serviceType": "Swim Service", + "isError": false, + "id": "ba152b35-afde-490f-8575-82c1fce0fab4" + } + }, + "image_activation_successful_response": { + "response": { + "data": "activate", + "progress": "completed successfully. Success = 1, Failure = 0, Running = 0, Pending = 0, Total = 1", "serviceType": "Swim Service", + "isError": false, + "id": "b6f3b7dd-f2b1-46ee-9111-32137226adc0" + } + }, + "fetch_site_id_response": { + "response": [{ + "parentId": "597dba2d-c09f-4bae-ae61-8b0d9c8cd268", + "additionalInfo": [{ + "nameSpace": "Location", + "attributes": { + "country": "India", + "latitude": "12.933971097851867", + "addressInheritedFrom": "597dba2d-c09f-4bae-ae61-8b0d9c8cd268", + "type": "building", + "longitude": "80.2467681804189" + } + }], + "name": "Trill", + "id": "87169daa-7de2-4f9d-814a-d8746de41be6", + "siteNameHierarchy": "Global/Chennai/Trill" + }] + } +} diff --git a/tests/unit/modules/dnac/test_swim_intent.py b/tests/unit/modules/dnac/test_swim_intent.py new file mode 100644 index 0000000000..39a98aa813 --- /dev/null +++ b/tests/unit/modules/dnac/test_swim_intent.py @@ -0,0 +1,309 @@ +# Copyright (c) 2020 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +import pdb + +from dnacentersdk import exceptions +from unittest.mock import patch + +from ansible_collections.cisco.dnac.plugins.modules import swim_intent +from .dnac_module import TestDnacModule, set_module_args, loadPlaybookData + +import json +import copy + + +class TestDnacSwimIntent(TestDnacModule): + + module = swim_intent + test_data = loadPlaybookData("swim_intent") + playbook_config = test_data.get("playbook_config") + playbook_config_untag_image = test_data.get("playbook_config_untag_golden_image") + + def setUp(self): + super(TestDnacSwimIntent, self).setUp() + + self.mock_dnac_init = patch( + "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.__init__") + self.run_dnac_init = self.mock_dnac_init.start() + self.run_dnac_init.side_effect = [None] + self.mock_dnac_exec = patch( + "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.exec" + ) + self.run_dnac_exec = self.mock_dnac_exec.start() + + def tearDown(self): + super(TestDnacSwimIntent, self).tearDown() + self.mock_dnac_exec.stop() + self.mock_dnac_init.stop() + + def load_fixtures(self, response=None, device=""): + if "full_flow" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + self.test_data.get("task_info_response"), + self.test_data.get("image_imported_successfully_response"), + self.test_data.get("image_id_fetched_successfully_response"), + self.test_data.get("device_family_fetched_successfully"), + self.test_data.get("device_id_fetched_successfully_response"), + self.test_data.get("device_id_fetched_successfully_response"), + self.test_data.get("task_info_response"), + self.test_data.get("tagging_image_successful_response"), + self.test_data.get("task_info_response"), + self.test_data.get("image_distribution_successful_response"), + self.test_data.get("task_info_response"), + self.test_data.get("image_activation_successful_response") + ] + elif "swim_image_import" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + self.test_data.get("task_info_response"), + self.test_data.get("image_already_exists_response"), + ] + elif "untag_image" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + self.test_data.get("image_id_fetched_successfully_response"), + self.test_data.get("fetch_site_id_response"), + self.test_data.get("device_family_fetched_successfully"), + self.test_data.get("task_info_response"), + self.test_data.get("untagging_image_successful_response"), + ] + elif "incorrect_site_untag_golden_image" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + self.test_data.get("image_id_fetched_successfully_response"), + Exception() + ] + elif "image_doesnot_exist_response" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + self.test_data.get("image_doesnot_exist_response"), + ] + elif "tag_golden_incorrect_family_name" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + self.test_data.get("image_id_fetched_successfully_response"), + self.test_data.get("fetch_site_id_response"), + self.test_data.get("device_family_fetched_successfully"), + ] + elif "only_image_distribution" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + self.test_data.get("image_id_fetched_successfully_response"), + self.test_data.get("device_id_fetched_successfully_response"), + self.test_data.get("task_info_response"), + self.test_data.get("image_distribution_successful_response"), + ] + elif "only_image_activation" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + self.test_data.get("image_id_fetched_successfully_response"), + self.test_data.get("device_id_fetched_successfully_response"), + self.test_data.get("task_info_response"), + self.test_data.get("image_activation_successful_response"), + ] + elif "device_doesnot_exist" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + self.test_data.get("image_id_fetched_successfully_response"), + self.test_data.get("device_doesnot_exist_response"), + ] + + def test_swim_full_flow(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.playbook_config + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertEqual( + result.get('msg'), + "Image activated successfully" + ) + + def test_swim_image_import(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.test_data.get("playbook_config_image_import") + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertEqual( + result.get('msg'), + "Image already exists." + ) + + def test_swim_untag_image(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.playbook_config_untag_image + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertEqual( + result.get('msg'), + "Un-tagging image as Golden." + ) + + def test_swim_missing_param_tag_golden_image(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.test_data.get("playbook_config_tag_golden_image_missing_param") + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get('msg'), + "Image details for tagging not provided" + ) + + def test_swim_incorrect_site_untag_golden_image(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.playbook_config_untag_image + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get('msg'), + "Site not found" + ) + + def test_swim_image_doesnot_exist_response(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.playbook_config_untag_image + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get('msg'), + "Image not found" + ) + + def test_swim_only_image_distribution(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.test_data.get("playbook_config_distribution") + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertEqual( + result.get('msg'), + "Image Distributed Successfully" + ) + + def test_swim_image_distribution_missing_param(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.test_data.get("playbook_config_distribution_missing_param") + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get('msg'), + "Image details for distribution not provided" + ) + + def test_swim_only_image_activation(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.test_data.get("playbook_config_activation") + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertEqual( + result.get('msg'), + "Image activated successfully" + ) + + def test_swim_image_activation_missing_param(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.test_data.get("playbook_config_activation_missing_param") + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get('msg'), + "Image details for activation not provided" + ) + + def test_swim_tag_golden_incorrect_family_name(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.test_data.get("playbook_config_tag_golden_image_incorrect_family_name") + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get('msg'), + "Family Device Name not found" + ) + + def test_swim_device_doesnot_exist(self): + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_log=True, + config=self.test_data.get("playbook_config_activation") + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get('msg'), + "Device not found" + )