Skip to content

Commit ca5437d

Browse files
authored
Merge pull request #38 from cisco-en-programmability/template_cg_bug_fixes
Temlate CG Bug Fixes
2 parents f02bed3 + 21735d3 commit ca5437d

3 files changed

Lines changed: 145 additions & 4 deletions

File tree

plugins/module_utils/catalystcenter.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,10 +1433,12 @@ def decrypt_password(self, encrypted_password, key):
14331433
def camel_to_snake_case(self, config):
14341434
"""
14351435
Convert camel case keys to snake case keys in the config.
1436+
Entries with None values are excluded from the returned config.
14361437
Args:
1437-
config (list) - Playbook details provided by the user.
1438+
config (list or dict) - Playbook details provided by the user.
14381439
Returns:
1439-
new_config (list) - Updated config after eliminating the camel cases.
1440+
new_config (list or dict) - Updated config after converting camel case
1441+
keys to snake case and removing entries with None values.
14401442
"""
14411443

14421444
if isinstance(config, dict):
@@ -1451,7 +1453,8 @@ def camel_to_snake_case(self, config):
14511453
"DEBUG",
14521454
)
14531455
new_value = self.camel_to_snake_case(value)
1454-
new_config[new_key] = new_value
1456+
if new_value is not None:
1457+
new_config[new_key] = new_value
14551458
elif isinstance(config, list):
14561459
return [self.camel_to_snake_case(item) for item in config]
14571460
else:

plugins/modules/template_playbook_config_generator.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ def __init__(self, module):
456456
super().__init__(module)
457457
self.module_schema = self.get_workflow_elements_schema()
458458
self.module_name = "template_workflow_manager"
459+
self.project_description_map = None
459460

460461
def validate_input(self):
461462
"""
@@ -760,6 +761,48 @@ def transform_containing_templates(self, template_details):
760761

761762
return final_containing_templates
762763

764+
def transform_template_params(self, template_details):
765+
"""
766+
Transforms template parameters from camelCase API response format to snake_case
767+
format compatible with the template_workflow_manager module.
768+
769+
Uses the base class camel_to_snake_case() for recursive conversion of all
770+
fields including nested range and selection objects.
771+
772+
Args:
773+
template_details (dict): A dictionary containing template-specific information.
774+
775+
Returns:
776+
list: A list of dictionaries containing transformed template parameter information.
777+
Each dictionary may contain keys such as parameter_name, data_type,
778+
default_value, description, range, selection, etc.
779+
"""
780+
781+
self.log(
782+
"Starting template params transformation for template: {0}".format(
783+
template_details.get("name", "Unknown")
784+
),
785+
"DEBUG"
786+
)
787+
788+
template_params = template_details.get("templateParams", [])
789+
if not template_params:
790+
self.log("No template params found in template details", "DEBUG")
791+
return template_params
792+
793+
self.log("Processing {0} template param(s)".format(len(template_params)), "DEBUG")
794+
795+
final_template_params = self.camel_to_snake_case(template_params)
796+
797+
self.log(
798+
"Completed template params transformation. Transformed {0} param(s)".format(
799+
len(final_template_params)
800+
),
801+
"DEBUG"
802+
)
803+
804+
return final_template_params
805+
763806
def containing_templates_temp_spec(self):
764807
"""
765808
Constructs a temporary specification for containing templates, defining the structure and types of attributes
@@ -817,6 +860,11 @@ def templates_temp_spec(self):
817860
"template_name": {"type": "str", "source_key": "name"},
818861
"template_description": {"type": "str", "source_key": "description"},
819862
"project_name": {"type": "str", "source_key": "projectName"},
863+
"project_description": {
864+
"type": "str",
865+
"special_handling": True,
866+
"transform": self.transform_project_description,
867+
},
820868
"author": {"type": "str"},
821869
"language": {"type": "str"},
822870
"composite": {"type": "bool"},
@@ -841,6 +889,12 @@ def templates_temp_spec(self):
841889
"special_handling": True,
842890
"transform": self.transform_template_content,
843891
},
892+
"template_params": {
893+
"type": "list",
894+
"element": "dict",
895+
"special_handling": True,
896+
"transform": self.transform_template_params,
897+
},
844898
"template_tag": {
845899
"type": "list",
846900
"element": "dict",
@@ -851,6 +905,80 @@ def templates_temp_spec(self):
851905
)
852906
return template_details
853907

908+
def initialize_project_description_map(self, project_details=None):
909+
"""
910+
Initializes the project_description_map by building a mapping of project names
911+
to their descriptions. If project_details is not provided, fetches project
912+
details from Catalyst Center using get_template_projects_details.
913+
914+
Args:
915+
project_details (list, optional): A list of project dictionaries from the API.
916+
If None, project details will be fetched from Catalyst Center.
917+
"""
918+
919+
if project_details is None:
920+
self.log(
921+
"Fetching project details from Catalyst Center for project_description_map",
922+
"DEBUG"
923+
)
924+
network_element = self.module_schema["network_elements"]["projects"]
925+
self.get_template_projects_details(
926+
network_element, {"component_specific_filters": None}
927+
)
928+
return
929+
930+
self.project_description_map = {
931+
p.get("name"): p.get("description")
932+
for p in (project_details or [])
933+
if p.get("description")
934+
}
935+
self.log(
936+
"project_description_map initialized with {0} entries".format(
937+
len(self.project_description_map)
938+
),
939+
"DEBUG"
940+
)
941+
942+
def transform_project_description(self, template_details):
943+
"""
944+
Retrieves the project description for a template by looking up the project name
945+
in a cached project description map. Lazily initializes the map by fetching
946+
project details from Catalyst Center if not already populated.
947+
948+
Args:
949+
template_details (dict): A dictionary containing template-specific information
950+
including 'projectName'.
951+
952+
Returns:
953+
str or None: The project description if found, otherwise None.
954+
"""
955+
956+
self.log(
957+
"Starting project description lookup for template: {0}".format(
958+
template_details.get("name", "Unknown")
959+
),
960+
"DEBUG"
961+
)
962+
963+
if self.project_description_map is None:
964+
self.log(
965+
"project_description_map not initialized, triggering lazy initialization",
966+
"DEBUG"
967+
)
968+
self.initialize_project_description_map()
969+
970+
project_name = template_details.get("projectName")
971+
project_description = self.project_description_map.get(project_name)
972+
973+
self.log(
974+
"Completed project description lookup for project '{0}': {1}".format(
975+
project_name, project_description if project_description else "No description found"
976+
),
977+
"DEBUG"
978+
)
979+
980+
return project_description
981+
854982
def get_template_projects_details(self, network_element, filters):
855983
"""
856984
Retrieves template project details from Catalyst Center with pagination support.
@@ -959,6 +1087,9 @@ def get_template_projects_details(self, network_element, filters):
9591087
else:
9601088
self.log("No projects found in Catalyst Center", "DEBUG")
9611089

1090+
# Cache project descriptions for use by transform_project_description
1091+
self.initialize_project_description_map(final_template_projects)
1092+
9621093
# Transform using temp spec
9631094
self.log(
9641095
"Transforming {0} project(s) using projects temp spec".format(

tests/unit/modules/catalyst/test_template_playbook_config_generator.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,13 @@ def load_fixtures(self, response=None, device=""):
9090
elif "template_by_name_single" in self._testMethodName:
9191
self.run_catalystcenter_exec.side_effect = [
9292
self.test_data.get("get_all_templates"),
93+
self.test_data.get("get_all_projects"),
9394
]
9495
elif "template_by_name_multiple" in self._testMethodName:
9596
self.run_catalystcenter_exec.side_effect = [
9697
self.test_data.get("get_all_templates"),
97-
self.test_data.get("get_all_templates")
98+
self.test_data.get("get_all_templates"),
99+
self.test_data.get("get_all_projects"),
98100
]
99101
elif "template_by_name_and_id" in self._testMethodName:
100102
self.run_catalystcenter_exec.side_effect = [
@@ -107,24 +109,29 @@ def load_fixtures(self, response=None, device=""):
107109
elif "templates_empty_filter" in self._testMethodName:
108110
self.run_catalystcenter_exec.side_effect = [
109111
self.test_data.get("get_all_templates"),
112+
self.test_data.get("get_all_projects"),
110113
]
111114
elif "templates_includes_uncommitted_filter" in self._testMethodName:
112115
self.run_catalystcenter_exec.side_effect = [
113116
self.test_data.get("get_all_templates"),
117+
self.test_data.get("get_all_projects"),
114118
]
115119
elif "template_by_project_name_multiple" in self._testMethodName:
116120
self.run_catalystcenter_exec.side_effect = [
117121
self.test_data.get("get_all_templates"),
118122
self.test_data.get("get_all_templates"),
123+
self.test_data.get("get_all_projects"),
119124
]
120125
elif "template_by_template_name_and_project_name" in self._testMethodName:
121126
self.run_catalystcenter_exec.side_effect = [
122127
self.test_data.get("get_all_templates"),
123128
self.test_data.get("get_all_templates"),
129+
self.test_data.get("get_all_projects"),
124130
]
125131
elif "template_all_filters" in self._testMethodName:
126132
self.run_catalystcenter_exec.side_effect = [
127133
self.test_data.get("get_all_templates"),
134+
self.test_data.get("get_all_projects"),
128135
]
129136
elif "invalid_project_details" in self._testMethodName:
130137
self.run_catalystcenter_exec.side_effect = [

0 commit comments

Comments
 (0)