Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions playbooks/provision_workflow_manager.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,15 @@
dnac_task_poll_interval: 1
state: merged
config:
- site_name_hierarchy: Global/Chennai/LTTS/FLOOR1
management_ip_address: 1.1.1.1
- site_name_hierarchy: Global/USA/SAN JOSE/BLD23
management_ip_address: 204.192.4.2
primary_managed_ap_locations:
- Global/USA/SAN JOSE/BLD23/FLOOR1_LEVEL2
ap_authorization_list_name: "AP-Auth-List"
authorize_mesh_and_non_mesh_aps: false
feature_template:
- design_name: newtest
additional_identifiers:
wlan_profile_name: ARUBA_SSID_profile
site_name_hierarchy: Global/USA/SAN JOSE/BLD23
excluded_attributes: [example, test]

Check failure on line 36 in playbooks/provision_workflow_manager.yml

View workflow job for this annotation

GitHub Actions / Ansible Lint

yaml[new-line-at-end-of-file]

No new line character at the end of file

Check warning on line 36 in playbooks/provision_workflow_manager.yml

View workflow job for this annotation

GitHub Actions / Sanity (Ⓐdevel)

36:53 [new-line-at-end-of-file] no new line character at the end of file
255 changes: 249 additions & 6 deletions plugins/modules/provision_workflow_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
provisioning
- API to re-provision provisioned devices
- API to un-provision provisioned devices
- Un-provisioning refers to removing a device from the inventory list
version_added: '6.6.0'
extends_documentation_fragment:
- cisco.dnac.workflow_manager_params
Expand Down Expand Up @@ -56,6 +57,7 @@
only.
- Set to 'true' to proceed with provisioning
to a site.
- only applicable for wired devices.
type: bool
required: false
default: true
Expand Down Expand Up @@ -198,6 +200,44 @@
- Must be either 5, 15 or 25 representing
the proportion of APs to reboot at once.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing support for multiple wireless fields:

  1. {
    "type": "boolean",
    "required": false,
    "name": "skipApProvision",
    "displayText": "Skip AP Provision",
    "description": "True if Skip AP Provision is enabled, else False"
    },
    {
    "type": "map",
    "address": "uuidfa2e4f30",
    "required": false,
    "name": "rollingApUpgrade",
    "displayText": "Rolling AP Upgrade",
    "description": "Rolling AP Upgrade"
    },
    {
    "type": "string",
    "enum": [],
    "sensitive": false,
    "default": "",
    "constraints": [],
    "required": false,
    "name": "apAuthorizationListName",
    "displayText": "Ap Authorization List Name",
    "description": "AP Authorization List name. 'Obtain the AP Authorization List names by using the API call GET: /intent/api/v1/wirelessSettings/apAuthorizationLists. During re-provision, obtain the AP Authorization List configured for the given provisioned network device Id using the API call GET: /intent/api/v1/wireless/apAuthorizationLists/{networkDeviceId}'"
    },
    {
    "type": "boolean",
    "required": false,
    "name": "authorizeMeshAndNonMeshAccessPoints",
    "displayText": "Authorize Mesh And Non Mesh Access Points",
    "description": "True if AP Authorization List should authorize against All Mesh/Non-Mesh APs, else false if AP Authorization List should only authorize against Mesh APs (Applicable only when Mesh is enabled on sites)"
    },
    {
    "type": "map",
    "address": "uuid789146a0",
    "required": false,
    "name": "featureTemplatesOverridenAttributes",
    "displayText": "Feature Templates Overriden Attributes"
    }

I could not find all these supported on wireless controller provisioning.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More missing: {
"type": "boolean",
"required": false,
"name": "enableRollingApUpgrade",
"displayText": "Enable Rolling AP Upgrade",
"description": "True if Rolling AP Upgrade is enabled, else False"
},
{
"type": "integer",
"constraints": [],
"required": false,
"name": "apRebootPercentage",
"displayText": "AP Reboot Percentage",
"description": "AP Reboot Percentage. Permissible values - 5, 15, 25"
}
],
"uuid789146a0": [
{
"type": "array",
"arrayType": "map",
"constraints": [],
"address": "uuid7a58402c",
"required": true,
"name": "editFeatureTemplates",
"displayText": "Edit Feature Templates",
"description": "This array consists of Feature Templates that need to be overridden during the provisioning process for the current provision instance. These edits will not alter the original designs of the Feature Templates but will only apply to the values for the current provisioning instance. Note: Locked attributes cannot be edited in the Provision API. Additionally, default feature templates ('systemTemplate') cannot be included in the payload, as they are not editable."
}
],

type: int
ap_authorization_list_name:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    - The name of the Access Point (AP) authorization list to be used during WLC provisioning.
    - This authorization list defines the security policies and access control rules that govern which APs can join the wireless network.
    - The authorization list must exist in Cisco Catalyst Center before provisioning and should contain the MAC addresses or certificate-based authentication rules for APs.
    - Used in conjunction with 'authorize_mesh_and_non_mesh_aps' for comprehensive AP management during wireless controller provisioning.
    - If not specified, the default authorization behavior of the WLC will be applied.
  type: str
  required: false

description: |
- The name of the Access Point (AP) authorization
list to be used during WLC provisioning.
type: str
authorize_mesh_and_non_mesh_aps:
description: |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- A flag that indicates whether to authorize both mesh and non-mesh Access Points (APs) during the WLC provisioning process.
- When set to true, all AP types (mesh and non-mesh) will be automatically authorized to join the wireless network.
- When set to false, only specifically configured APs matching the authorization criteria will be authorized.
- Mesh APs create wireless backhaul connections to extend network coverage, while non-mesh APs connect directly to the wired infrastructure.
- This setting works in conjunction with 'ap_authorization_list_name' for complete AP authorization workflow.
- Supported from Cisco Catalyst Center release version 2.3.7.6 onwards.

- A flag that indicates whether to authorize
both mesh and non-mesh Access Points (APs)
during the WLC provisioning process.
type: bool
feature_template:
description: |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    - A dictionary containing feature template configuration for advanced wireless device provisioning.
    - Feature templates provide standardized, reusable configuration patterns that ensure consistent deployment across multiple wireless controllers.
    - Templates enable centralized configuration management, reduce manual errors, and enforce organizational policies.
    - The specified template must exist in Cisco Catalyst Center before it can be applied during provisioning.
    - Feature templates can include WLAN configurations, security policies, QoS settings, and other wireless controller parameters.
    - Supported from Cisco Catalyst Center release version 3.1.3.0 onwards for wireless controller provisioning.
  type: dict
  required: false

- A list of feature templates to be applied
- Each entry represents a feature template
with associated configuration details.
type: dict
suboptions:
design_name:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

design_name:
  description:
    - The name of the feature template design to be applied during wireless controller provisioning.
    - This template name must match exactly with the template name defined in Cisco Catalyst Center.
    - The template defines standardized configuration parameters, policies, and settings to be applied to the wireless controller.
    - Template names are case-sensitive and should follow organizational naming conventions.
  type: str
  required: true

description: The name of the feature template.
type: str
additional_identifiers:
description: A list of additional identifiers

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it more descriptive, give examples.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    additional_identifiers:
      description:
        - A list of additional context-specific identifiers that provide customization parameters for the feature template.
        - These identifiers enable site-specific and WLAN-specific customization of the template during deployment.
        - Each identifier contains key-value pairs that help adapt the template for specific deployment scenarios and locations.
        - Multiple identifiers can be specified to support complex deployment requirements with different WLAN profiles and site contexts.
      type: list
      elements: dict
      required: false

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# Campus deployment example
additional_identifiers:
  - wlan_profile_name: "Student_Network"
    site_name_hierarchy: "Global/Education/University/Campus/Library"
  - wlan_profile_name: "Faculty_Network"
    site_name_hierarchy: "Global/Education/University/Campus/Admin_Building"
  - wlan_profile_name: "Research_Network"
    site_name_hierarchy: "Global/Education/University/Campus/Research_Lab"

# Healthcare facility example
additional_identifiers:
  - wlan_profile_name: "Medical_Device_Profile"
    site_name_hierarchy: "Global/Healthcare/Hospital/ICU/Floor_3"
  - wlan_profile_name: "Staff_Network_Profile"
    site_name_hierarchy: "Global/Healthcare/Hospital/Admin/Floor_1"
  - wlan_profile_name: "Guest_WiFi_Profile"
    site_name_hierarchy: "Global/Healthcare/Hospital/Lobby/Floor_1"



for the feature template.
type: list
elements: dict
suboptions:
wlan_profile_name:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    wlan_profile_name:
      description:
        - The WLAN profile name to be associated with the feature template during wireless controller provisioning.
        - This profile defines wireless network parameters including SSID, security settings, VLAN assignments, and QoS policies.
        - The WLAN profile must exist in Cisco Catalyst Center and be properly configured before template application.
        - Multiple WLAN profiles can be referenced by specifying multiple additional identifier entries.
      type: str
      required: false

description: The WLAN profile name.
type: str
site_name_hierarchy:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    site_name_hierarchy:
      description:
        - The site name hierarchy where the feature template should be applied during wireless controller provisioning.
        - Defines the specific site context for template deployment within the organizational hierarchy.
        - Must follow the format 'Global/Area/Building/Floor' as configured in Cisco Catalyst Center site topology.
        - The site hierarchy must exist in Cisco Catalyst Center before template application.
        - Used to apply site-specific configurations and policies defined in the feature template.
      type: str
      required: false

description: The site name hierarchy.
type: str
excluded_attributes:
description: A list of attributes to be excluded

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it more descriptive, give examples.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    excluded_attributes:
      description:
        - A list of specific template attributes to be excluded from the feature template application during wireless controller provisioning.
        - Use this to selectively apply only certain parts of a template while excluding others that may not be applicable to the specific deployment.
        - Attribute names must match the exact attribute names defined in the feature template configuration.
        - This provides fine-grained control over which template configurations are applied, allowing for customized deployments.
        - Useful for scenarios where most of the template is applicable but specific settings need to be omitted or handled separately.
      type: list
      elements: str
      required: false
      examples:
        - ["guest_ssid_settings", "bandwidth_limits"]
        - ["dhcp_pool_configuration"]
        - ["radius_server_config", "certificate_settings"]
        - ["qos_policies", "traffic_shaping"]
        - ["mesh_configuration", "ap_group_settings"]

from the feature template.
type: list
elements: str
application_telemetry:
description: |
- A list of settings for enabling or disabling application telemetry on a group of network devices.
Expand Down Expand Up @@ -307,6 +347,9 @@
rolling_ap_upgrade:
enable_rolling_ap_upgrade: false
ap_reboot_percentage: 5
ap_authorization_list_name: "AP-Auth-List"
authorize_mesh_and_non_mesh_aps: true

- name: Provision a wired device to a site
cisco.dnac.provision_workflow_manager:
dnac_host: "{{dnac_host}}"
Expand Down Expand Up @@ -455,6 +498,33 @@
- application_telemetry:
- device_ips: ["204.1.1.2", "204.192.6.200"]
telemetry: disable

- name: Provision a wireless device to a site with feature template
cisco.dnac.provision_workflow_manager:
dnac_host: "{{ dnac_host }}"
dnac_username: "{{ dnac_username }}"
dnac_password: "{{ dnac_password }}"
dnac_verify: "{{ dnac_verify }}"
dnac_port: "{{ dnac_port }}"
dnac_version: "{{ dnac_version }}"
dnac_debug: "{{ dnac_debug }}"
dnac_log: true
dnac_log_level: DEBUG
config_verify: false
dnac_api_task_timeout: 1000
dnac_task_poll_interval: 1
state: merged
config:
- site_name_hierarchy: Global/USA/SAN JOSE/BLD23
management_ip_address: 204.192.4.2
primary_managed_ap_locations:
- Global/USA/SAN JOSE/BLD23/FLOOR1_LEVEL2
feature_template:
- design_name: newtest
additional_identifiers:
wlan_profile_name: ARUBA_SSID_profile
site_name_hierarchy: Global/USA/SAN JOSE/BLD23
excluded_attributes: [example, test]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give example from a real test.

"""
RETURN = r"""
# Case_1: Successful creation/updation/deletion of provision
Expand Down Expand Up @@ -561,6 +631,8 @@ def validate_input(self, state=None):
},
"skip_ap_provision": {"type": "bool", "required": False},
"rolling_ap_upgrade": {"type": "dict", "required": False},
"ap_authorization_list_name": {"type": "str", "required": False},
"authorize_mesh_and_non_mesh_aps": {"type": "bool", "required": False, "default": False},
"provisioning": {"type": "bool", "required": False, "default": True},
"force_provisioning": {"type": "bool", "required": False, "default": False},
"clean_config": {"type": "bool", "required": False, "default": False},
Expand All @@ -578,6 +650,20 @@ def validate_input(self, state=None):
},
},
},
"feature_template": {
"type": "list",
"elements": "dict",
"options": {
"design_name": {"type": "str", "required": True},
"attributes": {"type": "dict", "required": True},
"additional_identifiers": {"type": "dict", "required": False},
"excluded_attributes": {
"type": "list",
"elements": "str",
"required": False,
},
},
}
}

if state == "merged":
Expand Down Expand Up @@ -1392,6 +1478,10 @@ def get_wireless_params(self):
if self.validated_config.get("rolling_ap_upgrade"):
rolling_ap_upgrade = self.validated_config["rolling_ap_upgrade"]
wireless_params[0]["rolling_ap_upgrade"] = rolling_ap_upgrade
if self.validated_config.get("ap_authorization_list_name"):
wireless_params[0]["ap_authorization_list_name"] = self.validated_config.get("ap_authorization_list_name")
if self.validated_config.get("authorize_mesh_and_non_mesh_aps") is not None:
wireless_params[0]["authorize_mesh_and_non_mesh_aps"] = self.validated_config.get("authorize_mesh_and_non_mesh_aps")

response = self.dnac_apply["exec"](
family="devices",
Expand All @@ -1413,8 +1503,83 @@ def get_wireless_params(self):
),
"INFO",
)

if self.validated_config.get("feature_template"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        self.log("Processing feature template configuration for wireless device provisioning", "DEBUG")

feature_templates = self.validated_config.get("feature_template")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to check feature_templates?

            feature_templates = self.validated_config.get("feature_template")
            if not isinstance(feature_templates, list):
                self.msg = "Feature template configuration must be a list. Received: {0}".format(type(feature_templates).__name__)
                self.log(self.msg, "ERROR")
                self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
            
            if not feature_templates:
                self.log("Empty feature template list provided", "WARNING")
                return self
            
            wireless_params[0]["feature_template"] = []

wireless_params[0]["feature_template"] = []

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        self.log("Processing {0} feature template(s)".format(len(feature_templates)), "INFO")

for template in feature_templates:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User enumerate, so we can print the index..

        for template_index, template in enumerate(feature_templates):
            self.log("Processing feature template {0} of {1}".format(template_index + 1, len(feature_templates)), "DEBUG")

design_name = template.get("design_name")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

design_name is required field? If yes, can we check and udpate?

            if not design_name:
                self.msg = "Feature template 'design_name' is required but not provided for template at index {0}".format(template_index)
                self.log(self.msg, "ERROR")
                self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()

            self.log("Processing feature template with design name: '{0}' at index {1}".format(design_name, template_index), "DEBUG")

attributes = template.get("attributes", [])
cleaned_attributes = []

if isinstance(attributes, dict):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

            if attributes:
                self.log("Processing {0} template attributes for template '{1}'".format(len(attributes), design_name), "DEBUG")
                
                if isinstance(attributes, dict):
                    for key, value in attributes.items():
                        if value is not None:  # Skip None values
                            cleaned_attributes.append({
                                "name": key,
                                "value": value
                            })
                            self.log("Added template attribute for '{0}': '{1}' = '{2}'".format(design_name, key, value), "DEBUG")
                elif isinstance(attributes, list):
                    self.log("Attributes provided as list for template '{0}', using directly".format(design_name), "DEBUG")
                    cleaned_attributes = attributes
                else:
                    self.log("Invalid 'attributes' format for template '{0}'. Expected dict or list, got: {1}".format(
                        design_name, type(attributes).__name__), "WARNING")
            else:
                self.log("No attributes provided for feature template '{0}'".format(design_name), "DEBUG")

for key, value in attributes.items():
cleaned_attributes.append({
"name": key,
"value": value
})
else:
self.log(f"Expected 'attributes' to be a dict, got: {type(attributes)}", "WARNING")

additional_identifiers = template.get("additional_identifiers", {})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to process additonal_identifiers?

            additional_identifiers = template.get("additional_identifiers", [])
            if additional_identifiers:
                self.log("Processing {0} additional identifiers for template '{1}'".format(
                    len(additional_identifiers), design_name), "DEBUG")
                for idx, identifier in enumerate(additional_identifiers):
                    if isinstance(identifier, dict):
                        wlan_profile = identifier.get("wlan_profile_name")
                        site_hierarchy = identifier.get("site_name_hierarchy")
                        if wlan_profile:
                            self.log("Template '{0}' - Additional identifier {1}: WLAN profile = '{2}'".format(
                                design_name, idx + 1, wlan_profile), "DEBUG")
                        if site_hierarchy:
                            self.log("Template '{0}' - Additional identifier {1}: Site hierarchy = '{2}'".format(
                                design_name, idx + 1, site_hierarchy), "DEBUG")
                    else:
                        self.log("Invalid additional identifier format for template '{0}' at index {1}. Expected dict, got: {2}".format(
                            design_name, idx, type(identifier).__name__), "WARNING")
            else:
                self.log("No additional identifiers provided for feature template '{0}'".format(design_name), "DEBUG")

excluded_attributes = template.get("excluded_attributes", [])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

            excluded_attributes = template.get("excluded_attributes", [])
            if excluded_attributes:
                self.log("Processing {0} excluded attributes for template '{1}': {2}".format(
                    len(excluded_attributes), design_name, excluded_attributes), "DEBUG")
                if not isinstance(excluded_attributes, list):
                    self.log("Invalid 'excluded_attributes' format for template '{0}'. Expected list, got: {1}".format(
                        design_name, type(excluded_attributes).__name__), "WARNING")
                    excluded_attributes = []
            else:
                self.log("No excluded attributes specified for feature template '{0}'".format(design_name), "DEBUG")


ft_entry = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

                # Build feature template entry
                ft_entry = {
                    "design_name": design_name
                }
                
                if cleaned_attributes:
                    ft_entry["attributes"] = cleaned_attributes
                    self.log("Added {0} cleaned attributes to feature template '{1}' entry".format(
                        len(cleaned_attributes), design_name), "DEBUG")
                
                if additional_identifiers:
                    ft_entry["additional_identifiers"] = additional_identifiers
                    self.log("Added additional identifiers to feature template '{0}' entry".format(design_name), "DEBUG")
                
                if excluded_attributes:
                    ft_entry["excluded_attributes"] = excluded_attributes
                    self.log("Added {0} excluded attributes to feature template '{1}' entry".format(
                        len(excluded_attributes), design_name), "DEBUG")
                
                wireless_params[0]["feature_template"].append(ft_entry)
                self.log("Successfully configured feature template '{0}' for wireless device provisioning".format(design_name), "INFO")

"design_name": design_name,
"attributes": cleaned_attributes
}

if additional_identifiers:
ft_entry["additional_identifiers"] = additional_identifiers
if excluded_attributes:
ft_entry["excluded_attributes"] = excluded_attributes

wireless_params[0]["feature_template"].append(ft_entry)

self.log(
"Parameters collected for the provisioning of wireless device: {0}".format(wireless_params),
"INFO",
)
return wireless_params

def resolve_template_id(self, design_name):
"""
Retrieves the feature template ID for a given design name.

Args:
design_name (str): Name of the feature template design to match.

Returns:
str or None: The featureTemplateId if found, else None.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    Description:
        This function queries Cisco Catalyst Center to resolve a feature template design name
        to its corresponding template ID. It searches through template groups and instances,
        filtering out system templates to find user-defined templates.

"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    self.log("Initiating feature template ID resolution for design name: '{0}'".format(design_name), "DEBUG")
    
    if not design_name:
        self.log("Design name is empty or None - cannot resolve template ID", "ERROR")
        return None
    
    if not isinstance(design_name, str):
        self.log("Design name must be a string, received: {0}".format(type(design_name).__name__), "ERROR")
        return None
    
    self.log("Querying Cisco Catalyst Center for feature template with design name: '{0}'".format(design_name), "INFO")

try:
ft_response = self.dnac_apply["exec"](
family="wireless",
function="get_feature_template_summary",
params={'designName': design_name}
)

self.log("Feature template response: {0}".format(str(ft_response)), "DEBUG")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        self.log("Received feature template API response from 'get_feature_template_summary': {0}".format(str(ft_response)), "DEBUG")


for template_group in ft_response.get("response", []):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        template_groups = ft_response.get("response", [])
        if not template_groups:
            self.log("No template groups found in API response", "WARNING")
            return None
        
        self.log("Processing {0} template group(s) for design name: '{1}'".format(len(template_groups), design_name), "DEBUG")
        
        for group_index, template_group in enumerate(template_groups):
            self.log("Processing template group {0} of {1}".format(group_index + 1, len(template_groups)), "DEBUG")
            
            instances = template_group.get("instances", [])
            if not instances:
                self.log("No instances found in template group {0}".format(group_index + 1), "DEBUG")
                continue
            
            self.log("Found {0} template instance(s) in group {1}".format(len(instances), group_index + 1), "DEBUG")
            
            for instance_index, instance in enumerate(instances):
                instance_design_name = instance.get("designName")
                instance_id = instance.get("id")
                is_system_template = instance.get("systemTemplate", False)
                
                self.log("Evaluating template instance {0}: design_name='{1}', id='{2}', system_template={3}".format(
                    instance_index + 1, instance_design_name, instance_id, is_system_template), "DEBUG")
                
                if instance_design_name == design_name and not is_system_template:
                    self.log("Successfully resolved feature template ID: '{0}' for design name: '{1}'".format(instance_id, design_name), "INFO")
                    return instance_id
                
                if instance_design_name == design_name and is_system_template:
                    self.log("Found matching design name '{0}' but it's a system template - skipping".format(design_name), "DEBUG")
                
                if instance_design_name != design_name:
                    self.log("Design name mismatch: expected '{0}', found '{1}' - skipping".format(design_name, instance_design_name), "DEBUG")
        
        self.log("Feature template with design name '{0}' not found after searching all template groups and instances".format(design_name), "WARNING")
        return None

for instance in template_group.get("instances", []):
if (
instance.get("designName") == design_name
and not instance.get("systemTemplate", False)
):
template_id = instance.get("id")
self.log("Resolved featureTemplateId: {0} for designName: '{1}'".format(template_id, design_name), "INFO")
return template_id

self.log("Feature template with designName '{0}' not found.".format(design_name), "WARNING")
return None

except Exception as e:
msg = "Failed to resolve featureTemplateId for designName '{0}': {1}".format(design_name, str(e))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        msg = "Exception occurred while resolving feature template ID for design name '{0}': {1}".format(design_name, str(e))

self.log(msg, "ERROR")
return None

def get_want(self, config):
"""
Get all provision related informantion from the playbook
Expand Down Expand Up @@ -1778,18 +1943,47 @@ def application_telemetry(self, telemetry_config):
"disable": "disable_application_telemetry_feature_on_multiple_network_devices"
}

self.log("Starting application telemetry configuration process", "DEBUG")
self.log("Received telemetry configuration: {0}".format(telemetry_config), "DEBUG")

application_telemetry_details = telemetry_config.get("application_telemetry", [])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check the value of application_telemetry_details? If not required, ignore this comment

    if not application_telemetry_details:
        self.msg = "No application telemetry configuration entries found in telemetry config."
        self.log(self.msg, "WARNING")
        self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
        return self

self.log("Processing {0} telemetry configuration entries".format(len(application_telemetry_details)), "INFO")

for detail in application_telemetry_details:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    for detail_index, detail in enumerate(application_telemetry_details):
        self.log("Processing telemetry configuration entry {0} of {1}".format(detail_index + 1, len(application_telemetry_details)), "DEBUG")
        
        device_ips = detail.get("device_ips", [])
        self.log("Retrieved device IPs from configuration entry {0}: {1}".format(detail_index + 1, device_ips), "DEBUG")
                    # Validate device_ips structure and content
        if device_ips is None:
            self.msg = "Device IPs field is None in telemetry configuration entry {0}.".format(detail_index + 1)
            self.log(self.msg, "ERROR")
            self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
            return self
        
        if not isinstance(device_ips, list):
            self.msg = "Device IPs must be provided as a list in telemetry configuration entry {0}. Received: {1}".format(
                detail_index + 1, type(device_ips).__name__)
            self.log(self.msg, "ERROR")
            self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
            return self
        
        if len(device_ips) == 0:
            self.msg = "Empty device IPs list provided in telemetry configuration entry {0}.".format(detail_index + 1)
            self.log(self.msg, "ERROR")
            self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
            return s

device_ips = detail.get("device_ips", [])
self.log("Processing device IPs: {0}".format(device_ips), "DEBUG")
if device_ips is None or len(device_ips) == 0:
self.msg = "No valid device IPs provided for application telemetry."
self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
return self

all_empty = True
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        # Validate individual IP addresses
        valid_ips = []
        invalid_ips = []
        
        self.log("Validating {0} device IP address(es) in entry {1}".format(len(device_ips), detail_index + 1), "DEBUG")
        
        for ip_index, ip in enumerate(device_ips):
            if not isinstance(ip, str):
                self.log("Device IP at index {0} in entry {1} is not a string: {2} (type: {3})".format(
                    ip_index, detail_index + 1, ip, type(ip).__name__), "WARNING")
                invalid_ips.append(str(ip))
                continue
            
            # Check for empty or whitespace-only IPs
            if not ip or not ip.strip():
                self.log("Empty or whitespace-only device IP found at index {0} in entry {1}".format(
                    ip_index, detail_index + 1), "WARNING")
                invalid_ips.append(ip)
                continue
            
            # Valid IP found
            valid_ip = ip.strip()
            valid_ips.append(valid_ip)
            self.log("Valid device IP validated at index {0} in entry {1}: '{2}'".format(
                ip_index, detail_index + 1, valid_ip), "DEBUG")
        
        # Check if any valid IPs were found
        if not valid_ips:
            if invalid_ips:
                self.msg = "No valid device IPs found in telemetry configuration entry {0}. Invalid IPs: {1}".format(
                    detail_index + 1, invalid_ips)
            else:
                self.msg = "No valid device IPs provided in telemetry configuration entry {0}.".format(detail_index + 1)
                                
            self.log(self.msg, "ERROR")
            self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
            return self
        
        if invalid_ips:
            self.log("Found {0} invalid device IP(s) in entry {1}: {2}. Proceeding with {3} valid IP(s): {4}".format(
                len(invalid_ips), detail_index + 1, invalid_ips, len(valid_ips), valid_ips), "WARNING")
        else:
            self.log("All {0} device IP(s) validated successfully in entry {1}".format(
                len(valid_ips), detail_index + 1), "INFO")
        
        # Update the detail with cleaned valid IPs for further processing
        detail["device_ips"] = valid_ips
        self.log("Updated telemetry configuration entry {0} with {1} validated device IP(s)".format(
            detail_index + 1, len(valid_ips)), "DEBUG")

for ip in device_ips:
if ip.strip() != "":
all_empty = False
self.log("Valid device IP found: {0}".format(ip), "DEBUG")
break

if all_empty:
self.msg = "No valid device IPs provided for application telemetry."
self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
return self

telemetry = detail.get("telemetry") # "enable" or "disable"
if telemetry not in ["enable", "disable"]:
self.msg = "Invalid telemetry action '{0}'. Expected 'enable' or 'disable'.".format(telemetry)
self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
wlan_mode = detail.get("wlan_mode")
include_guest_ssid = detail.get("include_guest_ssid", False)

self.log("Telemetry action: {0}, WLAN mode: {1}, Include guest SSID: {2}".format(
telemetry, wlan_mode, include_guest_ssid
), "DEBUG")
for ip in device_ips:
self.validated_config["management_ip_address"] = ip
device_type, device_family = self.get_device_type_and_family(ip)
self.log("Device type: {0}, Device family: {1} for IP: {2}".format(
device_type, device_family, ip
), "DEBUG")

unsupported_devices = [
"Cisco Catalyst 9500 Switch",
Expand Down Expand Up @@ -3052,6 +3246,54 @@ def provision_wireless_device(self):
)
payload["rollingApUpgrade"] = rolling_ap_upgrade

if "ap_authorization_list_name" in prov_params:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        # Process AP authorization list configuration if provided
        if "ap_authorization_list_name" in prov_params:
            ap_auth_list = prov_params.get("ap_authorization_list_name")
            self.log("Adding AP authorization list name to payload: '{0}'".format(ap_auth_list), "DEBUG")
            payload["apAuthorizationListName"] = ap_auth_list
        else:
            self.log("No AP authorization list name provided in provisioning parameters", "DEBUG")

payload["apAuthorizationListName"] = prov_params.get("ap_authorization_list_name")

if "authorize_mesh_and_non_mesh_aps" in prov_params:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        # Process mesh and non-mesh AP authorization configuration if provided  
        if "authorize_mesh_and_non_mesh_aps" in prov_params:
            authorize_aps = prov_params.get("authorize_mesh_and_non_mesh_aps")
            self.log("Adding mesh and non-mesh AP authorization flag to payload: '{0}'".format(authorize_aps), "DEBUG")
            payload["authorizeMeshAndNonMeshAPs"] = authorize_aps
        else:
            self.log("No mesh and non-mesh AP authorization flag provided in provisioning parameters", "DEBUG")

payload["authorizeMeshAndNonMeshAPs"] = prov_params.get("authorize_mesh_and_non_mesh_aps")

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        # Process feature template configuration for supported versions
        current_version = self.get_ccc_version()
        if self.compare_dnac_versions(current_version, "3.1.3.0") >= 0:
            self.log("Cisco Catalyst Center version '{0}' supports feature template functionality (>= 3.1.3.0)".format(current_version), "INFO")

if self.compare_dnac_versions(self.get_ccc_version(), "3.1.3.0") >= 0:
self.log("Catalyst Center version >= 3.1.3.0 — processing 'feature_template'", "INFO")
self.log(prov_params, "DEBUG")
if "feature_template" in prov_params:
ft = prov_params.get("feature_template", [])[0]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to check ft before it is being used?

            if "feature_template" in prov_params:
                self.log("Processing feature template configuration from provisioning parameters", "INFO")
                
                feature_templates = prov_params.get("feature_template", [])
                if not feature_templates:
                    self.log("Empty feature template list found in provisioning parameters", "WARNING")
                else:
                    self.log("Found {0} feature template(s) to process".format(len(feature_templates)), "DEBUG")
                    
                    # Process the first feature template (assuming single template for now)
                    ft = feature_templates[0]
                    self.log("Processing feature template data: {0}".format(self.pprint(ft)), "DEBUG")
                    
                    # Validate required fields
                    design_name = ft.get("design_name")
                    if not design_name:
                        self.msg = "Feature template 'design_name' is required but not provided"
                        self.log(self.msg, "ERROR")
                        self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
                    
                    additional_identifiers = ft.get("additional_identifiers", {})
                    if not additional_identifiers:
                        self.msg = "Feature template 'additional_identifiers' is required but not provided"
                        self.log(self.msg, "ERROR")
                        self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
                    
                    wlan_profile = additional_identifiers.get("wlan_profile_name")
                    site_hierarchy = additional_identifiers.get("site_name_hierarchy")
                    
                    if not wlan_profile:
                        self.msg = "Feature template 'wlan_profile_name' is required in additional_identifiers but not provided"
                        self.log(self.msg, "ERROR")
                        self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
                    
                    if not site_hierarchy:
                        self.msg = "Feature template 'site_name_hierarchy' is required in additional_identifiers but not provided"
                        self.log(self.msg, "ERROR")
                        self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
                    
                    excluded_attributes = ft.get("excluded_attributes", [])
                    
                    self.log("Feature template validation successful - design_name: '{0}', wlan_profile: '{1}', site_hierarchy: '{2}'".format(
                        design_name, wlan_profile, site_hierarchy), "DEBUG")
                    
                    # Resolve feature template ID
                    self.log("Resolving feature template ID for design name: '{0}'".format(design_name), "DEBUG")
                    feature_template_id = self.resolve_template_id(design_name)
                    if not feature_template_id:
                        self.msg = "Failed to resolve feature template ID for design name: '{0}'".format(design_name)
                        self.log(self.msg, "ERROR")
                        self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
                    
                    self.log("Successfully resolved feature template ID: '{0}' for design name: '{1}'".format(
                        feature_template_id, design_name), "INFO")
                    
                    # Resolve site UUID
                    self.log("Resolving site UUID for site hierarchy: '{0}'".format(site_hierarchy), "DEBUG")
                    site_exists, site_id = self.get_site_id(site_hierarchy)
                    if not site_exists or not site_id:
                        self.msg = "Failed to resolve site UUID for site hierarchy: '{0}'".format(site_hierarchy)
                        self.log(self.msg, "ERROR")
                        self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
                    
                    site_uuid = site_id
                    self.log("Successfully resolved site UUID: '{0}' for site hierarchy: '{1}'".format(
                        site_uuid, site_hierarchy), "DEBUG")
                    
                    # Build feature template entry
                    new_entry = {
                        "featureTemplateId": feature_template_id,
                        "attributes": {},
                        "additionalIdentifiers": {
                            "wlanProfileName": wlan_profile,
                            "siteUuid": site_uuid
                        }
                    }
                    
                    # Add excluded attributes if provided
                    if excluded_attributes:
                        new_entry["excludedAttributes"] = excluded_attributes
                        self.log("Added {0} excluded attributes to feature template entry: {1}".format(
                            len(excluded_attributes), excluded_attributes), "DEBUG")
                    else:
                        self.log("No excluded attributes specified for feature template", "DEBUG")
                    
                    # Initialize feature templates structure in payload if not exists
                    if "featureTemplatesOverridenAttributes" not in payload:
                        payload["featureTemplatesOverridenAttributes"] = {
                            "editFeatureTemplates": []
                        }
                        self.log("Initialized featureTemplatesOverridenAttributes structure in payload", "DEBUG")
                    
                    # Add the feature template entry to payload
                    payload["featureTemplatesOverridenAttributes"]["editFeatureTemplates"].append(new_entry)
                    self.log("Successfully added feature template entry to payload for design: '{0}'".format(design_name), "INFO")
                    
            else:
                self.log("No feature template configuration found in provisioning parameters", "DEBUG")

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we have many statements in else part, we can have a separate API to do it..

                feature_templates = prov_params.get("feature_template", [])
                if not feature_templates:
                    self.log("Empty feature template list found in provisioning parameters", "WARNING")
                else:
                    self.log("Found {0} feature template(s) to process".format(len(feature_templates)), "DEBUG")
                    payload = self.process_feature_template_configuration(feature_templates, payload)

  def process_feature_template_configuration(self, feature_templates, payload):
       """
       Processes feature template configuration for wireless device provisioning payload construction.
       Args:
           self (object): An instance of a class used for interacting with Cisco Catalyst Center.
           feature_templates (list): List of feature template configurations to process.
           payload (dict): The wireless provisioning payload to be updated with feature template data.
       Returns:
           dict: Updated payload containing feature template configuration.
       Description:
           This function validates and processes feature template configurations for wireless device
           provisioning. It performs comprehensive validation of required fields including design name,
           additional identifiers (WLAN profile and site hierarchy), and excluded attributes. The function
           resolves template and site identifiers using Cisco Catalyst Center APIs, constructs the
           appropriate payload structure for the provisioning API, and ensures all mandatory fields
           are present and properly formatted before adding the template configuration to the payload.
       """
       self.log("Initiating feature template configuration processing for wireless provisioning", "DEBUG")
       
       if not feature_templates:
           self.log("Empty feature template list provided for processing", "WARNING")
           return payload
       
       self.log("Processing {0} feature template(s) for wireless provisioning".format(len(feature_templates)), "DEBUG")
       
       # Process the first feature template (assuming single template for current implementation)
       ft = feature_templates[0]
       self.log("Processing feature template configuration data: {0}".format(self.pprint(ft)), "DEBUG")
       
       # Validate design name
       design_name = ft.get("design_name")
       if not design_name:
           self.msg = "Feature template 'design_name' is required but not provided"
           self.log(self.msg, "ERROR")
           self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
       
       self.log("Validated feature template design name: '{0}'".format(design_name), "DEBUG")
       
       # Validate additional identifiers
       additional_identifiers = ft.get("additional_identifiers", {})
       if not additional_identifiers:
           self.msg = "Feature template 'additional_identifiers' is required but not provided"
           self.log(self.msg, "ERROR")
           self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
       
       # Extract and validate WLAN profile and site hierarchy
       wlan_profile = additional_identifiers.get("wlan_profile_name")
       site_hierarchy = additional_identifiers.get("site_name_hierarchy")
       
       if not wlan_profile:
           self.msg = "Feature template 'wlan_profile_name' is required in additional_identifiers but not provided"
           self.log(self.msg, "ERROR")
           self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
       
       if not site_hierarchy:
           self.msg = "Feature template 'site_name_hierarchy' is required in additional_identifiers but not provided"
           self.log(self.msg, "ERROR")
           self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
       
       excluded_attributes = ft.get("excluded_attributes", [])
       
       self.log("Feature template validation completed successfully - design_name: '{0}', wlan_profile: '{1}', site_hierarchy: '{2}'".format(
           design_name, wlan_profile, site_hierarchy), "DEBUG")
       
       # Resolve feature template ID
       self.log("Resolving feature template ID for design name: '{0}'".format(design_name), "DEBUG")
       feature_template_id = self.resolve_template_id(design_name)
       if not feature_template_id:
           self.msg = "Failed to resolve feature template ID for design name: '{0}'".format(design_name)
           self.log(self.msg, "ERROR")
           self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
       
       self.log("Successfully resolved feature template ID: '{0}' for design name: '{1}'".format(
           feature_template_id, design_name), "INFO")
       
       # Resolve site UUID
       self.log("Resolving site UUID for site hierarchy: '{0}'".format(site_hierarchy), "DEBUG")
       site_exists, site_id = self.get_site_id(site_hierarchy)
       if not site_exists or not site_id:
           self.msg = "Failed to resolve site UUID for site hierarchy: '{0}'".format(site_hierarchy)
           self.log(self.msg, "ERROR")
           self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status()
       
       site_uuid = site_id
       self.log("Successfully resolved site UUID: '{0}' for site hierarchy: '{1}'".format(
           site_uuid, site_hierarchy), "DEBUG")
       
       # Build feature template entry
       template_entry = {
           "featureTemplateId": feature_template_id,
           "attributes": {},
           "additionalIdentifiers": {
               "wlanProfileName": wlan_profile,
               "siteUuid": site_uuid
           }
       }
       
       if excluded_attributes:
           template_entry["excludedAttributes"] = excluded_attributes
           self.log("Added {0} excluded attributes to feature template entry: {1}".format(
               len(excluded_attributes), excluded_attributes), "DEBUG")
       else:
           self.log("No excluded attributes specified for feature template", "DEBUG")
       
       # Initialize feature templates structure in payload if not exists
       if "featureTemplatesOverridenAttributes" not in payload:
           payload["featureTemplatesOverridenAttributes"] = {
               "editFeatureTemplates": []
           }
           self.log("Initialized featureTemplatesOverridenAttributes structure in payload", "DEBUG")
       
       # Add the feature template entry to payload
       payload["featureTemplatesOverridenAttributes"]["editFeatureTemplates"].append(template_entry)
       self.log("Successfully added feature template entry to payload for design: '{0}'".format(design_name), "INFO")
       
       self.log("Feature template configuration processing completed successfully", "INFO")
       return payload

self.log("Feature template data: {0}".format(ft), "DEBUG")
wlan_profile = ft["additional_identifiers"]["wlan_profile_name"]
site_hierarchy = ft["additional_identifiers"]["site_name_hierarchy"]
excluded = ft.get("excluded_attributes", [])

feature_template_id = self.resolve_template_id(ft["design_name"])
site_exists, site_id = self.get_site_id(site_hierarchy)
self.log(site_id, "DEBUG")
site_uuid = site_id

new_entry = {
"featureTemplateId": feature_template_id,
"attributes": {
},
"additionalIdentifiers": {
"wlanProfileName": wlan_profile,
"siteUuid": site_uuid
},
"excludedAttributes": excluded
}

if "featureTemplatesOverridenAttributes" not in payload:
payload["featureTemplatesOverridenAttributes"] = {
"editFeatureTemplates": []
}

payload["featureTemplatesOverridenAttributes"]["editFeatureTemplates"].append(new_entry)
else:
self.log("Catalyst Center version < 3.1.3.0 — skipping 'feature_template'", "INFO")

import json

self.log(
"Final constructed payload:\n{0}".format(json.dumps(payload, indent=2)),
"INFO",
)

try:
response = self.dnac_apply["exec"](
family="wireless",
Expand Down Expand Up @@ -3126,10 +3368,11 @@ def get_diff_deleted(self):
self.set_operation_result("success", False, self.msg, "INFO")
return self

if device_type != "wired":
self.result["msg"] = "APIs are not supported for the device"
self.log(self.result["msg"], "CRITICAL")
return self
if self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") <= 0:
if device_type != "wired":
self.result["msg"] = "APIs are not supported for the device"
self.log(self.result["msg"], "CRITICAL")
return self

device_id = self.get_device_id()
provision_id, status = self.get_device_provision_status(device_id)
Expand All @@ -3139,7 +3382,7 @@ def get_diff_deleted(self):
"Device associated with the passed IP address is not provisioned"
)
self.log(self.result["msg"], "CRITICAL")
self.result["response"] = self.want["prov_params"]
self.result["response"] = self.result["msg"]
return self

if self.compare_dnac_versions(self.get_ccc_version(), "2.3.5.3") <= 0:
Expand Down
Loading