From 5184212f0e49e18b09116a19a5697a2052b503e9 Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Tue, 1 Jul 2025 22:46:43 +0530 Subject: [PATCH 01/12] PNP Workflow - Set authorize flag enhancement --- playbooks/pnp_workflow_manager.yml | 2 + plugins/modules/pnp_workflow_manager.py | 151 +++++++++++++++++++++++- 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/playbooks/pnp_workflow_manager.yml b/playbooks/pnp_workflow_manager.yml index 4fd9f4e181..3ed1c51c10 100644 --- a/playbooks/pnp_workflow_manager.yml +++ b/playbooks/pnp_workflow_manager.yml @@ -32,6 +32,7 @@ state: Unclaimed pid: c9300-24P is_sudi_required: false + authorize: true - serial_number: QTC2320E0H9 state: Unclaimed @@ -55,6 +56,7 @@ state: Unclaimed pid: c9300-24P is_sudi_required: true + authorize: true - name: Claim a pre-added switch, apply a template, and perform an image upgrade for a specific site cisco.dnac.pnp_workflow_manager: diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index f2b80f57f2..98b270008c 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -82,6 +82,12 @@ description: Sudi Authentication requiremnet's flag. type: bool required: false + authorize: + description: + - Set the authorize flag for the device. + - Supported from Cisco Catalyst Center release version 2.3.7.6 onwards. + type: bool + required: false site_name: description: Name of the site for which the device will be claimed. type: str @@ -190,6 +196,7 @@ post /dna/intent/api/v1/onboarding/pnp-device/{id} get /dna/intent/api/v1/onboarding/pnp-device/count get /dna/intent/api/v1/onboarding/pnp-device put /onboarding/pnp-device/${id} get /dna/intent/api/v1/site get /dna/intent/api/v1/image/importation get /dna/intent/api/v1/template-programmer/template + post /api/v1/onboarding/pnp-device/authorize """ EXAMPLES = r""" - name: Import multiple switches in bulk only @@ -237,6 +244,7 @@ hostname: New_WLC state: Unclaimed pid: C9800-CL-K9 + authorize: true site_name: Global/USA/San Francisco/BGL_18 template_name: Ansible_PNP_WLC template_params: @@ -540,6 +548,10 @@ def get_pnp_params(self, params): param["serialNumber"] = param.pop("serial_number") if "is_sudi_required" in param: param["isSudiRequired"] = param.pop("is_sudi_required") + + if "authorize" in param: + param["authorize"] = param.pop("authorize") + device_dict["deviceInfo"] = param device_info_list.append(device_dict) @@ -705,6 +717,55 @@ def get_reset_params(self): self.pprint(reset_params)), "INFO") return reset_params + def authorize_device(self, device_id): + """ + Sets the authorization flag for a device on Cisco Catalyst Center. + + Parameters: + device_id (str): The ID of the device to authorize. + + Returns: + dict: The API response if the authorization is successful. + None: If the authorization fails or an unexpected response is received. + """ + self.log("Authorizing the device with device ID '{0}'.".format(device_id), "DEBUG") + + if not device_id: + self.msg = "No device ID provided for authorization." + self.log(self.msg, "ERROR") + return None + + authorize_payload = { + "deviceIdList": [device_id] + } + try: + authorize_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="authorize_device", + params=authorize_payload, + op_modifies=True + ) + self.log( + "Response from 'authorize_device' API for device authorization: {0}".format( + self.pprint(authorize_response) + ), + "DEBUG", + ) + + if authorize_response and isinstance(authorize_response, dict): + return authorize_response + + self.log("Received unexpected response from 'authorize_device' API for device ID {0}". + format(device_id), "ERROR") + + except Exception as e: + self.msg = "Unable to execute 'authorize_device' for device ID: '{0}'. ".format( + device_id) + self.log(self.msg + str(e), "ERROR") + return e + + return None + def bulk_devices_import(self, add_devices): """ Add Multiple devices to the Cisco Catalyst Center. @@ -763,12 +824,22 @@ def bulk_devices_import(self, add_devices): self.set_operation_result("failed", False, self.msg, "ERROR", bulk_params).check_return_status() - self.result['msg'] = "{0} device(s) imported successfully".format( + self.result['msg'] = "{0} device(s) imported successfully.".format( len(bulk_params.get("successList"))) self.log(self.result['msg'], "INFO") self.result['response'] = bulk_params self.result['diff'] = self.validated_config self.result['changed'] = True + + authorize_status, serial_number_list = self.bulk_authorize_devices(add_devices) + if authorize_status: + self.result['msg'] += " {0} device(s) authorized successfully".format( + len(serial_number_list)) + self.log(self.result['msg'], "INFO") + else: + self.result['msg'] += " Unable to authorize the device(s): {0}".format( + serial_number_list) + self.log(self.result['msg'], "INFO") return self except Exception as e: @@ -782,6 +853,65 @@ def bulk_devices_import(self, add_devices): self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status() return self + def bulk_authorize_devices(self, processed_devices): + """ + Authorizes multiple devices after bulk import is completed. + + Parameters: + processed_devices (list): A list of dictionaries containing bulk device information. + + Returns: + tuple: + bool: True if all devices are successfully authorized, False otherwise. + list: A list of serial numbers of the authorized or unauthorized devices. + """ + self.log("Processing bulk authorize devices started for: {0}".format( + self.pprint(processed_devices)), "INFO") + + authorized_devices = [] + unauthorized_devices = [] + + for device in processed_devices: + self.log("Checking device '{0}' for authorization flag.".format( + str(device)), "INFO") + device_info = device.get("deviceInfo", {}) + serial_number = device_info.get("serialNumber") + + for each_config in self.config: + input_device_info = each_config.get("device_info") + if not any(each_info.get("serialNumber") == serial_number + and each_info.get("authorize") is True + for each_info in input_device_info): + self.log("Config does not match with bulk processed serial number", "DEBUG") + continue + + device_response = self.get_device_list_pnp(serial_number) + if device_response and isinstance(device_response, dict): + authorize_response = self.authorize_device(device_response.get("id")) + self.log("Device authorization response: '{0}'".format( + authorize_response), "INFO") + + if isinstance(authorize_response, dict): + self.log("Device '{0}' authorized successfully.".format( + serial_number), "INFO") + authorized_devices.append(serial_number) + else: + self.log("Unable to authorized device: '{0}', error: {1}".format( + serial_number, authorize_response), "INFO") + unauthorized_devices.append(serial_number) + else: + self.log("No valid device response for serial number: '{0}'".format( + serial_number), "INFO" + ) + unauthorized_devices.append(serial_number) + + if authorized_devices and not unauthorized_devices: + self.log("Devices authorized successfully for '{0}'".format( + self.pprint(processed_devices)), "INFO") + return True, authorized_devices + + return False, unauthorized_devices + def compare_config_with_device_info(self, input_config, device_info): """ Compare the input config with the device info. @@ -799,6 +929,11 @@ def compare_config_with_device_info(self, input_config, device_info): unmatch_count = 0 for key, value in input_config.items(): device_value = device_info.get(key) + if key == "authorize" and value: + authorize_value = device_info.get("validActions", {}).get(key) + if authorize_value != value: + unmatch_count += 1 + if value != device_value: self.log("Mismatch found for key '{0}': expected '{1}', got '{2}'".format( key, value, device_value), "DEBUG") @@ -1131,6 +1266,8 @@ class instance for further use. for each_device in pnp_devices: serial_number = each_device.get("deviceInfo", {}).get("serialNumber") + authorize_flag = each_device.get("deviceInfo", {}).get("authorize") + if not serial_number: self.log("Skipping device entry due to missing serial number: {0}".format( self.pprint(each_device.get("deviceInfo"))), "WARNING") @@ -1153,6 +1290,18 @@ class instance for further use. self.log("Updating device info for serial: '{0}' as it's not provisioned or config doesn't match.".format( serial_number), "DEBUG") self.update_device_info(existing_device_info, device_info, device_response.get("id")) + + if authorize_flag: + authorize_response = self.authorize_device(device_response.get("id")) + self.log("Device authorize response: '{0}'".format( + authorize_response), "INFO") + + if isinstance(authorize_response, dict): + self.log("Device '{0}' authorized successfully.".format( + serial_number), "INFO") + else: + self.log("Unable to authorized device: '{0}', error: {1}".format( + serial_number, authorize_response), "INFO") else: self.log("Device '{0}' already provisioned with matching config. No update needed.".format( serial_number), "DEBUG") From 66df0665c73c871d2181ce31c35ef93679134d6e Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Tue, 1 Jul 2025 23:16:23 +0530 Subject: [PATCH 02/12] PNP Workflow - Set authorize flag enhancement --- plugins/modules/pnp_workflow_manager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index e71274f4b8..cb385f2b72 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -968,7 +968,7 @@ def bulk_authorize_devices(self, processed_devices): list: A list of serial numbers of the authorized or unauthorized devices. """ self.log("Processing bulk authorize devices started for: {0}".format( - self.pprint(processed_devices)), "INFO") + self.pprint(processed_devices)), "INFO") authorized_devices = [] unauthorized_devices = [] @@ -982,8 +982,8 @@ def bulk_authorize_devices(self, processed_devices): for each_config in self.config: input_device_info = each_config.get("device_info") if not any(each_info.get("serialNumber") == serial_number - and each_info.get("authorize") is True - for each_info in input_device_info): + and each_info.get("authorize") is True + for each_info in input_device_info): self.log("Config does not match with bulk processed serial number", "DEBUG") continue @@ -1510,12 +1510,12 @@ class instance for further use. self.log("Updating device info for serial: '{0}' as it's not provisioned or config doesn't match.".format( serial_number), "DEBUG") self.update_device_info( - existing_device_info, device_info, device_response.get("id")) + existing_device_info, device_info, device_response.get("id")) if authorize_flag: authorize_response = self.authorize_device(device_response.get("id")) self.log("Device authorize response: '{0}'".format( - authorize_response), "INFO") + authorize_response), "INFO") if isinstance(authorize_response, dict): self.log("Device '{0}' authorized successfully.".format( From cfea3f2e980ad36b2b47bfbcd3156ed28d2605c4 Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Tue, 1 Jul 2025 23:24:03 +0530 Subject: [PATCH 03/12] PNP Workflow - Set authorize flag enhancement --- tests/unit/modules/dnac/test_pnp_workflow_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/dnac/test_pnp_workflow_manager.py b/tests/unit/modules/dnac/test_pnp_workflow_manager.py index 37a13356b2..1fc1b20506 100644 --- a/tests/unit/modules/dnac/test_pnp_workflow_manager.py +++ b/tests/unit/modules/dnac/test_pnp_workflow_manager.py @@ -402,7 +402,7 @@ def test_pnp_workflow_manager_import_devices_in_bulk_new(self): self.maxDiff = None self.assertEqual( result.get('msg'), - "2 device(s) imported successfully" + "2 device(s) imported successfully. Unable to authorize the device(s): []" ) def test_pnp_workflow_manager_devices_idempotent(self): From 541db157283fa969fd38bc467dad2c27eee4239d Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Mon, 14 Jul 2025 01:23:34 +0530 Subject: [PATCH 04/12] PNP workflow - Authorization Set based on claimed device --- plugins/modules/pnp_workflow_manager.py | 132 +++++++----------- .../modules/dnac/test_pnp_workflow_manager.py | 2 +- 2 files changed, 48 insertions(+), 86 deletions(-) diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index cb385f2b72..7a8f041ea7 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -929,16 +929,6 @@ def bulk_devices_import(self, add_devices): self.result['response'] = bulk_params self.result['diff'] = self.validated_config self.result['changed'] = True - - authorize_status, serial_number_list = self.bulk_authorize_devices(add_devices) - if authorize_status: - self.result['msg'] += " {0} device(s) authorized successfully".format( - len(serial_number_list)) - self.log(self.result['msg'], "INFO") - else: - self.result['msg'] += " Unable to authorize the device(s): {0}".format( - serial_number_list) - self.log(self.result['msg'], "INFO") return self except Exception as e: @@ -955,65 +945,6 @@ def bulk_devices_import(self, add_devices): ).check_return_status() return self - def bulk_authorize_devices(self, processed_devices): - """ - Authorizes multiple devices after bulk import is completed. - - Parameters: - processed_devices (list): A list of dictionaries containing bulk device information. - - Returns: - tuple: - bool: True if all devices are successfully authorized, False otherwise. - list: A list of serial numbers of the authorized or unauthorized devices. - """ - self.log("Processing bulk authorize devices started for: {0}".format( - self.pprint(processed_devices)), "INFO") - - authorized_devices = [] - unauthorized_devices = [] - - for device in processed_devices: - self.log("Checking device '{0}' for authorization flag.".format( - str(device)), "INFO") - device_info = device.get("deviceInfo", {}) - serial_number = device_info.get("serialNumber") - - for each_config in self.config: - input_device_info = each_config.get("device_info") - if not any(each_info.get("serialNumber") == serial_number - and each_info.get("authorize") is True - for each_info in input_device_info): - self.log("Config does not match with bulk processed serial number", "DEBUG") - continue - - device_response = self.get_device_list_pnp(serial_number) - if device_response and isinstance(device_response, dict): - authorize_response = self.authorize_device(device_response.get("id")) - self.log("Device authorization response: '{0}'".format( - authorize_response), "INFO") - - if isinstance(authorize_response, dict): - self.log("Device '{0}' authorized successfully.".format( - serial_number), "INFO") - authorized_devices.append(serial_number) - else: - self.log("Unable to authorized device: '{0}', error: {1}".format( - serial_number, authorize_response), "INFO") - unauthorized_devices.append(serial_number) - else: - self.log("No valid device response for serial number: '{0}'".format( - serial_number), "INFO" - ) - unauthorized_devices.append(serial_number) - - if authorized_devices and not unauthorized_devices: - self.log("Devices authorized successfully for '{0}'".format( - self.pprint(processed_devices)), "INFO") - return True, authorized_devices - - return False, unauthorized_devices - def compare_config_with_device_info(self, input_config, device_info): """ Compare the input config with the device info. @@ -1507,23 +1438,15 @@ class instance for further use. ) if claim_stat != "Provisioned" and not match_stat: - self.log("Updating device info for serial: '{0}' as it's not provisioned or config doesn't match.".format( - serial_number), "DEBUG") + self.log( + "Updating device info for serial: '{0}' as it's not provisioned or config doesn't match.".format( + serial_number + ), + "DEBUG" + ) self.update_device_info( - existing_device_info, device_info, device_response.get("id")) - - if authorize_flag: - authorize_response = self.authorize_device(device_response.get("id")) - self.log("Device authorize response: '{0}'".format( - authorize_response), "INFO") - - if isinstance(authorize_response, dict): - self.log("Device '{0}' authorized successfully.".format( - serial_number), "INFO") - else: - self.log("Unable to authorized device: '{0}', error: {1}".format( - serial_number, authorize_response), "INFO") - + existing_device_info, device_info, device_response.get("id") + ) else: self.log( "Device '{0}' already provisioned with matching config. No update needed.".format( @@ -1566,6 +1489,17 @@ class instance for further use. "DEBUG", ) devices_exists.append(serial_number) + if authorize_flag and claim_stat == "Claimed": + authorize_response = self.authorize_device(device_response.get("id")) + self.log("Device authorize response: '{0}'".format( + authorize_response), "INFO") + + if isinstance(authorize_response, dict): + self.log("Device '{0}' authorized successfully.".format( + serial_number), "INFO") + else: + self.log("Unable to authorized device: '{0}', error: {1}".format( + serial_number, authorize_response), "INFO") else: self.log( "No valid device info returned for serial: '{0}'. Marking as not existing.".format( @@ -1679,6 +1613,20 @@ class instance for further use. and self.have["deviceInfo"] ): self.result["msg"] = "Device Added and Claimed Successfully" + authorize_flag = self.want.get("device_info").get("authorize", False) + if authorize_flag: + authorize_response = self.authorize_device(claim_params["deviceId"]) + self.log("Device authorize response: '{0}'".format( + authorize_response), "INFO") + + if isinstance(authorize_response, dict): + self.log("Device '{0}' authorized successfully.".format( + self.have["deviceInfo"].get("serialNumber")), "INFO") + self.result['msg'] += " device authorized successfully" + else: + self.log("Unable to authorized device: '{0}', error: {1}".format( + claim_params["deviceId"], authorize_response), "INFO") + self.log(self.result["msg"], "INFO") self.result["response"] = claim_response self.result["diff"] = self.validated_config @@ -1770,6 +1718,20 @@ class instance for further use. if claim_response.get("response") == "Device Claimed": self.result["msg"] = "Only Device Claimed Successfully" + authorize_flag = self.want.get("device_info").get("authorize", False) + if authorize_flag: + authorize_response = self.authorize_device(claim_params["deviceId"]) + self.log("Device authorize response: '{0}'".format( + authorize_response), "INFO") + + if isinstance(authorize_response, dict): + self.log("Device '{0}' authorized successfully.".format( + self.have["deviceInfo"].get("serialNumber")), "INFO") + self.result['msg'] += " device authorized successfully" + else: + self.log("Unable to authorized device: '{0}', error: {1}".format( + claim_params["deviceId"], authorize_response), "INFO") + self.log(self.result["msg"], "INFO") self.result["response"] = claim_response self.result["diff"] = self.validated_config diff --git a/tests/unit/modules/dnac/test_pnp_workflow_manager.py b/tests/unit/modules/dnac/test_pnp_workflow_manager.py index 1fc1b20506..37a13356b2 100644 --- a/tests/unit/modules/dnac/test_pnp_workflow_manager.py +++ b/tests/unit/modules/dnac/test_pnp_workflow_manager.py @@ -402,7 +402,7 @@ def test_pnp_workflow_manager_import_devices_in_bulk_new(self): self.maxDiff = None self.assertEqual( result.get('msg'), - "2 device(s) imported successfully. Unable to authorize the device(s): []" + "2 device(s) imported successfully" ) def test_pnp_workflow_manager_devices_idempotent(self): From 641acbcb01a2fbe41eb75e91139586dc0dd9eb46 Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Mon, 14 Jul 2025 01:34:22 +0530 Subject: [PATCH 05/12] PNP workflow - Authorization Set based on claimed device --- plugins/modules/pnp_workflow_manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 7a8f041ea7..7ec10c54fd 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -1489,7 +1489,8 @@ class instance for further use. "DEBUG", ) devices_exists.append(serial_number) - if authorize_flag and claim_stat == "Claimed": + if authorize_flag and claim_stat == "Claimed" and self.compare_dnac_versions( + self.get_ccc_version(), "2.3.7.6") > 0: authorize_response = self.authorize_device(device_response.get("id")) self.log("Device authorize response: '{0}'".format( authorize_response), "INFO") @@ -1614,7 +1615,7 @@ class instance for further use. ): self.result["msg"] = "Device Added and Claimed Successfully" authorize_flag = self.want.get("device_info").get("authorize", False) - if authorize_flag: + if authorize_flag and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: authorize_response = self.authorize_device(claim_params["deviceId"]) self.log("Device authorize response: '{0}'".format( authorize_response), "INFO") @@ -1719,7 +1720,7 @@ class instance for further use. if claim_response.get("response") == "Device Claimed": self.result["msg"] = "Only Device Claimed Successfully" authorize_flag = self.want.get("device_info").get("authorize", False) - if authorize_flag: + if authorize_flag and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: authorize_response = self.authorize_device(claim_params["deviceId"]) self.log("Device authorize response: '{0}'".format( authorize_response), "INFO") From edea3d402350b75915fcaf0b4d9da1526ed0e783 Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Mon, 14 Jul 2025 01:38:23 +0530 Subject: [PATCH 06/12] PNP workflow - Authorization Set based on claimed device --- plugins/modules/pnp_workflow_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 7ec10c54fd..0b3148929c 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -923,7 +923,7 @@ def bulk_devices_import(self, add_devices): "failed", False, self.msg, "ERROR", bulk_params ).check_return_status() - self.result['msg'] = "{0} device(s) imported successfully.".format( + self.result['msg'] = "{0} device(s) imported successfully".format( len(bulk_params.get("successList"))) self.log(self.result['msg'], "INFO") self.result['response'] = bulk_params From 1bcc0b844f5d4093b4c057082dd6d0b8036b7c1e Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Mon, 14 Jul 2025 01:41:39 +0530 Subject: [PATCH 07/12] PNP workflow - Authorization Set based on claimed device --- plugins/modules/pnp_workflow_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 0b3148929c..a5a9b410b4 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -1490,7 +1490,7 @@ class instance for further use. ) devices_exists.append(serial_number) if authorize_flag and claim_stat == "Claimed" and self.compare_dnac_versions( - self.get_ccc_version(), "2.3.7.6") > 0: + self.get_ccc_version(), "2.3.7.6") > 0: authorize_response = self.authorize_device(device_response.get("id")) self.log("Device authorize response: '{0}'".format( authorize_response), "INFO") From 57d97d88bee7e2dae352907869ef2c15e04788ad Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Mon, 28 Jul 2025 00:52:42 +0530 Subject: [PATCH 08/12] PNP Workflow - Set authorization code updated --- plugins/modules/pnp_workflow_manager.py | 158 ++++++++++++++++++------ 1 file changed, 117 insertions(+), 41 deletions(-) diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 8916f7fa00..42346f9b07 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -974,12 +974,22 @@ def bulk_devices_import(self, add_devices): "failed", False, self.msg, "ERROR", bulk_params ).check_return_status() - self.result['msg'] = "{0} device(s) imported successfully".format( + self.result['msg'] = "{0} device(s) imported successfully.".format( len(bulk_params.get("successList"))) self.log(self.result['msg'], "INFO") self.result['response'] = bulk_params self.result['diff'] = self.validated_config self.result['changed'] = True + + authorize_status, serial_number_list = self.bulk_authorize_devices(add_devices) + if authorize_status: + self.result['msg'] += " {0} device(s) authorized successfully".format( + len(serial_number_list)) + self.log(self.result['msg'], "INFO") + else: + self.result['msg'] += " Unable to authorize the device(s): {0}".format( + serial_number_list) + self.log(self.result['msg'], "INFO") return self except Exception as e: @@ -996,6 +1006,68 @@ def bulk_devices_import(self, add_devices): ).check_return_status() return self + def bulk_authorize_devices(self, processed_devices): + """ + Authorizes multiple devices after bulk import is completed. + + Parameters: + processed_devices (list): A list of dictionaries containing bulk device information. + + Returns: + tuple: + bool: True if all devices are successfully authorized, False otherwise. + list: A list of serial numbers of the authorized or unauthorized devices. + """ + self.log("Processing bulk authorize devices started for: {0}".format( + self.pprint(processed_devices)), "INFO") + + authorized_devices = [] + unauthorized_devices = [] + + for device in processed_devices: + self.log("Checking device '{0}' for authorization flag.".format( + str(device)), "INFO") + device_info = device.get("deviceInfo", {}) + serial_number = device_info.get("serialNumber") + + for each_config in self.config: + input_device_info = each_config.get("device_info") + if not any(each_info.get("serialNumber") == serial_number + and each_info.get("authorize") is True + for each_info in input_device_info): + self.log("Config does not match with bulk processed serial number", "DEBUG") + continue + + device_response = self.get_device_list_pnp(serial_number) + if device_response and isinstance(device_response, dict) \ + and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: + device_info = device_response.get("deviceInfo", {}) + if device_info.get("state") == "Pending Authorization": + authorize_response = self.authorize_device(device_response.get("id")) + self.log("Device authorization response: '{0}'".format( + authorize_response), "INFO") + + if isinstance(authorize_response, dict): + self.log("Device '{0}' authorized successfully.".format( + serial_number), "INFO") + authorized_devices.append(serial_number) + else: + self.log("Unable to authorized device: '{0}', error: {1}".format( + serial_number, authorize_response), "INFO") + unauthorized_devices.append(serial_number) + else: + self.log("No valid device response for serial number: '{0}'".format( + serial_number), "INFO" + ) + unauthorized_devices.append(serial_number) + + if authorized_devices and not unauthorized_devices: + self.log("Devices authorized successfully for '{0}'".format( + self.pprint(processed_devices)), "INFO") + return True, authorized_devices + + return False, unauthorized_devices + def compare_config_with_device_info(self, input_config, device_info): """ Compare the input config with the device info. @@ -1498,6 +1570,20 @@ class instance for further use. self.update_device_info( existing_device_info, device_info, device_response.get("id") ) + + if authorize_flag and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0 \ + and claim_stat == "Pending Authorization": + authorize_response = self.authorize_device(device_response.get("id")) + self.log("Device authorize response: '{0}'".format( + authorize_response), "INFO") + + if isinstance(authorize_response, dict): + self.log("Device '{0}' authorized successfully.".format( + serial_number), "INFO") + else: + self.log("Unable to authorized device: '{0}', error: {1}".format( + serial_number, authorize_response), "INFO") + else: self.log( "Device '{0}' already provisioned with matching config. No update needed.".format( @@ -1540,18 +1626,6 @@ class instance for further use. "DEBUG", ) devices_exists.append(serial_number) - if authorize_flag and claim_stat == "Claimed" and self.compare_dnac_versions( - self.get_ccc_version(), "2.3.7.6") > 0: - authorize_response = self.authorize_device(device_response.get("id")) - self.log("Device authorize response: '{0}'".format( - authorize_response), "INFO") - - if isinstance(authorize_response, dict): - self.log("Device '{0}' authorized successfully.".format( - serial_number), "INFO") - else: - self.log("Unable to authorized device: '{0}', error: {1}".format( - serial_number, authorize_response), "INFO") else: self.log( "No valid device info returned for serial: '{0}'. Marking as not existing.".format( @@ -1625,6 +1699,23 @@ class instance for further use. if self.have["deviceInfo"]: self.result["msg"] = "Only Device Added Successfully" + if self.have["deviceInfo"].get("state") == "Pending Authorization" \ + and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: + authorize_response = self.authorize_device(dev_add_response.get("id")) + self.log("Device authorize response: '{0}'".format( + authorize_response), "INFO") + + if isinstance(authorize_response, dict): + self.log("Device '{0}' authorized successfully.".format( + self.want.get("serial_number")), "INFO") + self.result["msg"] += ". Device '{0}' authorized successfully.".format( + self.want.get("serial_number")) + else: + self.log("Unable to authorized device: '{0}', error: {1}".format( + self.want.get("serial_number"), authorize_response), "INFO") + self.result["msg"] += ". Unable to authorize Device '{0}'.".format( + self.want.get("serial_number")) + self.log(self.result["msg"], "INFO") self.result["response"] = dev_add_response self.result["diff"] = self.validated_config @@ -1652,6 +1743,19 @@ class instance for further use. claim_params = self.get_claim_params() claim_params["deviceId"] = dev_add_response.get("id") + if self.have["deviceInfo"].get("state") == "Pending Authorization" \ + and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: + authorize_response = self.authorize_device(dev_add_response.get("id")) + self.log("Device authorize response: '{0}'".format( + authorize_response), "INFO") + + if isinstance(authorize_response, dict): + self.log("Device '{0}' authorized successfully.".format( + self.want.get("serial_number")), "INFO") + else: + self.log("Unable to authorized device: '{0}', error: {1}".format( + self.want.get("serial_number"), authorize_response), "INFO") + claim_response = self.claim_device_site(claim_params) self.log( "Response from API 'claim a device to a site' for a single claiming: {0}".format( @@ -1665,20 +1769,6 @@ class instance for further use. and self.have["deviceInfo"] ): self.result["msg"] = "Device Added and Claimed Successfully" - authorize_flag = self.want.get("device_info").get("authorize", False) - if authorize_flag and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: - authorize_response = self.authorize_device(claim_params["deviceId"]) - self.log("Device authorize response: '{0}'".format( - authorize_response), "INFO") - - if isinstance(authorize_response, dict): - self.log("Device '{0}' authorized successfully.".format( - self.have["deviceInfo"].get("serialNumber")), "INFO") - self.result['msg'] += " device authorized successfully" - else: - self.log("Unable to authorized device: '{0}', error: {1}".format( - claim_params["deviceId"], authorize_response), "INFO") - self.log(self.result["msg"], "INFO") self.result["response"] = claim_response self.result["diff"] = self.validated_config @@ -1770,20 +1860,6 @@ class instance for further use. if claim_response.get("response") == "Device Claimed": self.result["msg"] = "Only Device Claimed Successfully" - authorize_flag = self.want.get("device_info").get("authorize", False) - if authorize_flag and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: - authorize_response = self.authorize_device(claim_params["deviceId"]) - self.log("Device authorize response: '{0}'".format( - authorize_response), "INFO") - - if isinstance(authorize_response, dict): - self.log("Device '{0}' authorized successfully.".format( - self.have["deviceInfo"].get("serialNumber")), "INFO") - self.result['msg'] += " device authorized successfully" - else: - self.log("Unable to authorized device: '{0}', error: {1}".format( - claim_params["deviceId"], authorize_response), "INFO") - self.log(self.result["msg"], "INFO") self.result["response"] = claim_response self.result["diff"] = self.validated_config From c6268ba2d197d767445a1e2faf97b6986dc55943 Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Mon, 28 Jul 2025 00:59:46 +0530 Subject: [PATCH 09/12] PNP Workflow - Set authorization code updated --- plugins/modules/pnp_workflow_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 42346f9b07..64d90ee30b 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -974,7 +974,7 @@ def bulk_devices_import(self, add_devices): "failed", False, self.msg, "ERROR", bulk_params ).check_return_status() - self.result['msg'] = "{0} device(s) imported successfully.".format( + self.result['msg'] = "{0} device(s) imported successfully".format( len(bulk_params.get("successList"))) self.log(self.result['msg'], "INFO") self.result['response'] = bulk_params @@ -987,9 +987,9 @@ def bulk_devices_import(self, add_devices): len(serial_number_list)) self.log(self.result['msg'], "INFO") else: - self.result['msg'] += " Unable to authorize the device(s): {0}".format( + msg = " Unable to authorize the device(s): {0}".format( serial_number_list) - self.log(self.result['msg'], "INFO") + self.log(msg, "INFO") return self except Exception as e: From 38948dd503d789bba17a202960d115afada2ce81 Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Tue, 19 Aug 2025 20:05:45 +0530 Subject: [PATCH 10/12] PNP workflow - updated version control --- plugins/modules/pnp_workflow_manager.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 64d90ee30b..1b852bd3d0 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -981,16 +981,17 @@ def bulk_devices_import(self, add_devices): self.result['diff'] = self.validated_config self.result['changed'] = True - authorize_status, serial_number_list = self.bulk_authorize_devices(add_devices) - if authorize_status: - self.result['msg'] += " {0} device(s) authorized successfully".format( - len(serial_number_list)) - self.log(self.result['msg'], "INFO") - else: - msg = " Unable to authorize the device(s): {0}".format( - serial_number_list) - self.log(msg, "INFO") - return self + if self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: + authorize_status, serial_number_list = self.bulk_authorize_devices(add_devices) + if authorize_status: + self.result['msg'] += " {0} device(s) authorized successfully".format( + len(serial_number_list)) + self.log(self.result['msg'], "INFO") + else: + msg = " Unable to authorize the device(s): {0}".format( + serial_number_list) + self.log(msg, "INFO") + return self except Exception as e: msg = "Unable execute the function 'import_devices_in_bulk' for the payload: '{0}'. ".format( @@ -1039,8 +1040,7 @@ def bulk_authorize_devices(self, processed_devices): continue device_response = self.get_device_list_pnp(serial_number) - if device_response and isinstance(device_response, dict) \ - and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: + if device_response and isinstance(device_response, dict): device_info = device_response.get("deviceInfo", {}) if device_info.get("state") == "Pending Authorization": authorize_response = self.authorize_device(device_response.get("id")) From 2094d0b88e5a6e9616c9a705de95278c3fda1dc3 Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Thu, 28 Aug 2025 23:10:13 +0530 Subject: [PATCH 11/12] PNP Workflow - addressed code review comments --- plugins/modules/pnp_workflow_manager.py | 345 ++++++++++++++++++------ 1 file changed, 258 insertions(+), 87 deletions(-) diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 1b852bd3d0..60d2868e63 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -93,11 +93,18 @@ type: bool required: false authorize: - description: - - Set the authorize flag for the device. - - Supported from Cisco Catalyst Center release version 2.3.7.6 onwards. + description: | + - Set the authorization flag for PnP devices to enable provisioning after claiming. + - When set to true, devices in "Pending Authorization" state will be automatically authorized. + - This flag moves devices from "Pending Authorization" to "Authorized" state, allowing them to proceed with the provisioning workflow. + - Authorization is performed after successful device import (bulk operations) or device addition (single device operations). + - If not specified, devices will remain in their current authorization state and may require manual authorization. + - This parameter only applies to devices that support the authorization workflow in their PnP process. + - Authorization is skipped for devices that are not in "Pending Authorization" state. + - Supported from Cisco Catalyst Center release version 2.3.7.9 onwards. type: bool required: false + default: false site_name: description: Name of the site for which the device will be claimed. @@ -854,8 +861,15 @@ def authorize_device(self, device_id): Returns: dict: The API response if the authorization is successful. None: If the authorization fails or an unexpected response is received. + + Description: + This function authorizes a PnP device by setting the authorization flag, which moves the device + from "Pending Authorization" state to "Authorized" state. This is required for devices to be + provisioned after being claimed to a site. The function is supported from Cisco Catalyst Center + release version 2.3.7.9 onwards and handles both successful and failed authorization scenarios. """ - self.log("Authorizing the device with device ID '{0}'.".format(device_id), "DEBUG") + self.log("Initiating device authorization process for device ID: '{0}'".format( + device_id), "DEBUG") if not device_id: self.msg = "No device ID provided for authorization." @@ -873,23 +887,30 @@ def authorize_device(self, device_id): op_modifies=True ) self.log( - "Response from 'authorize_device' API for device authorization: {0}".format( + "Received API response from 'authorize_device' for device ID '{0}': {1}".format( + device_id, self.pprint(authorize_response) ), "DEBUG", ) if authorize_response and isinstance(authorize_response, dict): + self.log("Device authorization completed successfully for device ID: '{0}'".format( + device_id), "INFO") return authorize_response - self.log("Received unexpected response from 'authorize_device' API for device ID {0}". - format(device_id), "ERROR") + self.log( + "Received unexpected response format from 'authorize_device' API for device ID '{0}' - expected dict, got: {1}".format( + device_id, type(authorize_response).__name__ + ), + "ERROR" + ) except Exception as e: - self.msg = "Unable to execute 'authorize_device' for device ID: '{0}'. ".format( - device_id) - self.log(self.msg + str(e), "ERROR") - return e + self.msg = "Exception occurred while executing 'authorize_device' for device ID: '{0}' - {1}".format( + device_id, str(e) + ) + self.log(self.msg, "ERROR") return None @@ -981,17 +1002,32 @@ def bulk_devices_import(self, add_devices): self.result['diff'] = self.validated_config self.result['changed'] = True - if self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: + # Check for authorization support and process if applicable + current_version = self.get_ccc_version() + if self.compare_dnac_versions(current_version, "2.3.7.9") >= 0: + self.log("Cisco Catalyst Center version {0} supports device authorization. Checking for authorization requirements.".format( + current_version), "DEBUG") + authorize_status, serial_number_list = self.bulk_authorize_devices(add_devices) + if authorize_status: - self.result['msg'] += " {0} device(s) authorized successfully".format( - len(serial_number_list)) - self.log(self.result['msg'], "INFO") + auth_count = len(serial_number_list) + auth_msg = " {0} device(s) authorized successfully".format(auth_count) + self.result['msg'] += auth_msg + self.log("Device authorization completed successfully: {0} devices authorized".format( + auth_count), "INFO") else: - msg = " Unable to authorize the device(s): {0}".format( - serial_number_list) - self.log(msg, "INFO") - return self + if serial_number_list: + auth_msg = " Unable to authorize the device(s): {0}".format(serial_number_list) + self.log("Device authorization failed for devices: {0}".format( + serial_number_list), "WARNING") + else: + self.log("No devices required authorization or authorization was skipped", "INFO") + else: + self.log("Cisco Catalyst Center version {0} does not support device authorization feature (requires 2.3.7.9+)".format( + current_version), "INFO") + + return self except Exception as e: msg = "Unable execute the function 'import_devices_in_bulk' for the payload: '{0}'. ".format( @@ -1009,7 +1045,7 @@ def bulk_devices_import(self, add_devices): def bulk_authorize_devices(self, processed_devices): """ - Authorizes multiple devices after bulk import is completed. + Authorizes multiple devices after bulk import is completed based on authorization flag. Parameters: processed_devices (list): A list of dictionaries containing bulk device information. @@ -1018,55 +1054,129 @@ def bulk_authorize_devices(self, processed_devices): tuple: bool: True if all devices are successfully authorized, False otherwise. list: A list of serial numbers of the authorized or unauthorized devices. + + Description: + This function processes device authorization for devices that have the 'authorize' flag set to True + in the configuration. It checks each device's state and attempts authorization only for devices + in "Pending Authorization" state. The function is supported from Cisco Catalyst Center release + version 2.3.7.9 onwards and provides comprehensive status reporting for bulk authorization operations. """ - self.log("Processing bulk authorize devices started for: {0}".format( - self.pprint(processed_devices)), "INFO") + self.log("Initiating bulk device authorization process for {0} devices".format( + len(processed_devices)), "DEBUG") + + if not processed_devices: + self.log("No devices provided for bulk authorization - skipping process", "INFO") + return True, [] authorized_devices = [] unauthorized_devices = [] + devices_requiring_auth = [] + # First, identify devices that need authorization based on config for device in processed_devices: - self.log("Checking device '{0}' for authorization flag.".format( - str(device)), "INFO") device_info = device.get("deviceInfo", {}) serial_number = device_info.get("serialNumber") + if not serial_number: + self.log("Device missing serial number - skipping authorization check: {0}".format(device), "WARNING") + continue + + self.log("Checking authorization requirements for device: '{0}'".format(serial_number), "DEBUG") + + # Check if this device has authorize flag set in config + authorization_required = False for each_config in self.config: - input_device_info = each_config.get("device_info") - if not any(each_info.get("serialNumber") == serial_number - and each_info.get("authorize") is True - for each_info in input_device_info): - self.log("Config does not match with bulk processed serial number", "DEBUG") - continue + input_device_info = each_config.get("device_info", []) + for each_info in input_device_info: + if (each_info.get("serialNumber") == serial_number and + each_info.get("authorize") is True): + authorization_required = True + self.log("Device '{0}' requires authorization based on config".format(serial_number), "DEBUG") + break + if authorization_required: + break + + if authorization_required: + devices_requiring_auth.append(serial_number) + else: + self.log("Device '{0}' does not require authorization (authorize flag not set)".format(serial_number), "DEBUG") - device_response = self.get_device_list_pnp(serial_number) - if device_response and isinstance(device_response, dict): - device_info = device_response.get("deviceInfo", {}) - if device_info.get("state") == "Pending Authorization": - authorize_response = self.authorize_device(device_response.get("id")) - self.log("Device authorization response: '{0}'".format( - authorize_response), "INFO") + if not devices_requiring_auth: + self.log("No devices require authorization based on configuration", "INFO") + return True, [] - if isinstance(authorize_response, dict): - self.log("Device '{0}' authorized successfully.".format( - serial_number), "INFO") - authorized_devices.append(serial_number) - else: - self.log("Unable to authorized device: '{0}', error: {1}".format( - serial_number, authorize_response), "INFO") - unauthorized_devices.append(serial_number) - else: - self.log("No valid device response for serial number: '{0}'".format( - serial_number), "INFO" - ) - unauthorized_devices.append(serial_number) + self.log("Found {0} device(s) requiring authorization: {1}".format( + len(devices_requiring_auth), devices_requiring_auth), "INFO") + + # Process authorization for devices that require it + for serial_number in devices_requiring_auth: + self.log("Processing authorization for device: '{0}'".format(serial_number), "DEBUG") + device_response = self.get_device_list_pnp(serial_number) + if not device_response or not isinstance(device_response, dict): + self.log("Unable to retrieve device details for serial number: '{0}' - skipping authorization".format( + serial_number), "WARNING") + unauthorized_devices.append(serial_number) + continue + + device_info = device_response.get("deviceInfo", {}) + current_state = device_info.get("state") + device_id = device_response.get("id") + + self.log("Device '{0}' current state: '{1}'".format(serial_number, current_state), "DEBUG") + + if current_state != "Pending Authorization": + self.log("Device '{0}' is not in 'Pending Authorization' state (current: '{1}') - skipping authorization".format( + serial_number, current_state), "INFO") + unauthorized_devices.append(serial_number) + continue + + if not device_id: + self.log("Device '{0}' missing device ID - cannot authorize".format(serial_number), "ERROR") + unauthorized_devices.append(serial_number) + continue + + # Attempt device authorization + self.log("Attempting to authorize device '{0}' with ID '{1}'".format(serial_number, device_id), "INFO") + authorize_response = self.authorize_device(device_id) + + self.log("Authorization response for device '{0}': {1}".format( + serial_number, self.pprint(authorize_response)), "DEBUG") + + if authorize_response and isinstance(authorize_response, dict): + self.log("Device '{0}' authorized successfully".format(serial_number), "INFO") + authorized_devices.append(serial_number) + else: + error_msg = str(authorize_response) if authorize_response else "No response received" + self.log("Failed to authorize device '{0}': {1}".format(serial_number, error_msg), "ERROR") + unauthorized_devices.append(serial_number) + + # Generate final status summary + total_auth_required = len(devices_requiring_auth) + auth_success_count = len(authorized_devices) + auth_failed_count = len(unauthorized_devices) + + self.log("Bulk authorization completed - Required: {0}, Successful: {1}, Failed: {2}".format( + total_auth_required, auth_success_count, auth_failed_count), "INFO") + + if authorized_devices: + self.log("Successfully authorized devices: {0}".format(authorized_devices), "INFO") + + if unauthorized_devices: + self.log("Failed to authorize devices: {0}".format(unauthorized_devices), "WARNING") + + # Return success status and appropriate device list if authorized_devices and not unauthorized_devices: - self.log("Devices authorized successfully for '{0}'".format( - self.pprint(processed_devices)), "INFO") + self.log("All devices requiring authorization were successfully authorized", "INFO") return True, authorized_devices - return False, unauthorized_devices + if unauthorized_devices: + self.log("Some devices failed authorization or were not eligible", "WARNING") + return False, unauthorized_devices + + # This should not be reached, but included for completeness + self.log("No authorization operations were performed", "INFO") + return True, [] def compare_config_with_device_info(self, input_config, device_info): """ @@ -1571,18 +1681,28 @@ class instance for further use. existing_device_info, device_info, device_response.get("id") ) - if authorize_flag and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0 \ + current_version = self.get_ccc_version() + if authorize_flag and self.compare_dnac_versions(current_version, "2.3.7.9") >= 0 \ and claim_stat == "Pending Authorization": - authorize_response = self.authorize_device(device_response.get("id")) - self.log("Device authorize response: '{0}'".format( - authorize_response), "INFO") + self.log("Initiating device authorization process for device '{0}' - Version: {1}, State: {2}".format( + serial_number, current_version, claim_stat), "INFO") - if isinstance(authorize_response, dict): - self.log("Device '{0}' authorized successfully.".format( - serial_number), "INFO") + device_id = device_response.get("id") + if not device_id: + self.log("Device ID not found for device '{0}' - cannot proceed with authorization".format( + serial_number), "ERROR") else: - self.log("Unable to authorized device: '{0}', error: {1}".format( - serial_number, authorize_response), "INFO") + authorize_response = self.authorize_device(device_id) + self.log("Authorization API response for device '{0}': {1}".format( + serial_number, self.pprint(authorize_response)), "DEBUG") + + if authorize_response and isinstance(authorize_response, dict): + self.log("Device '{0}' authorized successfully and moved from 'Pending Authorization' state".format( + serial_number), "INFO") + else: + error_msg = str(authorize_response) if authorize_response else "No response received" + self.log("Failed to authorize device '{0}': {1}".format( + serial_number, error_msg), "ERROR") else: self.log( @@ -1699,22 +1819,47 @@ class instance for further use. if self.have["deviceInfo"]: self.result["msg"] = "Only Device Added Successfully" - if self.have["deviceInfo"].get("state") == "Pending Authorization" \ - and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: - authorize_response = self.authorize_device(dev_add_response.get("id")) - self.log("Device authorize response: '{0}'".format( - authorize_response), "INFO") - - if isinstance(authorize_response, dict): - self.log("Device '{0}' authorized successfully.".format( - self.want.get("serial_number")), "INFO") - self.result["msg"] += ". Device '{0}' authorized successfully.".format( - self.want.get("serial_number")) + self.log("Device successfully added to PnP database", "INFO") + + # Check if device requires authorization based on state and version compatibility + device_state = self.have["deviceInfo"].get("state") + current_version = self.get_ccc_version() + device_id = dev_add_response.get("id") + serial_number = self.want.get("serial_number") + + self.log("Device '{0}' current state: '{1}', Catalyst Center version: '{2}'".format( + serial_number, device_state, current_version), "DEBUG") + + # Check authorization requirements + if (device_state == "Pending Authorization" and + self.compare_dnac_versions(current_version, "2.3.7.9") >= 0): + + self.log("Device '{0}' is in 'Pending Authorization' state and version supports authorization - proceeding with authorization".format( + serial_number), "INFO") + + if not device_id: + self.log("Device ID not found for device '{0}' - cannot proceed with authorization".format( + serial_number), "ERROR") + self.result["msg"] += ". Unable to authorize Device '{0}' - missing device ID.".format( + serial_number) else: - self.log("Unable to authorized device: '{0}', error: {1}".format( - self.want.get("serial_number"), authorize_response), "INFO") - self.result["msg"] += ". Unable to authorize Device '{0}'.".format( - self.want.get("serial_number")) + self.log("Initiating authorization process for device '{0}' with ID '{1}'".format( + serial_number, device_id), "DEBUG") + + authorize_response = self.authorize_device(device_id) + self.log("Authorization API response for device '{0}': {1}".format( + serial_number, self.pprint(authorize_response)), "DEBUG") + + if authorize_response and isinstance(authorize_response, dict): + self.log("Device '{0}' authorization completed successfully".format( + serial_number), "INFO") + self.result["msg"] += ". Device '{0}' authorized successfully.".format( + serial_number) + else: + error_msg = str(authorize_response) if authorize_response else "No response received" + self.log("Failed to authorize device '{0}': {1}".format(serial_number, error_msg), "ERROR") + self.result["msg"] += ". Unable to authorize Device '{0}' - {1}.".format( + serial_number, error_msg) self.log(self.result["msg"], "INFO") self.result["response"] = dev_add_response @@ -1743,18 +1888,44 @@ class instance for further use. claim_params = self.get_claim_params() claim_params["deviceId"] = dev_add_response.get("id") - if self.have["deviceInfo"].get("state") == "Pending Authorization" \ - and self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") > 0: - authorize_response = self.authorize_device(dev_add_response.get("id")) - self.log("Device authorize response: '{0}'".format( - authorize_response), "INFO") + # Check if device requires authorization based on state and version compatibility + device_state = self.have["deviceInfo"].get("state") + current_version = self.get_ccc_version() + device_id = dev_add_response.get("id") + serial_number = self.want.get("serial_number") + + self.log("Device addition completed - checking authorization requirements for device '{0}'".format( + serial_number), "DEBUG") + self.log("Device '{0}' current state: '{1}', Catalyst Center version: '{2}'".format( + serial_number, device_state, current_version), "DEBUG") + + # Process device authorization if conditions are met + if (device_state == "Pending Authorization" and + self.compare_dnac_versions(current_version, "2.3.7.9") >= 0): + + self.log("Device '{0}' is in 'Pending Authorization' state and version supports authorization - initiating authorization process".format( + serial_number), "INFO") - if isinstance(authorize_response, dict): - self.log("Device '{0}' authorized successfully.".format( - self.want.get("serial_number")), "INFO") + if not device_id: + self.log("Device ID not found for device '{0}' - cannot proceed with authorization".format( + serial_number), "ERROR") + self.result["msg"] += ". Unable to authorize Device '{0}' - missing device ID.".format(serial_number) else: - self.log("Unable to authorized device: '{0}', error: {1}".format( - self.want.get("serial_number"), authorize_response), "INFO") + self.log("Attempting device authorization for device '{0}' with ID '{1}'".format( + serial_number, device_id), "DEBUG") + + authorize_response = self.authorize_device(device_id) + self.log("Authorization API response for device '{0}': {1}".format( + serial_number, self.pprint(authorize_response)), "DEBUG") + + if authorize_response and isinstance(authorize_response, dict): + self.log("Device '{0}' authorization completed successfully and moved from 'Pending Authorization' state".format( + serial_number), "INFO") + self.result["msg"] += ". Device '{0}' authorized successfully.".format(serial_number) + else: + error_msg = str(authorize_response) if authorize_response else "No response received" + self.log("Failed to authorize device '{0}': {1}".format(serial_number, error_msg), "ERROR") + self.result["msg"] += ". Unable to authorize Device '{0}' - {1}.".format(serial_number, error_msg) claim_response = self.claim_device_site(claim_params) self.log( From c57ed0b8cfb4e5f84fe36630735b1610ac56a149 Mon Sep 17 00:00:00 2001 From: md-rafeek Date: Thu, 28 Aug 2025 23:21:42 +0530 Subject: [PATCH 12/12] PNP Workflow - addressed code review comments --- plugins/modules/pnp_workflow_manager.py | 9 +++++---- tests/unit/modules/dnac/test_pnp_workflow_manager.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 60d2868e63..dde7059cb0 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -1089,9 +1089,10 @@ def bulk_authorize_devices(self, processed_devices): input_device_info = each_config.get("device_info", []) for each_info in input_device_info: if (each_info.get("serialNumber") == serial_number and - each_info.get("authorize") is True): + each_info.get("authorize") is True): authorization_required = True - self.log("Device '{0}' requires authorization based on config".format(serial_number), "DEBUG") + self.log("Device '{0}' requires authorization based on config".format( + serial_number), "DEBUG") break if authorization_required: break @@ -1832,7 +1833,7 @@ class instance for further use. # Check authorization requirements if (device_state == "Pending Authorization" and - self.compare_dnac_versions(current_version, "2.3.7.9") >= 0): + self.compare_dnac_versions(current_version, "2.3.7.9") >= 0): self.log("Device '{0}' is in 'Pending Authorization' state and version supports authorization - proceeding with authorization".format( serial_number), "INFO") @@ -1901,7 +1902,7 @@ class instance for further use. # Process device authorization if conditions are met if (device_state == "Pending Authorization" and - self.compare_dnac_versions(current_version, "2.3.7.9") >= 0): + self.compare_dnac_versions(current_version, "2.3.7.9") >= 0): self.log("Device '{0}' is in 'Pending Authorization' state and version supports authorization - initiating authorization process".format( serial_number), "INFO") diff --git a/tests/unit/modules/dnac/test_pnp_workflow_manager.py b/tests/unit/modules/dnac/test_pnp_workflow_manager.py index 37a13356b2..2f2a99c2c3 100644 --- a/tests/unit/modules/dnac/test_pnp_workflow_manager.py +++ b/tests/unit/modules/dnac/test_pnp_workflow_manager.py @@ -402,7 +402,7 @@ def test_pnp_workflow_manager_import_devices_in_bulk_new(self): self.maxDiff = None self.assertEqual( result.get('msg'), - "2 device(s) imported successfully" + "2 device(s) imported successfully 0 device(s) authorized successfully" ) def test_pnp_workflow_manager_devices_idempotent(self):