Skip to content

Commit ef407a5

Browse files
authored
Merge pull request #16 from cisco-en-programmability/main
Changes from mainline
2 parents 588d4ff + 222ace9 commit ef407a5

2 files changed

Lines changed: 211 additions & 45 deletions

File tree

plugins/modules/provision_playbook_config_generator.py

Lines changed: 90 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,9 @@ def __init__(self, module):
296296
self.module_schema = self.provision_workflow_manager_mapping()
297297
self.log("Initialized ProvisionPlaybookGenerator class instance.", "DEBUG")
298298
self.site_id_name_dict = self.get_site_id_name_mapping()
299+
self._cached_provisioned_devices = None
300+
self._cached_inventory_devices = None
301+
self._cached_inventory_by_id = None
299302

300303
def get_site_id_name_mapping(self):
301304
"""
@@ -324,6 +327,30 @@ def get_site_id_name_mapping(self):
324327

325328
return site_id_name_mapping
326329

330+
def get_inventory_devices(self):
331+
"""
332+
Retrieve inventory devices once and reuse them throughout the current run.
333+
334+
Returns:
335+
list: Inventory device records from Catalyst Center.
336+
"""
337+
if self._cached_inventory_devices is not None:
338+
return self._cached_inventory_devices
339+
340+
all_devices_response = self.catalystcenter._exec(
341+
family="devices",
342+
function="get_device_list",
343+
op_modifies=False,
344+
)
345+
self.log("Received API response: {0}".format(all_devices_response), "DEBUG")
346+
self._cached_inventory_devices = all_devices_response.get("response", [])
347+
self._cached_inventory_by_id = {
348+
device.get("id"): device
349+
for device in self._cached_inventory_devices
350+
if device.get("id")
351+
}
352+
return self._cached_inventory_devices
353+
327354
def write_dict_to_yaml(self, data_dict, file_path, dumper=OrderedDumper, file_mode="overwrite"):
328355
"""
329356
Converts a dictionary to YAML format and writes it to a specified file path.
@@ -916,6 +943,10 @@ def get_all_provisioned_devices_internal(self):
916943
list: List of all provisioned devices
917944
"""
918945
self.log("Retrieving all provisioned devices from SDA API", "INFO")
946+
if self._cached_provisioned_devices is not None:
947+
self.log("Using cached provisioned devices list", "DEBUG")
948+
return self._cached_provisioned_devices
949+
919950
try:
920951
# Get all provisioned devices from SDA API
921952
response = self.catalystcenter._exec(
@@ -928,63 +959,68 @@ def get_all_provisioned_devices_internal(self):
928959
self.log("Retrieved {0} devices from SDA provisioned devices API".format(len(sda_devices)), "INFO")
929960

930961
# WORKAROUND: Check for missing wireless controllers
931-
all_devices_response = self.catalystcenter._exec(
932-
family="devices",
933-
function="get_device_list",
934-
op_modifies=False,
935-
)
936-
self.log("Received API response: {0}".format(all_devices_response), "DEBUG")
937-
all_devices = all_devices_response.get("response", [])
962+
all_devices = self.get_inventory_devices()
963+
inventory_by_id = self._cached_inventory_by_id or {}
938964

939965
wireless_controllers_found = []
940966
sda_device_ids = {device.get("networkDeviceId") for device in sda_devices}
941967

968+
for sda_device in sda_devices:
969+
inventory_device = inventory_by_id.get(sda_device.get("networkDeviceId"))
970+
if inventory_device:
971+
sda_device.update(
972+
{
973+
"id": inventory_device.get("id"),
974+
"managementIpAddress": inventory_device.get("managementIpAddress"),
975+
"family": inventory_device.get("family"),
976+
"type": inventory_device.get("type"),
977+
"hostname": inventory_device.get("hostname"),
978+
"location": inventory_device.get("location"),
979+
"siteId": sda_device.get("siteId") or inventory_device.get("siteId"),
980+
}
981+
)
982+
942983
for device in all_devices:
943984
device_id = device.get("id")
944985
management_ip = device.get("managementIpAddress")
945986

946987
if device_id in sda_device_ids:
947988
continue
948989

949-
try:
950-
device_detail_response = self.catalystcenter._exec(
951-
family="devices",
952-
function="get_device_detail",
953-
op_modifies=False,
954-
params={"search_by": device_id, "identifier": "uuid"},
955-
)
956-
self.log("Received API response: {0}".format(device_detail_response), "DEBUG")
957-
device_info = device_detail_response.get("response", {})
958-
device_family = device_info.get("nwDeviceFamily")
990+
device_family = device.get("family") or device.get("nwDeviceFamily")
959991

960-
if device_family == "Wireless Controller":
961-
try:
962-
provision_response = self.catalystcenter._exec(
963-
family="sda",
964-
function="get_provisioned_wired_device",
965-
op_modifies=False,
966-
params={"device_management_ip_address": management_ip}
967-
)
968-
self.log("Received API response: {0}".format(provision_response), "DEBUG")
969-
if provision_response.get("status") == "success":
970-
mock_device = {
971-
"networkDeviceId": device_id,
972-
"siteId": device.get("siteId"),
973-
"deviceType": "WirelessController"
974-
}
975-
sda_devices.append(mock_device)
976-
wireless_controllers_found.append(management_ip)
977-
except Exception:
978-
self.log("Wireless controller with IP {0} not provisioned in SDA".format(management_ip), "WARNING")
979-
pass
980-
except Exception:
981-
self.log("Could not retrieve details for device ID {0}".format(device_id), "WARNING")
982-
pass
992+
if device_family == "Wireless Controller":
993+
try:
994+
provision_response = self.catalystcenter._exec(
995+
family="sda",
996+
function="get_provisioned_wired_device",
997+
op_modifies=False,
998+
params={"device_management_ip_address": management_ip}
999+
)
1000+
self.log("Received API response: {0}".format(provision_response), "DEBUG")
1001+
if provision_response.get("status") == "success":
1002+
mock_device = {
1003+
"networkDeviceId": device_id,
1004+
"siteId": device.get("siteId"),
1005+
"deviceType": "WirelessController",
1006+
"id": device.get("id"),
1007+
"managementIpAddress": management_ip,
1008+
"family": device_family,
1009+
"type": device.get("type"),
1010+
"hostname": device.get("hostname"),
1011+
"location": device.get("location"),
1012+
}
1013+
sda_devices.append(mock_device)
1014+
wireless_controllers_found.append(management_ip)
1015+
except Exception:
1016+
self.log("Wireless controller with IP {0} not provisioned in SDA".format(management_ip), "WARNING")
1017+
pass
9831018

9841019
self.log("Found {0} additional provisioned wireless controllers".format(
9851020
len(wireless_controllers_found)), "INFO")
9861021

987-
return sda_devices
1022+
self._cached_provisioned_devices = sda_devices
1023+
return self._cached_provisioned_devices
9881024

9891025
except Exception as e:
9901026
self.log("Error retrieving provisioned devices: {0}".format(str(e)), "ERROR")
@@ -1209,6 +1245,11 @@ def transform_device_site_hierarchy(self, device_details):
12091245
else:
12101246
self.log("WARNING: Site ID {0} not found in site mapping".format(site_id), "WARNING")
12111247

1248+
cached_location = device_details.get("location")
1249+
if cached_location and str(cached_location).strip():
1250+
self.log("Using cached location field for site hierarchy: {0}".format(cached_location), "DEBUG")
1251+
return cached_location
1252+
12121253
# Fallback: If no siteId or mapping failed, get it from device detail API
12131254
if not site_id or not site_name_hierarchy:
12141255
self.log("Fallback: Getting site hierarchy from device detail API for device {0}".format(device_id), "DEBUG")
@@ -1285,6 +1326,10 @@ def transform_device_family_info(self, device_details):
12851326
Returns:
12861327
str: Device family type (e.g., 'Switches and Hubs', 'Wireless Controller').
12871328
"""
1329+
cached_family = device_details.get("nwDeviceFamily") or device_details.get("family")
1330+
if cached_family:
1331+
return cached_family
1332+
12881333
device_id = device_details.get("networkDeviceId")
12891334
self.log("Transforming device family info for device ID: {0}".format(device_id), "DEBUG")
12901335

@@ -1329,6 +1374,10 @@ def transform_device_management_ip(self, device_details):
13291374
Returns:
13301375
str: Device management IP address.
13311376
"""
1377+
cached_ip = device_details.get("managementIpAddress") or device_details.get("managementIpAddr")
1378+
if cached_ip:
1379+
return cached_ip
1380+
13321381
self.log("Transforming device management IP for device: {0}".format(device_details.get("networkDeviceId")), "DEBUG")
13331382

13341383
device_id = device_details.get("networkDeviceId")

tests/unit/modules/catalyst/test_provision_playbook_config_generator.py

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
__metaclass__ = type
2020

21-
from unittest.mock import patch
21+
from unittest.mock import Mock, patch
2222

2323
from ansible_collections.cisco.catalystcenter.plugins.modules import provision_playbook_config_generator
2424
from .catalystcenter_module import TestCatalystModule, set_module_args, loadPlaybookData
@@ -190,17 +190,22 @@ def test_provision_playbook_config_generator_playbook_global_filters(self):
190190
file_mode="overwrite",
191191
)
192192
)
193-
result = self.execute_module(changed=True, failed=False)
194193
expected_file_path = (
195194
"/Users/syedkahm/ansible/catalystcenter/work/collections/ansible_collections/cisco/catalystcenter/"
196195
"playbooks/brownfield_provision_workflow_playbook.yml"
197196
)
197+
with patch.object(
198+
provision_playbook_config_generator.ProvisionPlaybookGenerator,
199+
"write_dict_to_yaml",
200+
return_value=True,
201+
):
202+
result = self.execute_module(changed=True, failed=False)
198203
self.assertEqual(
199204
result.get("response"),
200205
{
201206
"YAML config generation Task succeeded for module 'provision_workflow_manager'.": {
202207
"file_path": expected_file_path,
203-
"devices_count": 6
208+
"devices_count": 7
204209
}
205210
}
206211
)
@@ -264,7 +269,7 @@ def test_provision_playbook_config_generator_playbook_global_filters_default_fil
264269
{
265270
"YAML config generation Task succeeded for module 'provision_workflow_manager'.": {
266271
"file_path": default_file_path,
267-
"devices_count": 6,
272+
"devices_count": 7,
268273
}
269274
}
270275
)
@@ -369,3 +374,115 @@ def test_apply_device_filters_matches_list_values(self):
369374
[device.get("management_ip_address") for device in filtered_devices],
370375
["204.1.2.5", "204.1.2.6"],
371376
)
377+
378+
def test_get_all_provisioned_devices_internal_uses_cached_inventory_details(self):
379+
generator = self.module.ProvisionPlaybookGenerator.__new__(
380+
self.module.ProvisionPlaybookGenerator
381+
)
382+
generator.log = lambda *args, **kwargs: None
383+
generator._cached_provisioned_devices = None
384+
generator._cached_inventory_devices = None
385+
generator._cached_inventory_by_id = None
386+
387+
exec_mock = Mock(side_effect=[
388+
{
389+
"response": [
390+
{
391+
"networkDeviceId": "dev-1",
392+
"siteId": "site-1",
393+
}
394+
]
395+
},
396+
{
397+
"response": [
398+
{
399+
"id": "dev-1",
400+
"managementIpAddress": "10.10.10.1",
401+
"family": "Switches and Hubs",
402+
"type": "Cisco Catalyst 9300 Switch",
403+
"hostname": "edge-1",
404+
"location": "Global/USA/SAN JOSE/BLD23",
405+
"siteId": "site-1",
406+
},
407+
{
408+
"id": "dev-2",
409+
"managementIpAddress": "10.10.10.2",
410+
"family": "Wireless Controller",
411+
"type": "Cisco Catalyst 9800",
412+
"hostname": "wlc-1",
413+
"location": "Global/USA/SAN JOSE/BLD23",
414+
"siteId": "site-2",
415+
},
416+
]
417+
},
418+
{"status": "success"},
419+
])
420+
generator.catalystcenter = Mock(_exec=exec_mock)
421+
422+
devices = generator.get_all_provisioned_devices_internal()
423+
cached_devices = generator.get_all_provisioned_devices_internal()
424+
425+
self.assertIs(devices, cached_devices)
426+
self.assertEqual(len(devices), 2)
427+
self.assertEqual(devices[0]["managementIpAddress"], "10.10.10.1")
428+
self.assertEqual(devices[0]["family"], "Switches and Hubs")
429+
self.assertEqual(devices[1]["deviceType"], "WirelessController")
430+
self.assertEqual(devices[1]["managementIpAddress"], "10.10.10.2")
431+
self.assertEqual(exec_mock.call_count, 3)
432+
called_functions = [call.kwargs["function"] for call in exec_mock.call_args_list]
433+
self.assertEqual(
434+
called_functions,
435+
["get_provisioned_devices", "get_device_list", "get_provisioned_wired_device"],
436+
)
437+
438+
def test_transform_device_site_hierarchy_uses_cached_location(self):
439+
generator = self.module.ProvisionPlaybookGenerator.__new__(
440+
self.module.ProvisionPlaybookGenerator
441+
)
442+
generator.log = lambda *args, **kwargs: None
443+
generator.site_id_name_dict = {}
444+
generator.catalystcenter = Mock()
445+
446+
site_hierarchy = generator.transform_device_site_hierarchy(
447+
{
448+
"networkDeviceId": "dev-1",
449+
"location": "Global/USA/SAN JOSE/BLD23",
450+
}
451+
)
452+
453+
self.assertEqual(site_hierarchy, "Global/USA/SAN JOSE/BLD23")
454+
generator.catalystcenter._exec.assert_not_called()
455+
456+
def test_transform_device_family_info_prefers_cached_family(self):
457+
generator = self.module.ProvisionPlaybookGenerator.__new__(
458+
self.module.ProvisionPlaybookGenerator
459+
)
460+
generator.log = lambda *args, **kwargs: None
461+
generator.catalystcenter = Mock()
462+
463+
device_family = generator.transform_device_family_info(
464+
{
465+
"networkDeviceId": "dev-1",
466+
"family": "Wireless Controller",
467+
}
468+
)
469+
470+
self.assertEqual(device_family, "Wireless Controller")
471+
generator.catalystcenter._exec.assert_not_called()
472+
473+
def test_transform_device_management_ip_prefers_cached_management_ip(self):
474+
generator = self.module.ProvisionPlaybookGenerator.__new__(
475+
self.module.ProvisionPlaybookGenerator
476+
)
477+
generator.log = lambda *args, **kwargs: None
478+
generator.catalystcenter = Mock()
479+
480+
management_ip = generator.transform_device_management_ip(
481+
{
482+
"networkDeviceId": "dev-1",
483+
"managementIpAddress": "10.10.10.1",
484+
}
485+
)
486+
487+
self.assertEqual(management_ip, "10.10.10.1")
488+
generator.catalystcenter._exec.assert_not_called()

0 commit comments

Comments
 (0)