From f2494e08ba2879e45bde0fd0c939f33184d25523 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Wed, 24 Jan 2024 16:20:18 -0500 Subject: [PATCH 01/76] Added dnac_log_file_path and dnac_logs_append paramter to dnac.py --- plugins/module_utils/dnac.py | 69 ++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index e6ada258f0..ad3aa1ba51 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -28,6 +28,9 @@ import inspect import re +# Global log config variables +__first_log_written = False + class DnacBase(): @@ -65,7 +68,9 @@ def __init__(self, module): } self.dnac_log = dnac_params.get("dnac_log") self.dnac_log_level = dnac_params.get("dnac_log_level").upper() - log(str(dnac_params)) + self.dnac_log_file_path = dnac_params.get("dnac_log_file_path") + self.dnac_logs_append = dnac_params.get("dnac_logs_append") + self.log(str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -157,13 +162,18 @@ def log(self, message, level="info", frameIncrement=0): """ level = level.upper() + + # Validate dnac_log_level and level + valid_log_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') + if level and self.dnac_log_level not in valid_log_levels: + raise ValueError("Invalid log level: 'dnac_log_level: {}' and 'level: {}'. Expected one of {}.".format(self.dnac_log_level, level, valid_log_levels)) + if ( self.dnac_log - and self.dnac_log_level in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level) ): message = "Module: " + self.__class__.__name__ + ", " + message - log(message, level, (1 + frameIncrement)) + log(message, level, self.dnac_log_file_path, self.dnac_logs_append, (1 + frameIncrement)) def check_return_status(self): """API to check the return status value and exit/fail the module""" @@ -207,7 +217,9 @@ def get_dnac_params(self, params): "dnac_verify": params.get("dnac_verify"), "dnac_debug": params.get("dnac_debug"), "dnac_log": params.get("dnac_log"), - "dnac_log_level": params.get("dnac_log_level") + "dnac_log_level": params.get("dnac_log_level"), + "dnac_log_file_path": params.get("dnac_log_file_path"), + "dnac_logs_append": params.get("dnac_logs_append"), } return dnac_params @@ -231,7 +243,7 @@ def get_task_details(self, task_id): params={"task_id": task_id} ) - log(str(response)) + self.log(str(response)) if response and isinstance(response, dict): result = response.get('response') @@ -401,13 +413,50 @@ def camel_to_snake_case(self, config): return new_config -def log(msg, level='info', frameIncrement=0): - with open('dnac.log', 'a') as of: +def log(msg, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, frameIncrement=0): + """Logs/Appends messages into the specified log file or to dnac.log file by default + + Args: + msg (str, required): The log message to be recorded. + level (str, optional): The log level, default is "info". + The log level can be one of 'DEBUG', 'INFO', 'WARNING', 'ERROR', or 'CRITICAL'. + dnac_log_file_path (str, optional): The path to the log file, default is 'dnac.log'. + dnac_logs_append (bool, optional): A boolean indicating whether to append to an existing log file or create a new one, default is True. + frameIncrement (int, optional): The number of frames to increment in the call stack, default is 0. + + Note: + Validates if the directory exists and raises an exception if it doesn't. + File gets created if it doesn't exist + """ + + global __first_log_written + + # Validate dnac_log_file_path is absolute or relative + if not os.path.isabs(dnac_log_file_path): + dnac_log_file_path = os.path.join(os.getcwd(), dnac_log_file_path) + + # Validate if the directory exists + log_directory = os.path.dirname(dnac_log_file_path) + if not os.path.exists(log_directory): + raise FileNotFoundError("The directory for log file '{}' does not exist.".format(dnac_log_file_path)) + + # Check if the file extension is .log + if not dnac_log_file_path.lower().endswith('.log'): + raise ValueError("The specified log file '{}' does not have a .log extension.".format(dnac_log_file_path)) + + # Change mode based on dnac_logs_append + if not __first_log_written and not dnac_logs_append: + mode = 'w' + else: + mode = 'a' + + with open(dnac_log_file_path, mode) as of: callerframerecord = inspect.stack()[1 + frameIncrement] frame = callerframerecord[0] info = inspect.getframeinfo(frame) - d = datetime.datetime.now().replace(microsecond=0).isoformat() - of.write("---- %s ---- %s@%s ---- %s: %s\n" % (d, info.lineno, info.function, level.upper(), msg)) + current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() + of.write("---- {} ---- {}@{} ---- {}: {}\n".format(current_datetime, info.lineno, info.function, level.upper(), msg)) + __first_log_written = True def is_list_complex(x): @@ -685,7 +734,7 @@ def validate_list_of_dicts(param_list, spec, module=None): break for param in spec: item = list_entry.get(param) - log(str(item)) + #log(str(item)) if item is None: if spec[param].get("required"): invalid_params.append( From 46ac3a1fe86040dca89bda54ffdcc33d34767413 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Wed, 24 Jan 2024 17:02:28 -0500 Subject: [PATCH 02/76] dnac_log_file_path and dnac_logs_apped parameters added to dnac.py --- plugins/module_utils/dnac.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index cf3eab445f..701e530e60 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -71,8 +71,10 @@ def __init__(self, module): # Check if 'dnac_log_level' in the playbook params. If available, # convert it to uppercase; otherwise, set it to 'INFO' self.dnac_log_level = dnac_params.get("dnac_log_level", "INFO").upper() + self.dnac_log_file_path = dnac_params.get("dnac_log_file_path") + self.dnac_logs_append = dnac_params.get("dnac_logs_append") - log(str(dnac_params)) + self.log(str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -153,7 +155,7 @@ def verify_diff_parsed(self): self.parsed = True return self - def log(self, message, level="info", frameIncrement=0): + def log(self, message, level="INFO", frameIncrement=0): """Logs/Appends messages into dnac.log file if logging is enabled and the log level is appropriate Args: self (obj, required): An instance of the DnacBase Class. @@ -428,7 +430,7 @@ def log(msg, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, Note: Validates if the directory exists and raises an exception if it doesn't. - File gets created if it doesn't exist + File gets created if it doesn't already exist. """ global __first_log_written @@ -451,7 +453,6 @@ def log(msg, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, mode = 'w' else: mode = 'a' - with open(dnac_log_file_path, mode) as of: callerframerecord = inspect.stack()[1 + frameIncrement] frame = callerframerecord[0] From 3b1be6e03d1b083d6c4cc51e4cd94c055dc2e39a Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Wed, 24 Jan 2024 17:46:29 -0500 Subject: [PATCH 03/76] sanity test bug fixes --- plugins/module_utils/dnac.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 701e530e60..e873fd6a10 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -170,7 +170,8 @@ def log(self, message, level="INFO", frameIncrement=0): # Validate dnac_log_level and level valid_log_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') if level and self.dnac_log_level not in valid_log_levels: - raise ValueError("Invalid log level: 'dnac_log_level: {}' and 'level: {}'. Expected one of {}.".format(self.dnac_log_level, level, valid_log_levels)) + raise ValueError("Invalid log level: 'dnac_log_level: {}' and 'level: {}'." + " Expected one of {}.".format(self.dnac_log_level, level, valid_log_levels)) if ( self.dnac_log @@ -418,8 +419,8 @@ def camel_to_snake_case(self, config): def log(msg, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, frameIncrement=0): - """Logs/Appends messages into the specified log file or to dnac.log file by default - + """Logs/Appends messages into the specified log file or to dnac.log file by default + Args: msg (str, required): The log message to be recorded. level (str, optional): The log level, default is "info". @@ -427,8 +428,8 @@ def log(msg, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, dnac_log_file_path (str, optional): The path to the log file, default is 'dnac.log'. dnac_logs_append (bool, optional): A boolean indicating whether to append to an existing log file or create a new one, default is True. frameIncrement (int, optional): The number of frames to increment in the call stack, default is 0. - - Note: + + Note: Validates if the directory exists and raises an exception if it doesn't. File gets created if it doesn't already exist. """ @@ -737,7 +738,7 @@ def validate_list_of_dicts(param_list, spec, module=None): break for param in spec: item = list_entry.get(param) - #log(str(item)) + # log(str(item)) if item is None: if spec[param].get("required"): invalid_params.append( From e9025d96b612f77c9e1523167ab2d847b1b25dc9 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Wed, 24 Jan 2024 17:49:14 -0500 Subject: [PATCH 04/76] sanity test bug fixes --- plugins/module_utils/dnac.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index e873fd6a10..c001c53b29 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -170,8 +170,8 @@ def log(self, message, level="INFO", frameIncrement=0): # Validate dnac_log_level and level valid_log_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') if level and self.dnac_log_level not in valid_log_levels: - raise ValueError("Invalid log level: 'dnac_log_level: {}' and 'level: {}'." - " Expected one of {}.".format(self.dnac_log_level, level, valid_log_levels)) + raise ValueError("Invalid log level: 'dnac_log_level: {0}' and 'level: {1}'." + " Expected one of {2}.".format(self.dnac_log_level, level, valid_log_levels)) if ( self.dnac_log @@ -443,11 +443,11 @@ def log(msg, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, # Validate if the directory exists log_directory = os.path.dirname(dnac_log_file_path) if not os.path.exists(log_directory): - raise FileNotFoundError("The directory for log file '{}' does not exist.".format(dnac_log_file_path)) + raise FileNotFoundError("The directory for log file '{0}' does not exist.".format(dnac_log_file_path)) # Check if the file extension is .log if not dnac_log_file_path.lower().endswith('.log'): - raise ValueError("The specified log file '{}' does not have a .log extension.".format(dnac_log_file_path)) + raise ValueError("The specified log file '{0}' does not have a .log extension.".format(dnac_log_file_path)) # Change mode based on dnac_logs_append if not __first_log_written and not dnac_logs_append: @@ -459,7 +459,7 @@ def log(msg, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, frame = callerframerecord[0] info = inspect.getframeinfo(frame) current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() - of.write("---- {} ---- {}@{} ---- {}: {}\n".format(current_datetime, info.lineno, info.function, level.upper(), msg)) + of.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level.upper(), msg)) __first_log_written = True From eb9bf08f7382c00ee061598fdee10cd6e80f9bd8 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Wed, 24 Jan 2024 17:56:34 -0500 Subject: [PATCH 05/76] sanity test bug fixes --- plugins/module_utils/dnac.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index c001c53b29..dd12323298 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -170,8 +170,8 @@ def log(self, message, level="INFO", frameIncrement=0): # Validate dnac_log_level and level valid_log_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') if level and self.dnac_log_level not in valid_log_levels: - raise ValueError("Invalid log level: 'dnac_log_level: {0}' and 'level: {1}'." - " Expected one of {2}.".format(self.dnac_log_level, level, valid_log_levels)) + raise ValueError("Invalid log level: 'dnac_log_level:{0}' and 'level:{1}'." + " Expected one of {2}.".format(self.dnac_log_level, level, valid_log_levels)) if ( self.dnac_log From aff02c6f8647ecc8cf2b291c40300f3928612627 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Thu, 25 Jan 2024 00:57:15 -0500 Subject: [PATCH 06/76] bug fix for older versions --- plugins/module_utils/dnac.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index dd12323298..b335b58fd3 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -71,12 +71,13 @@ def __init__(self, module): # Check if 'dnac_log_level' in the playbook params. If available, # convert it to uppercase; otherwise, set it to 'INFO' self.dnac_log_level = dnac_params.get("dnac_log_level", "INFO").upper() - self.dnac_log_file_path = dnac_params.get("dnac_log_file_path") - self.dnac_logs_append = dnac_params.get("dnac_logs_append") + self.dnac_log_file_path = dnac_params.get("dnac_log_file_path", "dnac.log") + self.dnac_logs_append = dnac_params.get("dnac_logs_append", "True") self.log(str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} + self.log_written = False @abstractmethod def validate_input(self): @@ -178,7 +179,8 @@ def log(self, message, level="INFO", frameIncrement=0): and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level) ): message = "Module: " + self.__class__.__name__ + ", " + message - log(message, level, self.dnac_log_file_path, self.dnac_logs_append, (1 + frameIncrement)) + log(message, self.log_written, level, self.dnac_log_file_path, self.dnac_logs_append, (1 + frameIncrement)) + self.log_written = True def check_return_status(self): """API to check the return status value and exit/fail the module""" @@ -418,7 +420,7 @@ def camel_to_snake_case(self, config): return new_config -def log(msg, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, frameIncrement=0): +def log(msg, log_written, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, frameIncrement=0): """Logs/Appends messages into the specified log file or to dnac.log file by default Args: @@ -459,7 +461,7 @@ def log(msg, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, frame = callerframerecord[0] info = inspect.getframeinfo(frame) current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() - of.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level.upper(), msg)) + of.write("{5}-{6}--- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level.upper(), msg, log_written, frameIncrement)) __first_log_written = True From d5589a90a891cc59e6e89d3c7ebae4a895d36bd3 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Thu, 25 Jan 2024 01:03:21 -0500 Subject: [PATCH 07/76] bug fix for older versions --- plugins/module_utils/dnac.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index b335b58fd3..1af699d4fb 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -77,7 +77,6 @@ def __init__(self, module): self.log(str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} - self.log_written = False @abstractmethod def validate_input(self): @@ -179,8 +178,7 @@ def log(self, message, level="INFO", frameIncrement=0): and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level) ): message = "Module: " + self.__class__.__name__ + ", " + message - log(message, self.log_written, level, self.dnac_log_file_path, self.dnac_logs_append, (1 + frameIncrement)) - self.log_written = True + log(message, level, self.dnac_log_file_path, self.dnac_logs_append, (1 + frameIncrement)) def check_return_status(self): """API to check the return status value and exit/fail the module""" @@ -420,7 +418,7 @@ def camel_to_snake_case(self, config): return new_config -def log(msg, log_written, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, frameIncrement=0): +def log(msg, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, frameIncrement=0): """Logs/Appends messages into the specified log file or to dnac.log file by default Args: @@ -461,7 +459,7 @@ def log(msg, log_written, level='INFO', dnac_log_file_path='dnac.log', dnac_logs frame = callerframerecord[0] info = inspect.getframeinfo(frame) current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() - of.write("{5}-{6}--- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level.upper(), msg, log_written, frameIncrement)) + of.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level.upper(), msg)) __first_log_written = True @@ -896,4 +894,4 @@ def main(): if __name__ == "__main__": - main() + main() \ No newline at end of file From e8b065647a8a660fc7ff0619ae7926e1da35e8dd Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Thu, 25 Jan 2024 01:07:41 -0500 Subject: [PATCH 08/76] bug fix for older versions --- plugins/module_utils/dnac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 1af699d4fb..271ca5c614 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -894,4 +894,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() From b244e62b47f404f23205f6e639eb0b622ef1a58d Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Thu, 25 Jan 2024 22:03:12 -0500 Subject: [PATCH 09/76] bug fix for when dnac_log_level not specified in the playbook --- plugins/module_utils/dnac.py | 83 +++++++++--------------------------- 1 file changed, 21 insertions(+), 62 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 271ca5c614..5b5efd6b31 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -28,9 +28,6 @@ import inspect import re -# Global log config variables -__first_log_written = False - class DnacBase(): @@ -68,13 +65,13 @@ def __init__(self, module): } self.dnac_log = dnac_params.get("dnac_log") - # Check if 'dnac_log_level' in the playbook params. If available, - # convert it to uppercase; otherwise, set it to 'INFO' - self.dnac_log_level = dnac_params.get("dnac_log_level", "INFO").upper() - self.dnac_log_file_path = dnac_params.get("dnac_log_file_path", "dnac.log") - self.dnac_logs_append = dnac_params.get("dnac_logs_append", "True") + self.dnac_log_level = dnac_params.get("dnac_log_level") + self.is_valid_log_level() + if self.dnac_log_level is None: + self.dnac_log_level = "INFO" + self.dnac_log_level = self.dnac_log_level.upper() - self.log(str(dnac_params)) + log(str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -155,7 +152,7 @@ def verify_diff_parsed(self): self.parsed = True return self - def log(self, message, level="INFO", frameIncrement=0): + def log(self, message, level="info", frameIncrement=0): """Logs/Appends messages into dnac.log file if logging is enabled and the log level is appropriate Args: self (obj, required): An instance of the DnacBase Class. @@ -166,19 +163,13 @@ def log(self, message, level="INFO", frameIncrement=0): """ level = level.upper() - - # Validate dnac_log_level and level - valid_log_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') - if level and self.dnac_log_level not in valid_log_levels: - raise ValueError("Invalid log level: 'dnac_log_level:{0}' and 'level:{1}'." - " Expected one of {2}.".format(self.dnac_log_level, level, valid_log_levels)) - if ( self.dnac_log + and self.dnac_log_level in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level) ): message = "Module: " + self.__class__.__name__ + ", " + message - log(message, level, self.dnac_log_file_path, self.dnac_logs_append, (1 + frameIncrement)) + log(message, level, (1 + frameIncrement)) def check_return_status(self): """API to check the return status value and exit/fail the module""" @@ -222,9 +213,7 @@ def get_dnac_params(self, params): "dnac_verify": params.get("dnac_verify"), "dnac_debug": params.get("dnac_debug"), "dnac_log": params.get("dnac_log"), - "dnac_log_level": params.get("dnac_log_level"), - "dnac_log_file_path": params.get("dnac_log_file_path"), - "dnac_logs_append": params.get("dnac_logs_append"), + "dnac_log_level": params.get("dnac_log_level") } return dnac_params @@ -248,7 +237,7 @@ def get_task_details(self, task_id): params={"task_id": task_id} ) - self.log(str(response)) + log(str(response)) if response and isinstance(response, dict): result = response.get('response') @@ -417,50 +406,20 @@ def camel_to_snake_case(self, config): return config return new_config + def is_valid_log_level(self): + valid_log_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') + if self.dnac_log_level not in valid_log_levels: + raise ValueError("Invalid log level: 'dnac_log_level:{0}'." + " Expected one of {1}.".format(self.dnac_log_level, valid_log_levels)) -def log(msg, level='INFO', dnac_log_file_path='dnac.log', dnac_logs_append=True, frameIncrement=0): - """Logs/Appends messages into the specified log file or to dnac.log file by default - - Args: - msg (str, required): The log message to be recorded. - level (str, optional): The log level, default is "info". - The log level can be one of 'DEBUG', 'INFO', 'WARNING', 'ERROR', or 'CRITICAL'. - dnac_log_file_path (str, optional): The path to the log file, default is 'dnac.log'. - dnac_logs_append (bool, optional): A boolean indicating whether to append to an existing log file or create a new one, default is True. - frameIncrement (int, optional): The number of frames to increment in the call stack, default is 0. - - Note: - Validates if the directory exists and raises an exception if it doesn't. - File gets created if it doesn't already exist. - """ - - global __first_log_written - - # Validate dnac_log_file_path is absolute or relative - if not os.path.isabs(dnac_log_file_path): - dnac_log_file_path = os.path.join(os.getcwd(), dnac_log_file_path) - - # Validate if the directory exists - log_directory = os.path.dirname(dnac_log_file_path) - if not os.path.exists(log_directory): - raise FileNotFoundError("The directory for log file '{0}' does not exist.".format(dnac_log_file_path)) - - # Check if the file extension is .log - if not dnac_log_file_path.lower().endswith('.log'): - raise ValueError("The specified log file '{0}' does not have a .log extension.".format(dnac_log_file_path)) - # Change mode based on dnac_logs_append - if not __first_log_written and not dnac_logs_append: - mode = 'w' - else: - mode = 'a' - with open(dnac_log_file_path, mode) as of: +def log(msg, level='info', frameIncrement=0): + with open('dnac.log', 'a') as of: callerframerecord = inspect.stack()[1 + frameIncrement] frame = callerframerecord[0] info = inspect.getframeinfo(frame) - current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() - of.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level.upper(), msg)) - __first_log_written = True + d = datetime.datetime.now().replace(microsecond=0).isoformat() + of.write("---- %s ---- %s@%s ---- %s: %s\n" % (d, info.lineno, info.function, level.upper(), msg)) def is_list_complex(x): @@ -738,7 +697,7 @@ def validate_list_of_dicts(param_list, spec, module=None): break for param in spec: item = list_entry.get(param) - # log(str(item)) + log(str(item)) if item is None: if spec[param].get("required"): invalid_params.append( From 91b2c5b5b6e57de1dc6e6c1097047be3d565ecc4 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Thu, 25 Jan 2024 22:11:50 -0500 Subject: [PATCH 10/76] bug fix for when dnac_log_level not specified in the playbook --- plugins/module_utils/dnac.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 5b5efd6b31..c9c90e658e 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -165,7 +165,6 @@ def log(self, message, level="info", frameIncrement=0): level = level.upper() if ( self.dnac_log - and self.dnac_log_level in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level) ): message = "Module: " + self.__class__.__name__ + ", " + message From f27c01822a4b55052d3490afddab242da63f7de7 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Thu, 25 Jan 2024 22:46:07 -0500 Subject: [PATCH 11/76] Bug fix for modules that dont support dnac_log_level parameter --- plugins/module_utils/dnac.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index c9c90e658e..4e44c11bdf 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -66,10 +66,11 @@ def __init__(self, module): self.dnac_log = dnac_params.get("dnac_log") self.dnac_log_level = dnac_params.get("dnac_log_level") - self.is_valid_log_level() if self.dnac_log_level is None: self.dnac_log_level = "INFO" - self.dnac_log_level = self.dnac_log_level.upper() + else: + self.dnac_log_level = self.dnac_log_level.upper() + self.is_valid_log_level() log(str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] From eff6fbf4c3246418dd81d11b24cce95389527423 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 26 Jan 2024 09:56:07 +0530 Subject: [PATCH 12/76] update any interface details with it's name and device ip, added backward compatiblity for swim module, add log messgaes with severity for site and swim module --- plugins/modules/inventory_intent.py | 55 ++++++-- plugins/modules/site_intent.py | 201 ++++++++++++++------------- plugins/modules/swim_intent.py | 205 ++++++++++++++++------------ 3 files changed, 262 insertions(+), 199 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 6547aa9937..94436c0999 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -196,6 +196,8 @@ voice_vlan_id: description: Identifier used to distinguish a specific VLAN that is dedicated to voice traffic. type: int + interface_name: + description: Name of Interface in order to update the interface details of device.(Eg GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) deployment_mode: description: Preview/Deploy [Preview means the configuration is not pushed to the device. Deploy makes the configuration pushed to the device] type: str @@ -529,6 +531,7 @@ vlan_id: int voice_vlan_id: int deployment_mode: str + interface_name: str - name: Export Device Details in a CSV file Interface details with IP Address cisco.dnac.inventory_intent: @@ -747,6 +750,7 @@ def validate_input(self): 'description': {'type': 'str'}, 'vlan_id': {'type': 'int'}, 'voice_vlan_id': {'type': 'int'}, + 'interface_name': {'type': 'str'}, }, 'export_device_list': { 'type': 'dict', @@ -1980,12 +1984,13 @@ def get_device_ids(self, device_ips): return device_ids - def get_interface_from_ip(self, device_ip): + def get_interface_from_id_and_name(self, device_id, interface_name): """ Get the interface ID for a device in Cisco DNA Center based on its IP address. Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. - device_ip (str): The IP address of the device. + device_id (str): The id of the device. + interface_name(str): name of the interface for which details need to be collected. Returns: str: The interface ID for the specified device. Description: @@ -1995,17 +2000,21 @@ def get_interface_from_ip(self, device_ip): """ try: + interface_detail_params = { + 'device_id': device_id, + 'name': interface_name + } response = self.dnac._exec( family="devices", - function='get_interface_by_ip', - params={"ip_address": device_ip} + function='get_interface_details', + params=interface_detail_params ) self.log(str(response)) response = response.get("response") if response: - interface_id = response[0]["id"] - self.log("Fetch Interface Id for device {0} successfully !!".format(device_ip)) + interface_id = response["id"] + self.log("Fetch Interface Id for device successfully !!") return interface_id except Exception as e: @@ -2062,7 +2071,7 @@ def check_device_role(self, device_ip): return response.get('role') == role and response.get('roleSource') == role_source - def check_interface_details(self, device_ip): + def check_interface_details(self, device_ip, interface_name): """ Checks if the interface details for a device in Cisco DNA Center match the specified values in the configuration. Args: @@ -2076,13 +2085,26 @@ def check_interface_details(self, device_ip): If all specified parameters match the retrieved values or are not provided in the playbook parameters, the function returns True, indicating successful validation. """ + device_id = self.get_device_ids([device_ip]) + if not device_id: + self.log("Device {0} not present in Cisco Catalyst Center so cannot update interface details".format(device_ip)) + return False + interface_detail_params = { + 'device_id': device_id[0], + 'name': interface_name + } response = self.dnac._exec( family="devices", - function='get_interface_by_ip', - params={"ip_address": device_ip} + function='get_interface_details', + params=interface_detail_params ) - response = response.get("response")[0] + self.log(str(response)) + response = response.get("response") + + if not response: + return False + response_params = { 'description': response.get('description'), 'adminStatus': response.get('adminStatus'), @@ -2229,7 +2251,7 @@ def get_diff_merged(self, config): field_name = self.config[0].get('add_user_defined_field').get('name') if field_name is None: - self.msg = "Mandatory paramter for User Define Field - name is missing" + self.msg = "Mandatory paramter for User Define Field 'name' is missing" self.status = "failed" return self @@ -2271,7 +2293,7 @@ def get_diff_merged(self, config): break if not device_present: - msg = "Cannot perform Update operation as device - {0} not present in Cisco DNA Center".format(str(device_to_update)) + msg = "Cannot perform Update operation as device: {0} not present in Cisco DNA Center".format(str(device_to_update)) self.status = "success" self.result['changed'] = False self.result['response'] = msg @@ -2369,7 +2391,11 @@ def get_diff_merged(self, config): if self.config[0].get('update_interface_details'): # Call the Get interface details by device IP API and fetch the interface Id for device_ip in device_to_update: - interface_id = self.get_interface_from_ip(device_ip) + interface_params = self.config[0].get('update_interface_details') + interface_name = interface_params.get('interface_name') + device_id = self.get_device_ids([device_ip]) + interface_id = self.get_interface_from_id_and_name(device_id[0], interface_name) + # Now we call update interface details api with required parameter try: interface_params = self.config[0].get('update_interface_details') @@ -2781,9 +2807,10 @@ def verify_diff_merged(self, config): if device_updated and self.config[0].get('update_interface_details'): interface_update_flag = True + interface_name = self.config[0].get('update_interface_details').get('interface_name') for device_ip in device_ips: - if not self.check_interface_details(device_ip): + if not self.check_interface_details(device_ip, interface_name): interface_update_flag = False break diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 203a5ca896..45b6317fa5 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -26,7 +26,7 @@ Abhishek Maheshwari (@abhishekmaheshwari) options: config_verify: - description: Set to True to verify the Cisco DNA Center config after applying the playbook config. + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool default: False dnac_log_level: @@ -35,7 +35,7 @@ type: str default: INFO state: - description: The state of DNAC after module completion. + description: The state of Catalyst Center after module completion. type: str choices: [ merged, deleted ] default: merged @@ -235,7 +235,7 @@ RETURN = r""" #Case_1: Site is successfully created/updated/deleted response_1: - description: A dictionary with API execution details as returned by the Cisco DNAC Python SDK + description: A dictionary with API execution details as returned by the Cisco Catalyst Center Python SDK returned: always type: dict sample: > @@ -276,7 +276,7 @@ #Case_3: Error while creating/updating/deleting site response_3: - description: A dictionary with API execution details as returned by the Cisco DNAC Python SDK + description: A dictionary with API execution details as returned by the Cisco Catalyst Center Python SDK returned: always type: dict sample: > @@ -301,7 +301,7 @@ #Case_4: Site not found when atempting to delete site response_4: - description: A list with the response returned by the Cisco DNAC Python + description: A list with the response returned by the Cisco Catalyst Center Python returned: always type: list sample: > @@ -341,7 +341,7 @@ def validate_input(self): Checks the configuration provided in the playbook against a predefined specification to ensure it adheres to the expected structure and data types. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: The method returns an instance of the class with updated attributes: - self.msg: A message describing the validation result. @@ -355,8 +355,9 @@ def validate_input(self): """ if not self.config: - self.msg = "config not available in playbook for validattion" self.status = "success" + self.msg = "config not available in playbook for validattion" + self.log(self.msg, "ERROR") return self temp_spec = dict( @@ -364,6 +365,7 @@ def validate_input(self): site=dict(required=True, type='dict'), ) self.config = self.camel_to_snake_case(self.config) + # Validate site params valid_temp, invalid_params = validate_list_of_dicts( self.config, temp_spec @@ -373,20 +375,22 @@ def validate_input(self): self.msg = "Invalid parameters in playbook: {0}".format( "\n".join(invalid_params) ) + self.log(self.msg, "ERROR") self.status = "failed" return self self.validated_config = valid_temp - log(str(valid_temp)) - self.msg = "Successfully validated input" + self.msg = "Successfully validated playbook config params: {0}".format(str(valid_temp)) + self.log(self.msg, "INFO") self.status = "success" + return self def get_current_site(self, site): """ Get the current site information. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. - site (list): A list containing information about the site. Returns: - dict: A dictionary containing the extracted site information. @@ -446,13 +450,13 @@ def get_current_site(self, site): siteId=site[0].get("id") ) - log(str(current_site)) + self.log("Current site details: {0}".format(str(current_site)), "INFO") return current_site def site_exists(self): """ - Check if the site exists in Cisco DNA Center. + Check if the site exists in Cisco Catalyst Center. Parameters: - self (object): An instance of the class containing the method. @@ -464,7 +468,7 @@ def site_exists(self): - dict: Contains information about the existing site. If the site doesn't exist, this dictionary is empty. Description: - Checks the existence of a site in Cisco DNA Center by querying the + Checks the existence of a site in Cisco Catalyst Center by querying the 'get_site' function in the 'sites' family. It utilizes the 'site_name' parameter from the 'want' attribute to identify the site. """ @@ -480,16 +484,14 @@ def site_exists(self): ) except Exception as e: - self.log("The input site {0} is not valid or site is not present.".format(self.want.get("site_name"))) + self.log("The input site {0} is not valid or site is not present.".format(self.want.get("site_name")), "WARNING") if response: - self.log(str(response)) - response = response.get("response") + self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") current_site = self.get_current_site(response) site_exists = True - - log(str(self.validated_config)) + self.log("Site {0} exist in Cisco Catalyst Center".format(self.want.get("site_name")), "INFO") return (site_exists, current_site) @@ -498,7 +500,7 @@ def get_site_params(self, params): Store the site-related parameters. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. - params (dict): Dictionary containing site-related parameters. Returns: - dict: Dictionary containing the stored site-related parameters. @@ -543,12 +545,13 @@ def get_site_params(self, params): try: site_info["floor"]["rfModel"] = floor_details.get("rf_model") except Exception as e: - self.log("Floor doesnot have rfModel attribute") + self.log("Floor {0} doesnot have rfModel attribute".format(floor_details.get('name')), "WARNING") site_params = dict( type=typeinfo, site=site_info, ) + self.log("Parameters for site is: {0}".format(str(site_params)), "DEBUG") return site_params @@ -556,7 +559,7 @@ def get_site_name(self, site): """ Get and Return the site name. Parameters: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - site (dict): A dictionary containing information about the site. Returns: - str: The constructed site name. @@ -570,7 +573,7 @@ def get_site_name(self, site): parent_name = site.get("site").get(site_type).get("parent_name") name = site.get("site").get(site_type).get("name") site_name = '/'.join([parent_name, name]) - self.log(site_name) + self.log("Name of the site is: {0}".format(site_name), "INFO") return site_name @@ -578,7 +581,7 @@ def compare_float_values(self, ele1, ele2, precision=2): """ Compare two floating-point values with a specified precision. Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - ele1 (float): The first floating-point value to be compared. - ele2 (float): The second floating-point value to be compared. - precision (int, optional): The number of decimal places to consider in the comparison, Defaults to 2. @@ -596,7 +599,7 @@ def is_area_updated(self, updated_site, requested_site): """ Check if the area site details have been updated. Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - updated_site (dict): The site details after the update. - requested_site (dict): The site details as requested for the update. Return: @@ -616,7 +619,7 @@ def is_building_updated(self, updated_site, requested_site): """ Check if the building details in a site have been updated. Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - updated_site (dict): The site details after the update. - requested_site (dict): The site details as requested for the update. Return: @@ -641,7 +644,7 @@ def is_floor_updated(self, updated_site, requested_site): Check if the floor details in a site have been updated. Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - updated_site (dict): The site details after the update. - requested_site (dict): The site details as requested for the update. Return: @@ -666,7 +669,7 @@ def site_requires_update(self): """ Check if the site requires updates. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: bool: True if the site requires updates, False otherwise. Description: @@ -679,8 +682,8 @@ def site_requires_update(self): type = self.have['current_site']['type'] updated_site = self.have['current_site']['site'][type] requested_site = self.want['site_params']['site'][type] - self.log("Current Site: " + str(updated_site)) - self.log("Requested Site: " + str(requested_site)) + self.log("Current Site type: {0}".format(str(updated_site)), "INFO") + self.log("Requested Site type: {0}".format(str(requested_site)), "INFO") if type == "building": return not self.is_building_updated(updated_site, requested_site) @@ -692,14 +695,14 @@ def site_requires_update(self): def get_have(self, config): """ - Get the site details from DNAC + Get the site details from Cisco Catalyst Center Parameters: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): A dictionary containing the configuration details. Returns: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This method queries Cisco DNA Center to check if a specified site + This method queries Cisco Catalyst Center to check if a specified site exists. If the site exists, it retrieves details about the current site, including the site ID and other relevant information. The results are stored in the 'have' attribute for later reference. @@ -712,7 +715,7 @@ def get_have(self, config): # check if given site exits, if exists store current site info (site_exists, current_site) = self.site_exists() - log("Site Exists: " + str(site_exists) + "\n Current Site:" + str(current_site)) + self.log("Current Site details: {0}".format(str(current_site)), "DEBUG") if site_exists: have["site_id"] = current_site.get("siteId") @@ -720,20 +723,21 @@ def get_have(self, config): have["current_site"] = current_site self.have = have + self.log("Current State (have): {0}".format(str(self.have)), "INFO") return self def get_want(self, config): """ - Get all site-related information from the playbook needed for creation/updation/deletion of site in Cisco DNA Center. + Get all site-related information from the playbook needed for creation/updation/deletion of site in Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. config (dict): A dictionary containing configuration information. Returns: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: Retrieves all site-related information from playbook that is - required for creating a site in Cisco DNA Center. It includes + required for creating a site in Cisco Catalyst Center. It includes parameters such as 'site_params' and 'site_name.' The gathered information is stored in the 'want' attribute for later reference. """ @@ -743,25 +747,25 @@ def get_want(self, config): site_params=self.get_site_params(config), site_name=self.get_site_name(config), ) - self.want = want + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") return self def get_diff_merged(self, config): """ - Update/Create site information in Cisco DNA Center with fields + Update/Create site information in Cisco Catalyst Center with fields provided in the playbook. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. config (dict): A dictionary containing configuration information. Returns: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This method determines whether to update or create a site in Cisco DNA Center based on the provided + This method determines whether to update or create a site in Cisco Catalyst Center based on the provided configuration information. If the specified site exists, the method checks if it requires an update by calling the 'site_requires_update' method. If an update is required, it calls the 'update_site' - function from the 'sites' family of the Cisco DNA Center API. If the site does not require an update, + function from the 'sites' family of the Cisco Catalyst Center API. If the site does not require an update, the method exits, indicating that the site is up to date. """ @@ -787,7 +791,7 @@ def get_diff_merged(self, config): # Site does not neet update self.result['response'] = self.have.get("current_site") self.msg = "Site - {0} does not need any update".format(self.have.get("current_site")) - self.log(self.msg) + self.log(self.msg, "INFO") self.result['msg'] = self.msg return self @@ -800,8 +804,7 @@ def get_diff_merged(self, config): op_modifies=True, params=self.want.get("site_params"), ) - - log(str(response)) + self.log("Received API response from 'create_site': {0}".format(str(response)), "DEBUG") site_created = True if site_created or site_updated: @@ -821,7 +824,7 @@ def get_diff_merged(self, config): if site_updated: log_msg = "Site - {0} Updated Successfully".format(self.want.get("site_name")) - self.log(log_msg) + self.log(log_msg, "INFO") self.result['msg'] = log_msg self.result['response'].update({"siteId": self.have.get("site_id")}) @@ -830,9 +833,9 @@ def get_diff_merged(self, config): (site_exists, current_site) = self.site_exists() if site_exists: - log_msg = "Site - {0} Created Successfully".format(current_site) - self.log(log_msg) - self.log("Current site:" + str(current_site)) + log_msg = "Site {0} created Successfully".format(self.want.get("site_name")) + self.log(log_msg, "INFO") + self.log("Current site: {0}".format(str(current_site)), "DEBUG") self.result['msg'] = log_msg self.result['response'].update({"siteId": current_site.get('site_id')}) @@ -840,15 +843,15 @@ def get_diff_merged(self, config): def delete_single_site(self, site_id, site_name): """" - Delete a single site in the Cisco DNA Center. + Delete a single site in the Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. site_id (str): The ID of the site to be deleted. site_name (str): The name of the site to be deleted. Returns: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This function initiates the deletion of a site in the Cisco DNA Center by calling the delete API. + This function initiates the deletion of a site in the Cisco Catalyst Center by calling the delete API. If the deletion is successful, the result is marked as changed, and the status is set to "success." If an error occurs during the deletion process, the status is set to "failed," and the log contains details about the error. @@ -862,33 +865,35 @@ def delete_single_site(self, site_id, site_name): ) if response and isinstance(response, dict): + self.log("Received API response from 'delete_site': {0}".format(str(response)), "DEBUG") executionid = response.get("executionId") + while True: execution_details = self.get_execution_details(executionid) if execution_details.get("status") == "SUCCESS": - msg = "Site - {0} deleted successfully".format(site_name) + self.msg = "Site - {0} deleted successfully".format(site_name) self.result['changed'] = True - self.result['response'] = msg + self.result['response'] = self.msg self.status = "success" - self.log(msg) + self.log(self.msg, "INFO") break elif execution_details.get("bapiError"): + self.log("Execution error response for 'delete_site': {0}".format(execution_details.get("bapiError")), "ERROR") self.module.fail_json(msg=execution_details.get("bapiError"), response=execution_details) break except Exception as e: - msg = "Cannot Delete device from Inventory because of {0}".format(str(e)) - self.log(msg) self.status = "failed" - self.msg = msg + self.msg = "Cannot Delete device from Inventory because of {0}".format(str(e)) + self.log(self.msg, "CRITICAL") return self def get_diff_deleted(self, config): """ - Call Cisco DNA Center API to delete sites with provided inputs. + Call Cisco Catalyst Center API to delete sites with provided inputs. Parameters: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): Dictionary containing information for site deletion. Returns: - self: The result dictionary includes the following keys: @@ -899,17 +904,19 @@ def get_diff_deleted(self, config): - 'msg' (str): A message indicating the status of the deletion operation. Description: This method initiates the deletion of a site by calling the 'delete_site' function in the 'sites' family - of the Cisco DNA Center API. It uses the site ID obtained from the 'have' attribute. + of the Cisco Catalyst Center API. It uses the site ID obtained from the 'have' attribute. """ site_exists = self.have.get("site_exists") + site_name = self.want.get("site_name") if not site_exists: - msg = msg = "Cannot delete Site - {0} as it's not found in Cisco DNA Center".format(self.want.get("site_name")) - self.result.update({'changed': False, - 'response': msg, - 'msg': msg}) - self.log(msg) self.status = "success" + self.msg = "Cannot delete Site - {0} as it's not found in Cisco Catalyst Center".format(site_name) + self.result.update({'changed': False, + 'response': self.msg, + 'msg': self.msg}) + self.log(self.msg, "WARNING") + return self # Check here if the site have the childs then fetch it using get membership API and then sort it @@ -921,9 +928,10 @@ def get_diff_deleted(self, config): params={"site_id": site_id}, ) site_response = mem_response.get("site").get("response") + self.log("Site {0} response along with it's child sites: {1}".format(site_name, str(site_response)), "DEBUG") if len(site_response) == 0: - self.delete_single_site(site_id, self.want.get("site_name")) + self.delete_single_site(site_id, site_name) return self # Sorting the response in reverse order based on hierarchy levels @@ -934,75 +942,78 @@ def get_diff_deleted(self, config): self.delete_single_site(item['id'], item['name']) # Delete the final parent site - self.delete_single_site(site_id, self.want.get("site_name")) - msg = "Site - {0} and it's child sites deleted successfully".format(self.want.get("site_name")) - self.result['response'] = msg - self.log(msg) + self.delete_single_site(site_id, site_name) + self.msg = "Site - {0} and it's child sites deleted successfully".format(site_name) + self.result['response'] = self.msg + self.log(self.msg, "INFO") return self def verify_diff_merged(self, config): """ - Verify the merged status(Creation/Updation) of site configuration in Cisco DNA Center. + Verify the merged status(Creation/Updation) of site configuration in Cisco Catalyst Center. Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): The configuration details to be verified. Return: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This method checks the merged status of a configuration in Cisco DNA Center by retrieving the current state + This method checks the merged status of a configuration in Cisco Catalyst Center by retrieving the current state (have) and desired state (want) of the configuration, logs the states, and validates whether the specified - site exists in the DNA Center configuration. + site exists in the Catalyst Center configuration. """ self.get_have(config) - self.log(str(self.have)) - self.log(str(self.want)) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + # Code to validate dnac config for merged state site_exist = self.have.get("site_exists") + site_name = self.want.get("site_name") if site_exist: self.status = "success" - msg = "Requested Site - {0} present in Cisco DNA Center and creation verified.".format(self.want.get("site_name")) - self.log(msg) + self.msg = "Requested Site - {0} present in Cisco Catalyst Center and creation verified.".format(site_name) + self.log(self.msg, "INFO") require_update = self.site_requires_update() if not require_update: - self.log("Site - {0} Updation Verified Successfully.".format(self.want.get("site_name"))) + self.log("Site - {0} Updation Verified Successfully.".format(site_name), "INFO") self. status = "success" return self - self.log("Playbook paramater doesnot match with the Cisco DNA Center means Merged task not executed successfully.") + self.log("Playbook paramater doesnot match with the Cisco Catalyst Center means Merged task not executed successfully.", "INFO") return self def verify_diff_deleted(self, config): """ - Verify the deletion status of site configuration in Cisco DNA Center. + Verify the deletion status of site configuration in Cisco Catalyst Center. Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): The configuration details to be verified. Return: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This method checks the deletion status of a configuration in Cisco DNA Center. - It validates whether the specified site exists in the DNA Center configuration. + This method checks the deletion status of a configuration in Cisco Catalyst Center. + It validates whether the specified site exists in the Catalyst Center configuration. """ self.get_have(config) - self.log(str(self.have)) - self.log(str(self.want)) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + # Code to validate dnac config for delete state site_exist = self.have.get("site_exists") if not site_exist: self.status = "success" - msg = "Requested Site - {0} already deleted from Cisco DNA Center and verified successfully.".format(self.want.get("site_name")) - self.log(msg) + msg = "Requested Site - {0} already deleted from Cisco Catalyst Center and verified successfully.".format(self.want.get("site_name")) + self.log(msg, "INFO") return self - self.log("Playbook paramater doesnot match with the Cisco DNA Center means Deletion not executed successfully.") + self.log("Playbook paramater doesnot match with the Cisco Catalyst Center means Deletion not executed successfully.", "INFO") return self diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index a1f483999c..fbb5bba19f 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -15,13 +15,13 @@ short_description: Intent module for SWIM related functions description: - Manage operation related to image importation, distribution, activation and tagging image as golden -- API to fetch a software image from remote file system using URL for HTTP/FTP and upload it to DNA Center. +- API to fetch a software image from remote file system using URL for HTTP/FTP and upload it to Catalyst Center. Supported image files extensions are bin, img, tar, smu, pie, aes, iso, ova, tar_gz and qcow2. -- API to fetch a software image from local file system and upload it to DNA Center +- API to fetch a software image from local file system and upload it to Catalyst Center Supported image files extensions are bin, img, tar, smu, pie, aes, iso, ova, tar_gz and qcow2. - API to tag/untag image as golen for a given family of devices - API to distribute a software image on a given device. Software image must be imported successfully into - DNA Center before it can be distributed. + Catalyst Center before it can be distributed. - API to activate a software image on a given device. Software image must be present in the device flash. version_added: '6.6.0' extends_documentation_fragment: @@ -36,7 +36,7 @@ type: str default: INFO state: - description: The state of DNAC after module completion. + description: The state of Catalyst Center after module completion. type: str choices: [ merged ] default: merged @@ -358,7 +358,7 @@ RETURN = r""" #Case: SWIM image is successfully imported, tagged as golden, distributed and activated on a device response: - description: A dictionary with activation details as returned by the DNAC Python SDK + description: A dictionary with activation details as returned by the Catalyst Center Python SDK returned: always type: dict sample: > @@ -417,8 +417,9 @@ def validate_input(self): """ if not self.config: - self.msg = "config not available in playbook for validattion" self.status = "success" + self.msg = "config not available in playbook for validation" + self.log(self.msg, "ERROR") return self temp_spec = dict( @@ -427,32 +428,36 @@ def validate_input(self): image_distribution_details=dict(type='dict'), image_activation_details=dict(type='dict'), ) + self.config = self.camel_to_snake_case(self.config) # Validate swim params valid_temp, invalid_params = validate_list_of_dicts( self.config, temp_spec ) + if invalid_params: self.msg = "Invalid parameters in playbook: {0}".format(invalid_params) + self.log(self.msg, "ERROR") self.status = "failed" return self self.validated_config = valid_temp - self.log(str(valid_temp)) - self.msg = "Successfully validated input" + self.msg = "Successfully validated playbook config params: {0}".format(str(valid_temp)) + self.log(self.msg, "INFO") self.status = "success" + return self def site_exists(self, site_name): """ Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: tuple: A tuple containing two values: - site_exists (bool): A boolean indicating whether the site exists (True) or not (False). - site_id (str or None): The ID of the site if it exists, or None if the site is not found. Description: - This method checks the existence of a site in the DNAC. If the site is found,it sets 'site_exists' to True, + This method checks the existence of a site in the Catalyst Center. If the site is found,it sets 'site_exists' to True, retrieves the site's ID, and returns both values in a tuple. If the site does not exist, 'site_exists' is set to False, and 'site_id' is None. If an exception occurs during the site lookup, an exception is raised. """ @@ -467,11 +472,11 @@ def site_exists(self, site_name): params={"name": site_name}, ) except Exception as e: + self.log("Exception occured- Site {0} doesnot exist in Cisco Catalyst Center".format(site_name), "CRITICAL") self.module.fail_json(msg="Site not found") if response: - self.log(str(response)) - + self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") site = response.get("response") site_id = site[0].get("id") site_exists = True @@ -482,14 +487,14 @@ def get_image_id(self, name): """ Retrieve the unique image ID based on the provided image name. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. name (str): The name of the software image to search for. Returns: str: The unique image ID (UUID) corresponding to the given image name. Raises: AnsibleFailJson: If the image is not found in the response. Description: - This function sends a request to Cisco DNAC to retrieve details about a software image based on its name. + This function sends a request to Cisco Catalyst Center to retrieve details about a software image based on its name. It extracts and returns the image ID if a single matching image is found. If no image or multiple images are found with the same name, it raises an exception. """ @@ -499,17 +504,16 @@ def get_image_id(self, name): function='get_software_image_details', params={"image_name": name}, ) - - self.log(str(image_response)) - + self.log("Received API response from 'get_software_image_details': {0}".format(str(image_response)), "DEBUG") image_list = image_response.get("response") + if (len(image_list) == 1): image_id = image_list[0].get("imageUuid") - self.log("Image Id: " + str(image_id)) + self.log("SWIM image {0} having the Id: {1}".format(name, image_id), "INFO") else: - error_message = "Image {0} not found".format(name) - self.log(error_message) - self.module.fail_json(msg="Image not found", response=image_response) + error_message = "SWIM image {0} not found".format(name) + self.log(error_message, "WARNING") + self.module.fail_json(msg="SWIM image not found", response=image_response) return image_id @@ -517,14 +521,14 @@ def is_image_exist(self, name): """ Retrieve the unique image ID based on the provided image name. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. name (str): The name of the software image to search for. Returns: str: The unique image ID (UUID) corresponding to the given image name. Raises: AnsibleFailJson: If the image is not found in the response. Description: - This function sends a request to Cisco DNAC to retrieve details about a software image based on its name. + This function sends a request to Cisco Catalyst Center to retrieve details about a software image based on its name. It extracts and returns the image ID if a single matching image is found. If no image or multiple images are found with the same name, it raises an exception. """ @@ -535,8 +539,9 @@ def is_image_exist(self, name): function='get_software_image_details', params={"image_name": name}, ) - self.log(str(image_response)) + self.log("Received API response from 'get_software_image_details': {0}".format(str(image_response)), "DEBUG") image_list = image_response.get("response") + if (len(image_list) == 1): image_exist = True @@ -546,12 +551,12 @@ def get_device_id(self, params): """ Retrieve the unique device ID based on the provided parameters. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. params (dict): A dictionary containing parameters to filter devices. Returns: str: The unique device ID corresponding to the filtered device. Description: - This function sends a request to Cisco DNA Center to retrieve a list of devices based on the provided + This function sends a request to Cisco Catalyst Center to retrieve a list of devices based on the provided filtering parameters. If a single matching device is found, it extracts and returns the device ID. If no device or multiple devices match the criteria, it raises an exception. """ @@ -561,14 +566,14 @@ def get_device_id(self, params): function='get_device_list', params=params, ) - self.log(str(response)) + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") device_list = response.get("response") if (len(device_list) == 1): device_id = device_list[0].get("id") - self.log("Device Id: " + str(device_id)) + self.log("Device Id: {0}".format(str(device_id)), "INFO") else: - self.log("Device not found") + self.log("Device not found", "WARNING") return device_id @@ -576,7 +581,7 @@ def get_device_uuids(self, site_name, device_family, device_role): """ Retrieve a list of device UUIDs based on the specified criteria. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. site_name (str): The name of the site for which device UUIDs are requested. device_family (str): The family/type of devices to filter on. device_role (str): The role of devices to filter on. If None, all roles are considered. @@ -606,7 +611,9 @@ def get_device_uuids(self, site_name, device_family, device_role): op_modifies=True, params=site_params, ) + self.log("Received API response from 'get_membership': {0}".format(str(response)), "DEBUG") response = response['device'][0]['response'] + if len(response) > 0: for item in response: if item["reachabilityStatus"] != "Reachable": @@ -620,14 +627,14 @@ def get_device_family_identifier(self, family_name): """ Retrieve and store the device family identifier based on the provided family name. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. family_name (str): The name of the device family for which to retrieve the identifier. Returns: None Raises: AnsibleFailJson: If the family name is not found in the response. Description: - This function sends a request to Cisco DNA Center to retrieve a list of device family identifiers.It then + This function sends a request to Cisco Catalyst Center to retrieve a list of device family identifiers.It then searches for a specific family name within the response and stores its associated identifier. If the family name is found, the identifier is stored; otherwise, an exception is raised. """ @@ -637,15 +644,18 @@ def get_device_family_identifier(self, family_name): family="software_image_management_swim", function='get_device_family_identifiers', ) - self.log(str(response)) + self.log("Received API response from 'get_device_family_identifiers': {0}".format(str(response)), "DEBUG") device_family_db = response.get("response") + if device_family_db: device_family_details = get_dict_result(device_family_db, 'deviceFamily', family_name) + if device_family_details: device_family_identifier = device_family_details.get("deviceFamilyIdentifier") have["device_family_identifier"] = device_family_identifier - self.log("Family device indentifier:" + str(device_family_identifier)) + self.log("Family device indentifier: {0}".format(str(device_family_identifier)), "INFO") else: + self.log("Device Family: {0} not found".format(str(family_name)), "ERROR") self.module.fail_json(msg="Family Device Name not found", response=[]) self.have.update(have) @@ -659,7 +669,7 @@ def get_have(self): Description: This function populates the 'have' dictionary with details related to software images, site information, device families, distribution devices, and activation devices based on user-provided data in the 'want' dictionary. - It validates and retrieves the necessary information from Cisco DNAC to support later actions. + It validates and retrieves the necessary information from Cisco Catalyst Center to support later actions. """ if self.want.get("tagging_details"): @@ -674,6 +684,7 @@ def get_have(self): have["tagging_image_id"] = self.have.get("imported_image_id") else: + self.log("Image details for tagging not provided", "CRITICAL") self.module.fail_json(msg="Image details for tagging not provided", response=[]) # check if given site exists, store siteid @@ -684,11 +695,11 @@ def get_have(self): (site_exists, site_id) = self.site_exists(site_name) if site_exists: have["site_id"] = site_id - self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) + self.log("Site {0} exists having the site id: {1}".format(site_name, str(site_id)), "DEBUG") else: # For global site, use -1 as siteId have["site_id"] = "-1" - self.log("Site Name not given by user. Using global site.") + self.log("Site Name not given by user. Using global site.", "WARNING") self.have.update(have) # check if given device family name exists, store indentifier value @@ -705,7 +716,7 @@ def get_have(self): if site_exists: have["site_id"] = site_id - self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) + self.log("Site {0} exists having the site id: {1}".format(site_name, str(site_id)), "DEBUG") # check if image for distributon is available if distribution_details.get("image_name"): @@ -717,6 +728,7 @@ def get_have(self): have["distribution_image_id"] = self.have.get("imported_image_id") else: + self.log("Image details for distribution not provided", "CRITICAL") self.module.fail_json(msg="Image details for distribution not provided", response=[]) device_params = dict( @@ -741,8 +753,8 @@ def get_have(self): elif self.have.get("imported_image_id"): have["activation_image_id"] = self.have.get("imported_image_id") - else: + self.log("Image details for activation not provided", "CRITICAL") self.module.fail_json(msg="Image details for activation not provided", response=[]) site_name = activation_details.get("site_name") @@ -751,7 +763,7 @@ def get_have(self): (site_exists, site_id) = self.site_exists(site_name) if site_exists: have["site_id"] = site_id - self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) + self.log("Site {0} exists having the site id: {1}".format(site_name, str(site_id)), "INFO") device_params = dict( hostname=activation_details.get("device_hostname"), @@ -760,9 +772,11 @@ def get_have(self): macAddress=activation_details.get("device_mac_address"), ) device_id = self.get_device_id(device_params) + if device_id is not None: have["activation_device_id"] = device_id self.have.update(have) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") return self @@ -770,7 +784,7 @@ def get_want(self, config): """ Retrieve and store import, tagging, distribution, and activation details from playbook configuration. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. config (dict): The configuration dictionary containing image import and other details. Returns: self: The current instance of the class with updated 'want' attributes. @@ -791,6 +805,7 @@ def get_want(self, config): elif want["import_type"] == "local": want["local_import_details"] = config.get("import_image_details").get("local_image_details") else: + self.log("Incorrect import type. Supported Values: local or url", "CRITICAL") self.module.fail_json(msg="Incorrect import type. Supported Values: local or url") want["tagging_details"] = config.get("tagging_details") @@ -798,7 +813,7 @@ def get_want(self, config): want["activation_details"] = config.get("image_activation_details") self.want = want - self.log(str(self.want)) + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") return self @@ -806,9 +821,9 @@ def get_diff_import(self): """ Check the image import type and fetch the image ID for the imported image for further use. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: This function checks the type of image import (URL or local) and proceeds with the import operation accordingly. It then monitors the import task's progress and updates the 'result' dictionary. If the operation is successful, @@ -833,7 +848,7 @@ def get_diff_import(self): else: image_name = self.want.get("local_import_details").get("file_path") - # Code to check if the image already exists in DNAC + # Code to check if the image already exists in Catalyst Center name = image_name.split('/')[-1] image_exist = self.is_image_exist(name) @@ -847,9 +862,9 @@ def get_diff_import(self): if image_exist: image_id = self.get_image_id(name) self.have["imported_image_id"] = image_id - self.msg = "Image {0} already exists in the Cisco DNA Center".format(name) + self.msg = "Image {0} already exists in the Cisco Catalyst Center".format(name) self.result['msg'] = self.msg - self.log(self.msg) + self.log(self.msg, "INFO") self.status = "success" self.result['changed'] = False return self @@ -888,8 +903,7 @@ def get_diff_import(self): op_modifies=True, params=import_params, ) - - self.log(str(response)) + self.log("Received API response from {0}: {1}".format(import_function, str(response)), "DEBUG") task_details = {} task_id = response.get("response").get("taskId") @@ -904,21 +918,21 @@ def get_diff_import(self): self.status = "success" self.msg = "Swim Image {0} imported successfully".format(name) self.result['msg'] = self.msg - self.log(self.msg) + self.log(self.msg, "INFO") break if task_details and task_details.get("isError"): if "already exists" in task_details.get("failureReason", ""): - self.msg = "SWIM Image {0} already exists in the Cisco DNA Center".format(name) + self.msg = "SWIM Image {0} already exists in the Cisco Catalyst Center".format(name) self.result['msg'] = self.msg - self.log(self.msg) + self.log(self.msg, "INFO") self.status = "success" self.result['changed'] = False break else: self.status = "failed" self.msg = task_details.get("failureReason", "SWIM Image {0} seems to be invalid".format(image_name)) - self.log(self.msg) + self.log(self.msg, "WARNING") self.result['response'] = self.msg return self @@ -944,14 +958,14 @@ def get_diff_tagging(self): """ Tag or untag a software image as golden based on provided tagging details. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This function tags or untags a software image as a golden image in Cisco DNAC based on the provided + This function tags or untags a software image as a golden image in Cisco Catalyst Center based on the provided tagging details. The tagging action is determined by the value of the 'tagging' attribute in the 'tagging_details' dictionary.If 'tagging' is True, the image is tagged as golden, and if 'tagging' - is False, the golden tag is removed. The function sends the appropriate request to Cisco DNAC and updates the + is False, the golden tag is removed. The function sends the appropriate request to Cisco Catalyst Center and updates the task details in the 'result' dictionary. If the operation is successful, 'changed' is set to True. """ @@ -965,7 +979,7 @@ def get_diff_tagging(self): deviceFamilyIdentifier=self.have.get("device_family_identifier"), deviceRole=tagging_details.get("device_role") ) - self.log("Image params for tagging image as golden:" + str(image_params)) + self.log("Image params for tagging image as golden: {0}".format(str(image_params)), "INFO") response = self.dnac._exec( family="software_image_management_swim", @@ -973,7 +987,7 @@ def get_diff_tagging(self): op_modifies=True, params=image_params ) - self.log(str(response)) + self.log("Received API response from 'tag_as_golden_image': {0}".format(str(response)), "DEBUG") else: image_params = dict( @@ -982,7 +996,7 @@ def get_diff_tagging(self): device_family_identifier=self.have.get("device_family_identifier"), device_role=tagging_details.get("device_role") ) - self.log("Image params for un-tagging image as golden:" + str(image_params)) + self.log("Image params for tagging image as golden: {0}".format(str(image_params)), "INFO") response = self.dnac._exec( family="software_image_management_swim", @@ -990,7 +1004,7 @@ def get_diff_tagging(self): op_modifies=True, params=image_params ) - self.log(str(response)) + self.log("Received API response from 'remove_golden_tag_for_image': {0}".format(str(response)), "DEBUG") if response: task_details = {} @@ -1007,16 +1021,16 @@ def get_diff_tagging(self): def get_device_ip_from_id(self, device_id): """ - Retrieve the management IP address of a device from Cisco DNA Center using its ID. - Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. - - device_id (str): The unique identifier of the device in Cisco DNA Center. + Retrieve the management IP address of a device from Cisco Catalyst Center using its ID. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - device_id (str): The unique identifier of the device in Cisco Catalyst Center. Returns: str: The management IP address of the specified device. Raises: - Exception: If there is an error while retrieving the response from Cisco DNA Center. + Exception: If there is an error while retrieving the response from Cisco Catalyst Center. Description: - This method queries Cisco DNA Center for the device details based on its unique identifier (ID). + This method queries Cisco Catalyst Center for the device details based on its unique identifier (ID). It uses the 'get_device_list' function in the 'devices' family, extracts the management IP address from the response, and returns it. If any error occurs during the process, an exception is raised with an appropriate error message logged. @@ -1028,12 +1042,13 @@ def get_device_ip_from_id(self, device_id): function='get_device_list', params={"id": device_id} ) + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") response = response.get('response')[0] device_ip = response.get("managementIpAddress") return device_ip except Exception as e: - error_message = "Error while getting the response of device from Cisco DNA Center - {0}".format(str(e)) + error_message = "Error while getting the response of device from Cisco Catalyst Center - {0}".format(str(e)) self.log(error_message, "ERROR") raise Exception(error_message) @@ -1041,9 +1056,9 @@ def get_diff_distribution(self): """ Get image distribution parameters from the playbook and trigger image distribution. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: This function retrieves image distribution parameters from the playbook's 'distribution_details' and triggers the distribution of the specified software image to the specified device. It monitors the distribution task's @@ -1064,7 +1079,7 @@ def get_diff_distribution(self): imageUuid=image_id )] ) - self.log("Distribution Params: " + str(distribution_params)) + self.log("Distribution Params: {0}".format(str(distribution_params)), "INFO") response = self.dnac._exec( family="software_image_management_swim", @@ -1072,6 +1087,8 @@ def get_diff_distribution(self): op_modifies=True, params=distribution_params, ) + self.log("Received API response from 'trigger_software_image_distribution': {0}".format(str(response)), "DEBUG") + if response: task_details = {} task_id = response.get("response").get("taskId") @@ -1089,7 +1106,7 @@ def get_diff_distribution(self): if task_details.get("isError"): self.status = "failed" self.msg = "Image with Id {0} Distribution Failed".format(image_id) - self.log(self.msg) + self.log(self.msg, "WARNING") self.result['response'] = task_details break @@ -1099,12 +1116,13 @@ def get_diff_distribution(self): if len(device_uuid_list) == 0: self.status = "failed" - msg = "No devices found for Image Distribution" - self.result['msg'] = msg - self.log(msg) + self.msg = "No devices found for Image Distribution" + self.result['msg'] = self.msg + self.log(self.msg, "WARNING") return self - self.log("List of device UUID's for Image Distribution " + str(device_uuid_list)) + self.log("List of device UUID's for Image Distribution: {0}".format(str(device_uuid_list)), "INFO") + device_distribution_count = 0 device_ips_list = [] for device_uuid in device_uuid_list: @@ -1115,13 +1133,15 @@ def get_diff_distribution(self): imageUuid=image_id )] ) - self.log("Distribution Params: " + str(distribution_params)) + self.log("Distribution Params: {0}".format(str(distribution_params)), "INFO") response = self.dnac._exec( family="software_image_management_swim", function='trigger_software_image_distribution', op_modifies=True, params=distribution_params, ) + self.log("Received API response from 'trigger_software_image_distribution': {0}".format(str(response)), "DEBUG") + if response: task_details = {} task_id = response.get("response").get("taskId") @@ -1139,7 +1159,7 @@ def get_diff_distribution(self): if task_details.get("isError"): error_msg = "Image with Id {0} Distribution Failed".format(image_id) - self.log(error_msg) + self.log(error_msg, "WARNING") self.result['response'] = task_details device_ips_list.append(device_management_ip) break @@ -1158,7 +1178,7 @@ def get_diff_distribution(self): self.log("For Devices {0} Image Distribution gets Failed".format(str(device_ips_list)), "CRITICAL") self.result['msg'] = self.msg - self.log(self.msg) + self.log(self.msg, "INFO") return self @@ -1166,9 +1186,9 @@ def get_diff_activation(self): """ Get image activation parameters from the playbook and trigger image activation. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: This function retrieves image activation parameters from the playbook's 'activation_details' and triggers the activation of the specified software image on the specified device. It monitors the activation task's progress and @@ -1195,7 +1215,7 @@ def get_diff_activation(self): schedule_validate=activation_details.get("scehdule_validate"), payload=payload ) - self.log("Activation Params: " + str(activation_params)) + self.log("Activation Params: {0}".format(str(activation_params)), "INFO") response = self.dnac._exec( family="software_image_management_swim", @@ -1203,6 +1223,8 @@ def get_diff_activation(self): op_modifies=True, params=activation_params, ) + self.log("Received API response from 'trigger_software_image_activation': {0}".format(str(response)), "DEBUG") + task_details = {} task_id = response.get("response").get("taskId") @@ -1221,6 +1243,7 @@ def get_diff_activation(self): self.status = "failed" self.result['response'] = task_details self.msg = error_msg + self.log(error_msg, "WARNING") return self self.result['response'] = task_details if task_details else response @@ -1229,13 +1252,12 @@ def get_diff_activation(self): if len(device_uuid_list) == 0: self.status = "failed" - msg = "No Devices found for Image Activation" - self.result['msg'] = msg - self.log(msg) + self.msg = "No Devices found for Image Activation" + self.result['msg'] = self.msg + self.log(self.msg, "WARNING") return self - # if len(device_uuid_list) > 0: - self.log("List of device UUID's for Image Activation" + str(device_uuid_list)) + self.log("List of device UUID's for Image Activation: {0}".format(str(device_uuid_list)), "INFO") device_activation_count = 0 device_ips_list = [] @@ -1253,7 +1275,7 @@ def get_diff_activation(self): schedule_validate=activation_details.get("scehdule_validate"), payload=payload ) - self.log("Activation Params: " + str(activation_params)) + self.log("Activation Params: {0}".format(str(activation_params)), "INFO") response = self.dnac._exec( family="software_image_management_swim", @@ -1261,6 +1283,8 @@ def get_diff_activation(self): op_modifies=True, params=activation_params, ) + self.log("Received API response from 'trigger_software_image_activation': {0}".format(str(response)), "DEBUG") + if response: task_details = {} task_id = response.get("response").get("taskId") @@ -1278,6 +1302,7 @@ def get_diff_activation(self): if task_details.get("isError"): error_msg = "Image with Id {0} Activation Failed".format(image_id) + self.log(error_msg, "WARNING") self.result['response'] = task_details device_ips_list.append(device_management_ip) break @@ -1296,7 +1321,7 @@ def get_diff_activation(self): self.log("For Devices {0} Image Activation gets Failed".format(str(device_ips_list)), "CRITICAL") self.result['msg'] = msg - self.log(msg) + self.log(msg, "INFO") return self @@ -1304,7 +1329,7 @@ def get_diff_merged(self, config): """ Get tagging details and then trigger distribution followed by activation if specified in the playbook. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. config (dict): The configuration dictionary containing tagging, distribution, and activation details. Returns: self: The current instance of the class with updated 'result' and 'have' attributes. From 7780d5048af8bc582f8b7fcb0963f7644e95d941 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 26 Jan 2024 10:00:49 +0530 Subject: [PATCH 13/76] removed unused log import --- plugins/modules/site_intent.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 45b6317fa5..8b330d7a87 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -315,7 +315,6 @@ from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, validate_list_of_dicts, - log, get_dict_result, ) From 6c65c5f713e5a632c4ce3f0ff5bc17597478db28 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 26 Jan 2024 22:17:54 +0530 Subject: [PATCH 14/76] Add log level severity in inventory, site and swim module as per coding guidelines, address review comments --- plugins/modules/inventory_intent.py | 696 +++++++++++++++------------- plugins/modules/site_intent.py | 46 +- plugins/modules/swim_intent.py | 38 +- 3 files changed, 417 insertions(+), 363 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 94436c0999..08eafea631 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -25,7 +25,7 @@ Madhan Sankaranarayanan (@madhansansel) options: config_verify: - description: Set to True to verify the Cisco DNA Center config after applying the playbook config. + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool default: False dnac_log_level: @@ -34,7 +34,7 @@ type: str default: INFO state: - description: The state of Cisco DNA Center after module completion. + description: The state of Cisco Catalyst Center after module completion. type: str choices: [ merged, deleted ] default: merged @@ -197,7 +197,7 @@ description: Identifier used to distinguish a specific VLAN that is dedicated to voice traffic. type: int interface_name: - description: Name of Interface in order to update the interface details of device.(Eg GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) + description: Specify the interface name to update the details of the device interface. (For example, GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) deployment_mode: description: Preview/Deploy [Preview means the configuration is not pushed to the device. Deploy makes the configuration pushed to the device] type: str @@ -224,32 +224,32 @@ description: Ip Address allocated to the interface type: str interface_netmask_in_cidr: - description: Ip Address allocated to the interface + description: Interface mask in cidr. type: int interface_gateway: - description: Ip Address allocated to the interface + description: name of the gateway for the interface type: str lag_or_port_number: - description: Ip Address allocated to the interface + description: Lag or port number of interface type: int vlan_id: - description: Ip Address allocated to the interface + description: Vlan Ifd of interface type: int interface_name: - description: Ip Address allocated to the interface + description: Name of the interface. type: str requirements: - dnacentersdk >= 2.5.5 - python >= 3.5 seealso: -- name: Cisco DNA Center documentation for Devices AddDevice2 +- name: Cisco Catalyst Center documentation for Devices AddDevice2 description: Complete reference of the AddDevice2 API. link: https://developer.cisco.com/docs/dna-center/#!add-device -- name: Cisco DNA Center documentation for Devices DeleteDeviceById +- name: Cisco Catalyst Center documentation for Devices DeleteDeviceById description: Complete reference of the DeleteDeviceById API. link: https://developer.cisco.com/docs/dna-center/#!delete-device-by-id -- name: Cisco DNA Center documentation for Devices SyncDevices2 +- name: Cisco Catalyst Center documentation for Devices SyncDevices2 description: Complete reference of the SyncDevices2 API. link: https://developer.cisco.com/docs/dna-center/#!sync-devices notes: @@ -648,7 +648,7 @@ RETURN = r""" dnac_response: - description: A dictionary or list with the response returned by the Cisco DNA Center Python SDK + description: A dictionary or list with the response returned by the Cisco Catalyst Center Python SDK returned: always type: dict sample: > @@ -782,32 +782,31 @@ def validate_input(self): ) if invalid_params: - self.msg = "Invalid parameters in playbook: {0}".format( - "\n".join(invalid_params) - ) + self.msg = "Invalid parameters in playbook: {0}".format(invalid_params) + self.log(self.msg, "ERROR") self.status = "failed" return self self.validated_config = valid_temp - self.log(str(valid_temp)) - self.msg = "Successfully validated input" + self.msg = "Successfully validated playbook configuration parameters using 'validate_input': {0}".format(str(valid_temp)) + self.log(self.msg, "INFO") self.status = "success" return self def device_exists_in_dnac(self): """ - Check which devices already exists in Cisco DNA Center and return both device_exist and device_not_exist in dnac. + Check which devices already exists in Cisco Catalyst Center and return both device_exist and device_not_exist in dnac. Parameters: - self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. Returns: - list: A list of devices that exist in Cisco DNA Center. + list: A list of devices that exist in Cisco Catalyst Center. Description: - Queries Cisco DNA Center to check which devices are already present in Cisco DNA Center and store + Queries Cisco Catalyst Center to check which devices are already present in Cisco Catalyst Center and store its management IP address in the list of devices that exist. Example: To use this method, create an instance of the class and call 'device_exists_in_dnac' on it, - The method returns a list of management IP addressesfor devices that exist in Cisco DNA Center. + The method returns a list of management IP addressesfor devices that exist in Cisco Catalyst Center. """ device_in_dnac = [] @@ -819,12 +818,12 @@ def device_exists_in_dnac(self): ) except Exception as e: - error_message = "Error while fetching device from Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) + error_message = "Error while fetching device from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "CRITICAL") raise Exception(error_message) if response: - self.log(str(response)) + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") response = response.get("response") for ip in response: device_ip = ip["managementIpAddress"] @@ -834,14 +833,14 @@ def device_exists_in_dnac(self): def is_udf_exist(self, field_name): """ - Check if a Global User Defined Field exists in Cisco DNA Center based on its name. + Check if a Global User Defined Field exists in Cisco Catalyst Center based on its name. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. field_name (str): The name of the Global User Defined Field. Returns: bool: True if the Global User Defined Field exists, False otherwise. Description: - The function sends a request to Cisco DNA Center to retrieve all Global User Defined Fields + The function sends a request to Cisco Catalyst Center to retrieve all Global User Defined Fields with the specified name. If matching field is found, the function returns True, indicating that the field exists else returns False. """ @@ -851,27 +850,28 @@ def is_udf_exist(self, field_name): function='get_all_user_defined_fields', params={"name": field_name}, ) - self.log(str(response)) + + self.log("Received API response from 'get_all_user_defined_fields': {0}".format(str(response)), "DEBUG") udf = response.get("response") if (len(udf) == 1): return True - message = "Global User Defined Field with name - {0} doesnot exist in Cisco DNA Center".format(field_name) - self.log(message) + message = "Global User Defined Field with name '{0}' doesnot exist in Cisco Catalyst Center".format(field_name) + self.log(message, "INFO") return False def create_user_defined_field(self): """ - Create a Global User Defined Field in Cisco DNA Center based on the provided configuration. + Create a Global User Defined Field in Cisco Catalyst Center based on the provided configuration. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: The function retrieves the configuration for adding a user-defined field from the configuration object, - sends the request to Cisco DNA Center to create the field, and logs the response. + sends the request to Cisco Catalyst Center to create the field, and logs the response. """ try: payload = self.config[0].get('add_user_defined_field') @@ -880,30 +880,30 @@ def create_user_defined_field(self): function='create_user_defined_field', params=payload, ) - self.log(str(response)) + self.log("Received API response from 'create_user_defined_field': {0}".format(str(response)), "DEBUG") response = response.get("response") field_name = self.config[0].get('add_user_defined_field').get('name') - self.log("Global User Defined Field with name - {0} created successfully").format(field_name) + self.log("Global User Defined Field with name '{0}' created successfully".format(field_name), "INFO") self.status = "success" except Exception as e: - error_message = "Error while Creating Global User Defined Field in Cisco DNA Center - {0}".format(str(e)) - log(error_message) + error_message = "Error while creating Global UDF in Cisco Catalyst Center: {0}".format(str(e)) + log(error_message, "ERROR") return self def add_field_to_devices(self, device_ids): """ - Add a Global user-defined field with specified details to a list of devices in Cisco DNA Center. + Add a Global user-defined field with specified details to a list of devices in Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ids (list): A list of device IDs to which the user-defined field will be added. Returns: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: The function retrieves the details of the user-defined field from the configuration object, including the field name and default value then iterates over list of device IDs, creating a payload for - each device and sending the request to Cisco DNA Center to add the user-defined field. + each device and sending the request to Cisco Catalyst Center to add the user-defined field. """ field_details = self.config[0].get('add_user_defined_field') field_name = field_details.get('name') @@ -922,15 +922,15 @@ def add_field_to_devices(self, device_ids): function='add_user_defined_field_to_device', params=udf_param_dict, ) - self.log(str(response)) + self.log("Received API response from 'add_user_defined_field_to_device': {0}".format(str(response)), "DEBUG") response = response.get("response") self.status = "success" self.result['changed'] = True except Exception as e: - error_message = "Error while Adding Global UDF to device in Cisco DNA Center - {0}".format(str(e)) - log(error_message) self.status = "failed" + error_message = "Error while adding Global UDF to device in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") self.result['changed'] = False return self @@ -938,14 +938,14 @@ def add_field_to_devices(self, device_ids): def trigger_export_api(self, payload_params): """ Triggers the export API to generate a CSV file containing device details based on the given payload parameters. - Args: - self (object): An instance of a class used for interacting with Cisco DNA Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. payload_params (dict): A dictionary containing parameters required for the export API. Returns: dict: The response from the export API, including information about the task and file ID. If the export is successful, the CSV file can be downloaded using the file ID. Description: - The function initiates the export API in Cisco DNA Center to generate a CSV file containing detailed information + The function initiates the export API in Cisco Catalyst Center to generate a CSV file containing detailed information about devices.The response from the API includes task details and a file ID. """ @@ -955,7 +955,7 @@ def trigger_export_api(self, payload_params): op_modifies=True, params=payload_params, ) - self.log(str(response)) + self.log("Received API response from 'export_device_list': {0}".format(str(response)), "DEBUG") response = response.get("response") task_id = response.get("taskId") @@ -972,7 +972,8 @@ def trigger_export_api(self, payload_params): self.msg = "Could not get the File ID because of {0} so can't export device details in csv file".format(failure_reason) else: self.msg = "Could not get the File ID so can't export device details in csv file" - self.log(self.msg) + self.log(self.msg, "ERROR") + return response # With this File ID call the Download File by FileID API and process the response @@ -982,13 +983,14 @@ def trigger_export_api(self, payload_params): op_modifies=True, params={"file_id": file_id}, ) + self.log("Received API response from 'download_a_file_by_fileid': {0}".format(str(response)), "DEBUG") return response def decrypt_and_read_csv(self, response, password): """ - Args: - self (object): An instance of a class used for interacting with Cisco DNA Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. response (requests.Response): HTTP response object containing the encrypted CSV file. password (str): Password used for decrypting the CSV file. Returns: @@ -1001,7 +1003,7 @@ def decrypt_and_read_csv(self, response, password): if not HAS_PYZIPPER: self.msg = "pyzipper is required for this module. Install pyzipper to use this functionality." - self.log(self.msg) + self.log(self.msg, "CRITICAL") self.status = "failed" return self @@ -1014,11 +1016,11 @@ def decrypt_and_read_csv(self, response, password): try: encryption_method = encryption_dict.get(snmp_protocol) except Exception as e: - self.log("Given SNMP protcol {0} not present".format(snmp_protocol)) + self.log("Given SNMP protcol '{0}' not present".format(snmp_protocol), "WARNING") if not encryption_method: - self.msg = "Invalid SNMP protocol {0} specified for encryption.".format(snmp_protocol) - self.log(self.msg) + self.msg = "Invalid SNMP protocol '{0}' specified for encryption.".format(snmp_protocol) + self.log(self.msg, "ERROR") self.status = "failed" return self @@ -1035,7 +1037,7 @@ def decrypt_and_read_csv(self, response, password): file_content_text = file_content_binary.decode('utf-8') # Now 'file_content_text' contains the text content of the decrypted file - self.log(file_content_text) + self.log("Text content of decrypted file: {0}".format(file_content_text), "DEBUG") # Parse the CSV-like string into a list of dictionaries csv_reader = csv.DictReader(StringIO(file_content_text)) @@ -1044,13 +1046,13 @@ def decrypt_and_read_csv(self, response, password): def export_device_details(self): """ - Export device details from Cisco DNA Center into a CSV file. + Export device details from Cisco Catalyst Center into a CSV file. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: self (object): An instance of the class with updated result, status, and log. Description: - This function exports device details from Cisco DNA Center based on the provided IP addresses in the configuration. + This function exports device details from Cisco Catalyst Center based on the provided IP addresses in the configuration. It retrieves the device UUIDs, calls the export device list API, and downloads the exported data of both device details and and device credentials with an encrtypted zip file with password into CSV format. The CSV data is then parsed and written to a file. @@ -1059,10 +1061,9 @@ def export_device_details(self): device_ips = self.config[0].get("ip_address", []) if not device_ips: - msg = "No Devices are given in the playbook so can't export device details" self.status = "failed" - self.msg = msg - self.log(msg) + self.msg = "No Devices are given in the playbook so can't export device details" + self.log(self.msg, "ERROR") return self try: @@ -1072,7 +1073,7 @@ def export_device_details(self): self.status = "failed" self.result['changed'] = False self.msg = "Could not find device UUIDs for exporting device details" - self.log(self.msg) + self.log(self.msg, "ERROR") return self # Now all device UUID get collected so call the export device list API @@ -1085,7 +1086,7 @@ def export_device_details(self): one uppercase letter, one digit and one special characters from -=\\;,./~!@#$%^&*()_+{}[]|:?""" formatted_msg = ' '.join(line.strip() for line in detailed_msg.splitlines()) self.msg = formatted_msg - self.log(formatted_msg) + self.log(formatted_msg, "INFO") return self payload_params = { @@ -1104,11 +1105,11 @@ def export_device_details(self): csv_reader = self.decrypt_and_read_csv(response, password) self.check_return_status() else: - encoded_resp = response.data.decode(encoding='utf-8') - self.log(str(encoded_resp)) + decoded_resp = response.data.decode(encoding='utf-8') + self.log("Decoded response of Export Device Credential file: {0}".format(str(decoded_resp)), "DEBUG") # Parse the CSV-like string into a list of dictionaries - csv_reader = csv.DictReader(StringIO(encoded_resp)) + csv_reader = csv.DictReader(StringIO(decoded_resp)) current_date = datetime.now() formatted_date = current_date.strftime("%m-%d-%Y") output_file_name = "devices-" + str(formatted_date) + ".csv" @@ -1124,27 +1125,27 @@ def export_device_details(self): csv_writer.writeheader() csv_writer.writerows(device_data) - self.msg = "Device Details Exported Successfully to the CSV file - {0}".format(output_file_name) - self.log(self.msg) + self.msg = "Device Details Exported Successfully to the CSV file: {0}".format(output_file_name) + self.log(self.msg, "INFO") self.status = "success" self.result['changed'] = True except Exception as e: - self.msg = "Cannot Export the Device Details into CSV file for {0}".format(str(device_ips)) - self.log(self.msg) + self.msg = "Error while exporting device details into CSV file for device(s): '{0}'".format(str(device_ips)) + self.log(self.msg, "ERROR") self.status = "failed" return self def get_ap_devices(self, device_ips): """ - Args: - self (object): An instance of a class used for interacting with Cisco DNA Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ip (str): The management IP address of the device for which the response is to be retrieved. Returns: - list: A list containing Access Point device IP's obtained from the Cisco DNA Center. + list: A list containing Access Point device IP's obtained from the Cisco Catalyst Center. Description: - This method communicates with Cisco DNA Center to retrieve the details of a device with the specified + This method communicates with Cisco Catalyst Center to retrieve the details of a device with the specified management IP address and check if device family matched to Unified AP. It executes the 'get_device_list' API call with the provided device IP address, logs the response, and returns list containing ap device ips. """ @@ -1162,20 +1163,20 @@ def get_ap_devices(self, device_ips): if response and response[0].get('family', '') == "Unified AP": ap_device_list.append(device_ip) except Exception as e: - error_message = "Error while getting the response of device from Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) + error_message = "Error while getting the response of device from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "CRITICAL") raise Exception(error_message) return ap_device_list def resync_devices(self): """ - Resync devices in Cisco DNA Center. + Resync devices in Cisco Catalyst Center. This function performs the Resync operation for the devices specified in the playbook. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: The function expects the following parameters in the configuration: - "ip_address": List of device IP addresses to be resynced. @@ -1192,19 +1193,19 @@ def resync_devices(self): input_device_ips.remove(device_ip) ap_devices = self.get_ap_devices(input_device_ips) - self.log("AP Devices from the playbook input are: {0}".format(str(ap_devices))) + self.log("AP Devices from the playbook input are: {0}".format(str(ap_devices)), "INFO") if ap_devices: for ap_ip in ap_devices: input_device_ips.remove(ap_ip) - self.log("Following devices {0} are AP, so can't perform resync operation.".format(str(ap_devices))) + self.log("Following devices {0} are AP, so can't perform resync operation.".format(str(ap_devices)), "WARNING") if not input_device_ips: self.msg = "Cannot perform the Resync operation as the device(s) with IP(s) {0} are not present in Cisco Catalyst Center".format(str(device_ips)) self.status = "success" self.result['changed'] = False self.result['response'] = self.msg - self.log(self.msg, "INFO") + self.log(self.msg, "WARNING") return self device_ids = self.get_device_ids(input_device_ips) @@ -1220,7 +1221,7 @@ def resync_devices(self): op_modifies=True, params=resync_param_dict, ) - self.log(str(response)) + self.log("Received API response from 'sync_devices_using_forcesync': {0}".format(str(response)), "DEBUG") if response and isinstance(response, dict): task_id = response.get('response').get('taskId') @@ -1232,8 +1233,8 @@ def resync_devices(self): self.status = "success" self.result['changed'] = True self.result['response'] = execution_details - self.log("Device Resynced Successfully and Resynced devices are :" + str(input_device_ips)) - self.msg = "Device " + str(input_device_ips) + " Resynced Successfully !!" + self.msg = "Device Resynced Successfully and Resynced devices are: {0}".format(str(input_device_ips)) + self.log(self.msg, "INFO") break elif execution_details.get("isError"): self.status = "failed" @@ -1242,25 +1243,25 @@ def resync_devices(self): self.msg = "Device Resynced get failed because of {0}".format(failure_reason) else: self.msg = "Device Resynced get failed." - self.log(self.msg) + self.log(self.msg, "ERROR") break except Exception as e: - error_message = "Error while Resyncing device in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) - raise Exception(error_message) + self.status = "failed" + error_message = "Error while resyncing device in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") return self def reboot_access_points(self): """ - Reboot access points in Cisco DNA Center. + Reboot access points in Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: self (object): An instance of the class with updated result, status, and log. Description: - This function performs a reboot operation on access points in Cisco DNA Center based on the provided IP addresses + This function performs a reboot operation on access points in Cisco Catalyst Center based on the provided IP addresses in the configuration. It retrieves the AP devices' MAC addresses, calls the reboot access points API, and monitors the progress of the reboot operation. """ @@ -1270,7 +1271,7 @@ def reboot_access_points(self): if input_device_ips: ap_devices = self.get_ap_devices(input_device_ips) - self.log("AP Devices from the playbook input are : {0}".format(str(ap_devices))) + self.log("AP Devices from the playbook input are: {0}".format(str(ap_devices)), "INFO") for device_ip in input_device_ips: if device_ip not in ap_devices: input_device_ips.remove(device_ip) @@ -1280,7 +1281,7 @@ def reboot_access_points(self): self.status = "success" self.result['changed'] = False self.result['response'] = self.msg - self.log(self.msg) + self.log(self.msg, "WARNING") return self # Get and store the apEthernetMacAddress of given devices @@ -1306,7 +1307,7 @@ def reboot_access_points(self): self.result['changed'] = False self.msg = "Cannot find the AP devices for rebooting" self.result['response'] = self.msg - self.log(self.msg) + self.log(self.msg, "INFO") return self # Now call the Reboot Access Point API @@ -1331,8 +1332,8 @@ def reboot_access_points(self): self.status = "success" self.result['changed'] = True self.result['response'] = execution_details - self.log("AP Devices rebooted successfully. Rebooted devices: {0}".format(str(input_device_ips)), "INFO") self.msg = "AP Device(s) {0} successfully rebooted!".format(str(input_device_ips)) + self.log(self.msg, "INFO") break elif execution_details.get("isError"): self.status = "failed" @@ -1341,6 +1342,7 @@ def reboot_access_points(self): self.msg = "AP Device Rebooting get failed because of {0}".format(failure_reason) else: self.msg = "AP Device Rebooting get failed" + self.log(self.msg, "ERROR") break return self @@ -1349,7 +1351,7 @@ def handle_successful_provisioning(self, device_ip, execution_details, device_ty """ Handle successful provisioning of Wired/Wireless device. Parameters: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - device_ip (str): The IP address of the provisioned device. - execution_details (str): Details of the provisioning execution. - device_type (str): The type or category of the provisioned device(Wired/Wireless). @@ -1362,13 +1364,13 @@ def handle_successful_provisioning(self, device_ip, execution_details, device_ty self.status = "success" self.result['changed'] = True self.result['response'] = execution_details - self.log("{0} Device {1} provisioned successfully!!".format(device_type, device_ip)) + self.log("{0} Device {1} provisioned successfully!!".format(device_type, device_ip), "INFO") def handle_failed_provisioning(self, device_ip, execution_details, device_type): """ Handle failed provisioning of Wired/Wireless device. Parameters: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - device_ip (str): The IP address of the device that failed provisioning. - execution_details (dict): Details of the failed provisioning execution in key "failureReason" indicating reason for failure. - device_type (str): The type or category of the provisioned device(Wired/Wireless). @@ -1381,13 +1383,13 @@ def handle_failed_provisioning(self, device_ip, execution_details, device_type): self.status = "failed" failure_reason = execution_details.get("failureReason", "Unknown failure reason") self.msg = "{0} Device Provisioning failed for {1} because of {2}".format(device_type, device_ip, failure_reason) - self.log(self.msg) + self.log(self.msg, "WARNING") def handle_provisioning_exception(self, device_ip, exception, device_type): """ Handle an exception during the provisioning process of Wired/Wireless device.. Parameters: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - device_ip (str): The IP address of the device involved in provisioning. - exception (Exception): The exception raised during provisioning. - device_type (str): The type or category of the provisioned device(Wired/Wireless). @@ -1397,14 +1399,14 @@ def handle_provisioning_exception(self, device_ip, exception, device_type): This method logs an error message indicating an exception occurred during the provisioning process for a device. """ - error_message = "Error while Provisioning the {0} device {1} in Cisco DNA Center - {2}".format(device_type, device_ip, str(exception)) - self.log(error_message) + error_message = "Error while Provisioning the {0} device {1} in Cisco Catalyst Center: {2}".format(device_type, device_ip, str(exception)) + self.log(error_message, "ERROR") def handle_all_already_provisioned(self, device_ips, device_type): """ Handle successful provisioning for all devices(Wired/Wireless). Parameters: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - device_type (str): The type or category of the provisioned device(Wired/Wireless). Return: None @@ -1413,8 +1415,8 @@ def handle_all_already_provisioned(self, device_ips, device_type): """ self.status = "success" - self.msg = "All the {0} Devices - {1} given in the playbook are already Provisioned".format(device_type, str(device_ips)) - self.log(self.msg) + self.msg = "All the {0} Devices '{1}' given in the playbook are already Provisioned".format(device_type, str(device_ips)) + self.log(self.msg, "INFO") self.result['response'] = self.msg self.result['changed'] = False @@ -1422,7 +1424,7 @@ def handle_all_provisioned(self, device_type): """ Handle successful provisioning for all devices(Wired/Wireless). Parameters: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - device_type (str): The type or category of the provisioned devices(Wired/Wireless). Return: None @@ -1432,13 +1434,13 @@ def handle_all_provisioned(self, device_type): self.status = "success" self.result['changed'] = True - self.log("All {0} Devices provisioned successfully!!".format(device_type)) + self.log("All {0} Devices provisioned successfully!!".format(device_type), "INFO") def handle_all_failed_provision(self, device_type): """ Handle failure of provisioning for all devices(Wired/Wireless). Parameters: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - device_type (str): The type or category of the devices(Wired/Wireless). Return: None @@ -1449,13 +1451,13 @@ def handle_all_failed_provision(self, device_type): self.status = "failed" self.msg = "{0} Device Provisioning failed for all devices".format(device_type) - self.log(self.msg) + self.log(self.msg, "INFO") def handle_partially_provisioned(self, provision_count, device_type): """ Handle partial success in provisioning for devices(Wired/Wireless). Parameters: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - provision_count (int): The count of devices that were successfully provisioned. - device_type (str): The type or category of the provisioned devices(Wired/Wireless). Return: @@ -1467,17 +1469,17 @@ def handle_partially_provisioned(self, provision_count, device_type): self.status = "success" self.result['changed'] = True - self.log("{0} Devices provisioned successfully partially for {1} devices".format(device_type, provision_count)) + self.log("{0} Devices provisioned successfully partially for {1} devices".format(device_type, provision_count), "INFO") def provisioned_wired_device(self): """ - Provision wired devices in Cisco DNA Center. + Provision wired devices in Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: self (object): An instance of the class with updated result, status, and log. Description: - This function provisions wired devices in Cisco DNA Center based on the configuration provided. + This function provisions wired devices in Cisco Catalyst Center based on the configuration provided. It retrieves the site name and IP addresses of the devices from the configuration, attempts to provision each device, and monitors the provisioning process. """ @@ -1497,7 +1499,7 @@ def provisioned_wired_device(self): if not site_name and not input_device_ips: self.status = "failed" self.msg = "Site/Devices are required for Provisioning of Wired Devices." - self.log(self.msg) + self.log(self.msg, "ERROR") self.result['response'] = self.msg return self @@ -1514,7 +1516,7 @@ def provisioned_wired_device(self): # Check till device comes into managed state while True: response = self.get_device_response(device_ip) - self.log("Device is in {0} state waiting for Managed State.".format(response['managementState'])) + self.log("Device is in {0} state waiting for Managed State.".format(response['managementState']), "DEBUG") if ( response.get('managementState') == "Managed" @@ -1529,7 +1531,7 @@ def provisioned_wired_device(self): if not managed_flag: self.log("Device {0} is not transitioning to the managed state, so provisioning operation cannot be performed." - .format(device_ip), 'warning') + .format(device_ip), "WARNING") continue response = self.dnac._exec( @@ -1565,7 +1567,7 @@ def provisioned_wired_device(self): # but for others it gets provision successfully or If some devices are already provsioned self.handle_provisioning_exception(device_ip, e, device_type) if "already provisioned" in str(e): - self.log(str(e)) + self.log(str(e), "INFO") already_provision_count += 1 # Check If all the devices are already provsioned, return from here only @@ -1584,14 +1586,14 @@ def get_wireless_param(self, device_ip): """ Get wireless provisioning parameters for a device. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ip (str): The IP address of the device for which to retrieve wireless provisioning parameters. Returns: wireless_param (list of dict): A list containing a dictionary with wireless provisioning parameters. Description: This function constructs a list containing a dictionary with wireless provisioning parameters based on the configuration provided in the playbook. It validates the managed AP locations, ensuring they are of type "floor." - The function then queries Cisco DNA Center to get network device details using the provided device IP. + The function then queries Cisco Catalyst Center to get network device details using the provided device IP. If the device is not found, the function returns the class instance with appropriate status and log messages and returns the wireless provisioning parameters containing site information, managed AP locations, dynamic interfaces, and device name. @@ -1609,7 +1611,7 @@ def get_wireless_param(self, device_ip): if self.get_site_type(site_name=ap_loc) != "floor": self.status = "failed" self.msg = "Managed AP Location must be a floor" - self.log(self.msg) + self.log(self.msg, "ERROR") return self wireless_param[0]["dynamicInterfaces"] = [] @@ -1632,28 +1634,28 @@ def get_wireless_param(self, device_ip): ) if not response: self.status = "failed" - self.msg = "Device Host name is not present in the Cisco DNA Center" - self.log(self.msg) + self.msg = "Device Host name is not present in the Cisco Catalyst Center" + self.log(self.msg, "INFO") return self response = response.get("response") wireless_param[0]["deviceName"] = response.get("hostname") self.wireless_param = wireless_param self.status = "success" - self.log("Successfully collected all parameters required for Wireless Provisioing") + self.log("Successfully collected all parameters required for Wireless Provisioing", "DEBUG") return self def get_site_type(self, site_name): """ - Get the type of a site in Cisco DNA Center. + Get the type of a site in Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. site_name (str): The name of the site for which to retrieve the type. Returns: site_type (str or None): The type of the specified site, or None if the site is not found. Description: - This function queries Cisco DNA Center to retrieve the type of a specified site. It uses the + This function queries Cisco Catalyst Center to retrieve the type of a specified site. It uses the get_site API with the provided site name, extracts the site type from the response, and returns it. If the specified site is not found, the function returns None, and an appropriate log message is generated. """ @@ -1667,11 +1669,11 @@ def get_site_type(self, site_name): ) if not response: - self.msg = "Site - {0} not found".format(site_name) - self.log(self.msg) + self.msg = "Site '{0}' not found".format(site_name) + self.log(self.msg, "INFO") return site_type - self.log(str(response)) + self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") site = response.get("response") site_additional_info = site[0].get("additionalInfo") @@ -1679,23 +1681,25 @@ def get_site_type(self, site_name): if item["nameSpace"] == "Location": site_type = item.get("attributes").get("type") - except Exception: + except Exception as e: + self.msg = "Error while fetching the site '{0}'.".format(site_name) + self.log(self.msg, "ERROR") self.module.fail_json(msg="Site not found", response=[]) return site_type def provisioned_wireless_devices(self, device_ips): """ - Provision Wireless devices in Cisco DNA Center. + Provision Wireless devices in Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ips (list): List of IP addresses of the devices to be provisioned. Returns: self (object): An instance of the class with updated result, status, and log. Description: This function performs wireless provisioning for the provided list of device IP addresses. It iterates through each device, retrieves provisioning parameters using the get_wireless_param function, - and then calls the Cisco DNA Center API for wireless provisioning. If all devices are already provisioned, + and then calls the Cisco Catalyst Center API for wireless provisioning. If all devices are already provisioned, it returns success with a relevant message. """ @@ -1721,7 +1725,7 @@ def provisioned_wireless_devices(self, device_ips): # Check till device comes into managed state while True: response = self.get_device_response(device_ip) - self.log("Device is in {0} state waiting for Managed State.".format(response['managementState'])) + self.log("Device is in {0} state waiting for Managed State.".format(response['managementState']), "DEBUG") if ( response.get('managementState') == "Managed" @@ -1737,7 +1741,7 @@ def provisioned_wireless_devices(self, device_ips): if not managed_flag: self.log("Device {0} is not transitioning to the managed state, so provisioning operation cannot be performed." - .format(device_ip), 'warning') + .format(device_ip), 'WARNING') continue # Now we have provisioning_param so we can do wireless provisioning @@ -1751,7 +1755,7 @@ def provisioned_wireless_devices(self, device_ips): if response.get("status") == "failed": description = response.get("description") error_msg = "Cannot do Provisioning for Wireless device {0} beacuse of {1}".format(device_ip, description) - self.log(error_msg) + self.log(error_msg, "ERROR") continue task_id = response.get("taskId") @@ -1773,7 +1777,8 @@ def provisioned_wireless_devices(self, device_ips): # but for others it gets provision successfully or If some devices are already provsioned self.handle_provisioning_exception(device_ip, e, device_type) if "already provisioned" in str(e): - self.log(str(e)) + self.msg = "Device '{0}' already provisioned".format(device_ip) + self.log(self.msg, "INFO") already_provision_count += 1 # Check If all the devices are already provsioned, return from here only @@ -1790,14 +1795,14 @@ def provisioned_wireless_devices(self, device_ips): def get_udf_id(self, field_name): """ - Get the ID of a Global User Defined Field in Cisco DNA Center based on its name. + Get the ID of a Global User Defined Field in Cisco Catalyst Center based on its name. Parameters: - self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. field_name (str): The name of the Global User Defined Field. Returns: str: The ID of the Global User Defined Field. Description: - The function sends a request to Cisco DNA Center to retrieve all Global User Defined Fields + The function sends a request to Cisco Catalyst Center to retrieve all Global User Defined Fields with the specified name and extracts the ID of the first matching field.If successful, it returns the ID else returns None. """ @@ -1808,25 +1813,25 @@ def get_udf_id(self, field_name): function='get_all_user_defined_fields', params={"name": field_name}, ) - self.log(str(response)) + self.log("Received API response from 'get_all_user_defined_fields': {0}".format(str(response)), "DEBUG") udf = response.get("response") udf_id = udf[0].get("id") except Exception as e: - error_message = "Cannot get the Id of Global UDF - from Cisco DNA Center - {0}".format(str(e)) - log(error_message) + error_message = "Exception occurred while getting Global UDF ID from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") return udf_id def mandatory_parameter(self): """ - Check for and validate mandatory parameters for adding network devices in Cisco DNA Center. + Check for and validate mandatory parameters for adding network devices in Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. Returns: dict: The input `config` dictionary if all mandatory parameters are present. Description: - It will check the mandatory parameters for adding the devices in Cisco DNA Center. + It will check the mandatory parameters for adding the devices in Cisco Catalyst Center. """ device_type = self.config[0].get("type", "NETWORK_DEVICE") @@ -1846,35 +1851,37 @@ def mandatory_parameter(self): mandatory_params_absent.append(param) if mandatory_params_absent: - self.msg = "Mandatory paramters {0} not present".format(mandatory_params_absent) - self.result['msg'] = "Required parameters {0} for adding devices are not present".format(mandatory_params_absent) self.status = "failed" + self.msg = "Required parameters {0} for adding devices are not present".format(str(mandatory_params_absent)) + self.result['msg'] = self.msg + self.log(self.msg, "ERROR") else: - self.msg = "Required parameter for Adding the devices in Inventory are present." self.status = "success" + self.msg = "Required parameter for Adding the devices in Inventory are present." + self.log(self.msg, "INFO") return self def get_have(self, config): """ - Retrieve and check device information with Cisco DNA Center to determine if devices already exist. + Retrieve and check device information with Cisco Catalyst Center to determine if devices already exist. Parameters: - self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. config (dict): A dictionary containing the configuration details of devices to be checked. Returns: dict: A dictionary containing information about the devices in the playbook, devices that exist in - Cisco DNA Center, and devices that are not present in Cisco DNA Center. + Cisco Catalyst Center, and devices that are not present in Cisco Catalyst Center. Description: - This function checks the specified devices in the playbook against the devices existing in Cisco DNA Center with following keys: + This function checks the specified devices in the playbook against the devices existing in Cisco Catalyst Center with following keys: - "want_device": A list of devices specified in the playbook. - - "device_in_dnac": A list of devices that already exist in Cisco DNA Center. - - "device_not_in_dnac": A list of devices that are not present in Cisco DNA Center. + - "device_in_dnac": A list of devices that already exist in Cisco Catalyst Center. + - "device_not_in_dnac": A list of devices that are not present in Cisco Catalyst Center. """ have = {} want_device = config.get("ip_address") - # Get the list of device that are present in Cisco DNA Center + # Get the list of device that are present in Cisco Catalyst Center device_in_dnac = self.device_exists_in_dnac() device_not_in_dnac = [] @@ -1882,25 +1889,26 @@ def get_have(self, config): if ip not in device_in_dnac: device_not_in_dnac.append(ip) - self.log("Device Exists in Cisco DNA Center : " + str(device_in_dnac)) + self.log("Device(s) {0} exists in Cisco Catalyst Center".format(str(device_in_dnac)), "INFO") have["want_device"] = want_device have["device_in_dnac"] = device_in_dnac have["device_not_in_dnac"] = device_not_in_dnac self.have = have + self.log("Current State (have): {0}".format(str(self.have)), "INFO") return self def get_device_params(self, params): """ - Extract and store device parameters from the playbook for device processing in Cisco DNA Center. + Extract and store device parameters from the playbook for device processing in Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. params (dict): A dictionary containing device parameters retrieved from the playbook. Returns: dict: A dictionary containing the extracted device parameters. Description: - This function will extract and store parameters in dictionary for adding, updating, editing, or deleting devices Cisco DNA Center. + This function will extract and store parameters in dictionary for adding, updating, editing, or deleting devices Cisco Catalyst Center. """ device_param = { @@ -1948,15 +1956,15 @@ def get_device_params(self, params): def get_device_ids(self, device_ips): """ - Get the list of unique device IDs for list of specified management IP addresses of devices in Cisco DNA Center. + Get the list of unique device IDs for list of specified management IP addresses of devices in Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ips (list): The management IP addresses of devices for which you want to retrieve the device IDs. Returns: list: The list of unique device IDs for the specified devices. Description: - Queries Cisco DNA Center to retrieve the unique device ID associated with a device having the specified - IP address. If the device is not found in Cisco DNA Center, it raises an exception. + Queries Cisco Catalyst Center to retrieve the unique device ID associated with a device having the specified + IP address. If the device is not found in Cisco Catalyst Center, it raises an exception. """ device_ids = [] @@ -1970,7 +1978,7 @@ def get_device_ids(self, device_ips): ) if response: - self.log(str(response)) + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") response = response.get("response") if not response: continue @@ -1978,24 +1986,24 @@ def get_device_ids(self, device_ips): device_ids.append(device_id) except Exception as e: - error_message = "Error while fetching device from Cisco DNA Center - {0}".format(str(e)) - log(error_message) + error_message = "Error while fetching device '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)) + self.log(error_message, "ERROR") raise Exception(error_message) return device_ids def get_interface_from_id_and_name(self, device_id, interface_name): """ - Get the interface ID for a device in Cisco DNA Center based on its IP address. + Retrieve the interface ID for a device in Cisco DNA Center based on device id and interface name. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_id (str): The id of the device. - interface_name(str): name of the interface for which details need to be collected. + interface_name (str): Name of the interface for which details need to be collected. Returns: - str: The interface ID for the specified device. + str: The interface ID for the specified device and interface name. Description: - The function sends a request to Cisco DNA Center to retrieve the interface information - for the device with the provided IP address and extracts the interface ID from the + The function sends a request to Cisco Catalyst Center to retrieve the interface information + for the device with the provided device id and interface name and extracts the interface ID from the response, and returns the interface ID. """ @@ -2009,28 +2017,62 @@ def get_interface_from_id_and_name(self, device_id, interface_name): function='get_interface_details', params=interface_detail_params ) - self.log(str(response)) + self.log("Received API response from 'get_interface_details': {0}".format(str(response)), "DEBUG") response = response.get("response") if response: interface_id = response["id"] - self.log("Fetch Interface Id for device successfully !!") + self.log("""Successfully fetched interface ID ({0}) by using device id {1} and interface name {2}.""" + .format(interface_id, device_id, interface_name), "INFO") return interface_id except Exception as e: - error_message = "Error while fetching Interface Id from Cisco DNA Center - {0}".format(str(e)) - log(error_message) + error_message = "Error while fetching interface id for interface({0}) from Cisco Catalyst Center: {1}".format(interface_name, str(e)) + self.log(error_message, "ERROR") raise Exception(error_message) + def get_interface_from_ip(self, device_ip): + """ + Get the interface ID for a device in Cisco DNA Center based on its IP address. + Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. + device_ip (str): The IP address of the device. + Returns: + str: The interface ID for the specified device. + Description: + The function sends a request to Cisco DNA Center to retrieve the interface information + for the device with the provided IP address and extracts the interface ID from the + response, and returns the interface ID. + """ + + try: + response = self.dnac._exec( + family="devices", + function='get_interface_by_ip', + params={"ip_address": device_ip} + ) + self.log("Received API response from 'get_interface_by_ip': {0}".format(str(response)), "DEBUG") + response = response.get("response") + + if response: + interface_id = response[0]["id"] + self.log("Fetch Interface Id for device '{0}' successfully !!".format(device_ip)) + return interface_id + + except Exception as e: + error_message = "Error while fetching Interface Id for device '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)) + log(error_message, "ERROR") + raise Exception(error_message) + def get_device_response(self, device_ip): """ - Args: - self (object): An instance of a class used for interacting with Cisco DNA Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ip (str): The management IP address of the device for which the response is to be retrieved. Returns: - dict: A dictionary containing details of the device obtained from the Cisco DNA Center. + dict: A dictionary containing details of the device obtained from the Cisco Catalyst Center. Description: - This method communicates with Cisco DNA Center to retrieve the details of a device with the specified + This method communicates with Cisco Catalyst Center to retrieve the details of a device with the specified management IP address. It executes the 'get_device_list' API call with the provided device IP address, logs the response, and returns a dictionary containing information about the device. """ @@ -2044,22 +2086,22 @@ def get_device_response(self, device_ip): response = response.get('response')[0] except Exception as e: - error_message = "Error while Getting the response of device from Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) + error_message = "Error while getting the response of device from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") raise Exception(error_message) return response def check_device_role(self, device_ip): """ - Checks if the device role and role source for a device in Cisco DNA Center match the specified values in the configuration. - Args: - self (object): An instance of a class used for interacting with Cisco DNA Center. + Checks if the device role and role source for a device in Cisco Catalyst Center match the specified values in the configuration. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ip (str): The management IP address of the device for which the device role is to be checked. Returns: bool: True if the device role and role source match the specified values, False otherwise. Description: - This method retrieves the device role and role source for a device in Cisco DNA Center using the + This method retrieves the device role and role source for a device in Cisco Catalyst Center using the 'get_device_response' method and compares the retrieved values with specified values in the configuration for updating device roles. """ @@ -2073,14 +2115,14 @@ def check_device_role(self, device_ip): def check_interface_details(self, device_ip, interface_name): """ - Checks if the interface details for a device in Cisco DNA Center match the specified values in the configuration. - Args: - self (object): An instance of a class used for interacting with Cisco DNA Center. + Checks if the interface details for a device in Cisco Catalyst Center match the specified values in the configuration. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ip (str): The management IP address of the device for which interface details are to be checked. Returns: bool: True if the interface details match the specified values, False otherwise. Description: - This method retrieves the interface details for a device in Cisco DNA Center using the 'get_interface_by_ip' API call. + This method retrieves the interface details for a device in Cisco Catalyst Center using the 'get_interface_by_ip' API call. It then compares the retrieved details with the specified values in the configuration for updating interface details. If all specified parameters match the retrieved values or are not provided in the playbook parameters, the function returns True, indicating successful validation. @@ -2088,8 +2130,10 @@ def check_interface_details(self, device_ip, interface_name): device_id = self.get_device_ids([device_ip]) if not device_id: - self.log("Device {0} not present in Cisco Catalyst Center so cannot update interface details".format(device_ip)) + self.log("""Error: Device with IP '{0}' not found in Cisco Catalyst Center.Unable to update interface details.""" + .format(device_ip), "ERROR") return False + interface_detail_params = { 'device_id': device_id[0], 'name': interface_name @@ -2099,10 +2143,11 @@ def check_interface_details(self, device_ip, interface_name): function='get_interface_details', params=interface_detail_params ) - self.log(str(response)) + self.log("Received API response from 'get_interface_details': {0}".format(str(response)), "DEBUG") response = response.get("response") if not response: + self.log("Doesnot get the response of 'get_interface_details' api.", "DEBUG") return False response_params = { @@ -2130,13 +2175,13 @@ def check_interface_details(self, device_ip, interface_name): def check_credential_update(self): """ - Checks if the credentials for devices in the configuration match the updated values in Cisco DNA Center. - Args: - self (object): An instance of a class used for interacting with Cisco DNA Center. + Checks if the credentials for devices in the configuration match the updated values in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Returns: bool: True if the credentials match the updated values, False otherwise. Description: - This method triggers the export API in Cisco DNA Center to obtain the updated credential details for + This method triggers the export API in Cisco Catalyst Center to obtain the updated credential details for the specified devices. It then decrypts and reads the CSV file containing the updated credentials, comparing them with the credentials specified in the configuration. """ @@ -2176,14 +2221,14 @@ def check_credential_update(self): def get_provision_wired_device(self, device_ip): """ - Retrieves the provisioning status of a wired device with the specified management IP address in Cisco DNA Center. - Args: - self (object): An instance of a class used for interacting with Cisco DNA Center. + Retrieves the provisioning status of a wired device with the specified management IP address in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ip (str): The management IP address of the wired device for which provisioning status is to be retrieved. Returns: bool: True if the device is provisioned successfully, False otherwise. Description: - This method communicates with Cisco DNA Center to check the provisioning status of a wired device. + This method communicates with Cisco Catalyst Center to check the provisioning status of a wired device. It executes the 'get_provisioned_wired_device' API call with the provided device IP address and logs the response. """ @@ -2196,7 +2241,7 @@ def get_provision_wired_device(self, device_ip): ) if response.get("status") == "failed": - self.log("Cannot do provisioning for wired device {0} because of {1}.".format(device_ip, response.get('description'))) + self.log("Cannot do provisioning for wired device {0} because of {1}.".format(device_ip, response.get('description')), "ERROR") return False return True @@ -2204,15 +2249,15 @@ def get_provision_wired_device(self, device_ip): def get_want(self, config): """ Get all the device related information from playbook that is needed to be - add/update/delete/resync device in Cisco DNA Center. + add/update/delete/resync device in Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. config (dict): A dictionary containing device-related information from the playbook. Returns: dict: A dictionary containing the extracted device parameters and other relevant information. Description: Retrieve all the device-related information from the playbook needed for adding, updating, deleting, - or resyncing devices in Cisco DNA Center. + or resyncing devices in Cisco Catalyst Center. """ want = {} @@ -2222,20 +2267,21 @@ def get_want(self, config): self.want = want self.msg = "Successfully collected all parameters from the playbook " self.status = "success" + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") return self def get_diff_merged(self, config): """ - Merge and process differences between existing devices and desired device configuration in Cisco DNA Center. + Merge and process differences between existing devices and desired device configuration in Cisco Catalyst Center. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. config (dict): A dictionary containing the desired device configuration and relevant information from the playbook. Returns: object: An instance of the class with updated results and status based on the processing of differences. Description: The function processes the differences and, depending on the changes required, it may add, update, - or resynchronize devices in Cisco DNA Center. + or resynchronize devices in Cisco Catalyst Center. The updated results and status are stored in the class instance for further use. """ @@ -2251,8 +2297,9 @@ def get_diff_merged(self, config): field_name = self.config[0].get('add_user_defined_field').get('name') if field_name is None: - self.msg = "Mandatory paramter for User Define Field 'name' is missing" self.status = "failed" + self.msg = "Error: The mandatory parameter 'name' for the User Defined Field is missing. Please provide the required information." + self.log(self.msg, "ERROR") return self # Check if the Global User defined field exist if not then create it with given field name @@ -2267,8 +2314,9 @@ def get_diff_merged(self, config): device_ids = self.get_device_ids(device_ips) if len(device_ids) == 0: - self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco DNA Center" self.status = "failed" + self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" + self.log(self.msg, "INFO") self.result['changed'] = False return self @@ -2276,8 +2324,8 @@ def get_diff_merged(self, config): self.add_field_to_devices(device_ids).check_return_status() self.result['changed'] = True - self.msg = "Global User Defined Added with name {0} added to device Successfully !".format(field_name) - self.log(self.msg) + self.msg = "Global User Defined Added with name '{0}' added to device Successfully !".format(field_name) + self.log(self.msg, "INFO") config['type'] = device_type if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": @@ -2285,7 +2333,7 @@ def get_diff_merged(self, config): if device_updated: device_to_update = self.config[0].get("ip_address") - # First check if device present in Cisco DNA Center or not + # First check if device present in Cisco Catalyst Center or not device_present = False for device in device_to_update: if device in self.have.get("device_in_dnac"): @@ -2293,11 +2341,11 @@ def get_diff_merged(self, config): break if not device_present: - msg = "Cannot perform Update operation as device: {0} not present in Cisco DNA Center".format(str(device_to_update)) + self.msg = "Cannot perform Update operation as device: {0} not present in Cisco Catalyst Center".format(str(device_to_update)) self.status = "success" self.result['changed'] = False - self.result['response'] = msg - self.log(msg) + self.result['response'] = self.msg + self.log(self.msg, "INFO") return self if credential_update: @@ -2350,8 +2398,7 @@ def get_diff_merged(self, config): op_modifies=True, params=playbook_params, ) - - self.log(str(response)) + self.log("Received API response from 'sync_devices': {0}".format(str(response)), "DEBUG") if response and isinstance(response, dict): task_id = response.get('response').get('taskId') @@ -2363,6 +2410,8 @@ def get_diff_merged(self, config): self.status = "success" self.result['changed'] = True self.result['response'] = execution_details + self.msg = "Device(s) {0} updated successfully".format(str(device_to_update)) + self.log(self.msg, "INFO") break elif execution_details.get("isError"): self.status = "failed" @@ -2371,21 +2420,16 @@ def get_diff_merged(self, config): self.msg = "Device Updation get failed because of {0}".format(failure_reason) else: self.msg = "Device Updation get failed" - self.log(self.msg) + self.log(self.msg, "ERROR") break - self.log("Device Updated Successfully") - self.log("Updated devices are :" + str(device_to_update)) - self.msg = "Device " + str(device_to_update) + " updated Successfully !!" - self.log(self.msg) - except Exception as e: - error_message = "Error while Updating device in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) + error_message = "Error while updating device in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") raise Exception(error_message) - self.msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ip_address']) - self.log(self.msg) + self.msg = "Devices {0} present in Cisco Catalyst Center and updated successfully".format(config['ip_address']) + self.log(self.msg, "INFO") self.status = "success" if self.config[0].get('update_interface_details'): @@ -2421,7 +2465,7 @@ def get_diff_merged(self, config): op_modifies=True, params=update_interface_params, ) - self.log(str(response)) + self.log("Received API response from 'update_interface_details': {0}".format(str(response)), "DEBUG") if response and isinstance(response, dict): task_id = response.get('response').get('taskId') @@ -2433,8 +2477,8 @@ def get_diff_merged(self, config): self.status = "success" self.result['changed'] = True self.result['response'] = execution_details - self.msg = "Update Interface Details for device {0} Added Successfully".format(device_ip) - self.log(self.msg) + self.msg = "Updated Interface Details for device '{0}' successfully".format(device_ip) + self.log(self.msg, "INFO") break elif execution_details.get("isError"): self.status = "failed" @@ -2443,16 +2487,16 @@ def get_diff_merged(self, config): self.msg = "Interface Updation get failed because of {0}".format(failure_reason) else: self.msg = "Interface Updation get failed" - self.log(self.msg) + self.log(self.msg, "ERROR") break except Exception as e: - error_message = "Error while Updating Interface Details in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) + error_message = "Error while updating interface details in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "INFO") self.status = "success" self.result['changed'] = False self.msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" - self.log(self.msg) + self.log(self.msg, "INFO") if self.config[0].get('update_device_role'): for device_ip in device_to_update: @@ -2460,8 +2504,9 @@ def get_diff_merged(self, config): device_role_args = self.config[0].get('update_device_role') if 'role' not in device_role_args or 'role_source' not in device_role_args: - self.msg = "Mandatory paramter(role/sourceRole) to update Device Role are missing" self.status = "failed" + self.msg = "Mandatory paramter(role/sourceRole) to update Device Role are missing" + self.log(self.msg, "WARNING") return self # Check if the same role of device is present in dnac then no need to change the state @@ -2475,8 +2520,8 @@ def get_diff_merged(self, config): if response.get('role') == device_role_args.get('role'): self.status = "success" self.result['changed'] = False - log_msg = "Device Role - {0} same in Cisco DNA Center as well, no updation needed".format(device_role_args.get('role')) - self.log(log_msg) + log_msg = "Device Role '{0}' same in Cisco Catalyst Center as well, no updation needed".format(device_role_args.get('role')) + self.log(log_msg, "INFO") continue device_role_params = { @@ -2492,7 +2537,7 @@ def get_diff_merged(self, config): op_modifies=True, params=device_role_params, ) - self.log(str(response)) + self.log("Received API response from 'update_device_role': {0}".format(str(response)), "DEBUG") if response and isinstance(response, dict): task_id = response.get('response').get('taskId') @@ -2504,22 +2549,22 @@ def get_diff_merged(self, config): self.status = "success" self.result['changed'] = True self.result['response'] = execution_details - self.log("Device Role Updated Successfully") - msg = "Device " + str(device_to_update) + " Role updated Successfully !!" + self.msg = "Device(s) '{0}' role updated successfully".format(str(device_to_update)) + self.log(self.msg, "INFO") break elif execution_details.get("isError"): self.status = "failed" failure_reason = execution_details.get("failureReason") if failure_reason: - self.msg = "Device Role Updation get failed because of {0}".format(failure_reason) + self.msg = "Device role updation get failed because of {0}".format(failure_reason) else: - self.msg = "Device Role Updation get failed" - self.log(self.msg) + self.msg = "Device role updation get failed" + self.log(self.msg, "ERROR") break except Exception as e: - error_message = "Error while Updating device role in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) + error_message = "Error while updating device role in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") raise Exception(error_message) # If we want to add device in inventory @@ -2533,7 +2578,7 @@ def get_diff_merged(self, config): op_modifies=True, params=self.want.get("device_params"), ) - self.log(str(response)) + self.log("Received API response from 'add_device': {0}".format(str(response)), "DEBUG") if response and isinstance(response, dict): task_id = response.get('response').get('taskId') @@ -2547,36 +2592,37 @@ def get_diff_merged(self, config): if len(devices_to_add) > 0: self.result['changed'] = True - log("Device Added Successfully") - log("Added devices are :" + str(devices_to_add)) - msg = "Device " + str(devices_to_add) + " added Successfully !!" - self.result['msg'] = msg + self.msg = "Device(s) '{0}' added to Cisco Catalyst Center".format(str(devices_to_add)) + self.log(self.msg, "INFO") + self.result['msg'] = self.msg break - msg = "Devices " + str(self.config[0].get("ip_address")) + " already present in Cisco DNA Center" - self.result['msg'] = msg + self.msg = "Device(s) '{0}' already present in Cisco Catalyst Center".format(str(self.config[0].get("ip_address"))) + self.log(self.msg, "INFO") + self.result['msg'] = self.msg break elif execution_details.get("isError"): self.status = "failed" failure_reason = execution_details.get("failureReason") if failure_reason: - self.msg = "Device Addition get failed because of {0}".format(failure_reason) + self.msg = "Device addition get failed because of {0}".format(failure_reason) else: - self.msg = "Device Addition get failed" - self.log(self.msg) + self.msg = "Device addition get failed" + self.log(self.msg, "ERROR") self.result['msg'] = self.msg break except Exception as e: - error_message = "Error while Adding device in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) + error_message = "Error while adding device in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") raise Exception(error_message) if self.config[0].get('add_user_defined_field'): field_name = self.config[0].get('add_user_defined_field').get('name') if field_name is None: - self.msg = "Mandatory paramter for User Define Field - name is missing" self.status = "failed" + self.msg = "Mandatory paramter for User Define Field 'name' is missing" + self.log(self.msg, "ERROR") self.result['response'] = self.msg return self @@ -2592,10 +2638,11 @@ def get_diff_merged(self, config): device_ids = self.get_device_ids(device_ips) if not device_ids: - self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco DNA Center" self.status = "failed" + self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" self.result['changed'] = False self.result['response'] = self.msg + self.log(self.msg, "INFO") return self # Now add code for adding Global UDF to device with Id @@ -2603,7 +2650,7 @@ def get_diff_merged(self, config): self.result['changed'] = True self.msg = "Global User Defined Added with name {0} added to device Successfully !".format(field_name) - self.log(self.msg) + self.log(self.msg, "INFO") # Once Wired device get added we will assign device to site and Provisioned it if self.config[0].get('provision_wired_device'): @@ -2627,14 +2674,14 @@ def get_diff_merged(self, config): def get_diff_deleted(self, config): """ - Delete devices in Cisco DNA Center based on device IP Address. + Delete devices in Cisco Catalyst Center based on device IP Address. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center + self (object): An instance of a class used for interacting with Cisco Catalyst Center config (dict): A dictionary containing the list of device IP addresses to be deleted. Returns: object: An instance of the class with updated results and status based on the deletion operation. Description: - This function is responsible for removing devices from the Cisco DNA Center inventory and + This function is responsible for removing devices from the Cisco Catalyst Center inventory and also unprovsioned and removed wired provsion devices from the Inventory page and also delete the Global User Defined Field that are associated to the devices. """ @@ -2647,11 +2694,11 @@ def get_diff_deleted(self, config): udf_id = self.get_udf_id(field_name) if udf_id is None: - msg = "Global UDF - {0} is not present in Cisco DNA Center".format(field_name) - self.msg = msg self.status = "success" + self.msg = "Global UDF '{0}' is not present in Cisco Catalyst Center".format(field_name) + self.log(self.msg, "INFO") self.result['changed'] = False - self.result['msg'] = msg + self.result['msg'] = self.msg return self try: @@ -2661,14 +2708,16 @@ def get_diff_deleted(self, config): params={"id": udf_id}, ) if response and isinstance(response, dict): + self.log("Received API response from 'delete_user_defined_field': {0}".format(str(response)), "DEBUG") task_id = response.get('response').get('taskId') while True: execution_details = self.get_task_details(task_id) if 'success' in execution_details.get("progress"): - self.msg = "Global UDF - {0} Deleted Successfully from Cisco DNA Center".format(field_name) self.status = "success" + self.msg = "Global UDF '{0}' deleted successfully from Cisco Catalyst Center".format(field_name) + self.log(self.msg, "INFO") self.result['changed'] = True self.result['response'] = execution_details break @@ -2676,26 +2725,26 @@ def get_diff_deleted(self, config): self.status = "failed" failure_reason = execution_details.get("failureReason") if failure_reason: - self.msg = "Global UDF Deletion get failed because of {0}".format(failure_reason) + self.msg = "Global UDF deletion get failed because of {0}".format(failure_reason) else: - self.msg = "Global UDF Deletion get failed." - self.log(self.msg) + self.msg = "Global UDF deletion get failed." + self.log(self.msg, "ERROR") break except Exception as e: - error_message = "Error while Deleting Global UDF from Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) + error_message = "Error while deleting Global UDF from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") raise Exception(error_message) return self for device_ip in device_to_delete: if device_ip not in self.have.get("device_in_dnac"): - self.result['changed'] = False - self.msg = "The device {0} is not present in Cisco DNA Center so can't perform delete operation".format(device_ip) self.status = "success" self.result['changed'] = False + self.msg = "Device '{0}' is not present in Cisco Catalyst Center so can't perform delete operation".format(device_ip) self.result['msg'] = self.msg + self.log(self.msg, "INFO") continue try: @@ -2715,17 +2764,18 @@ def get_diff_deleted(self, config): params=provision_params, ) executionid = response.get("executionId") + while True: execution_details = self.get_execution_details(executionid) if execution_details.get("status") == "SUCCESS": self.result['changed'] = True self.msg = execution_details.get("bapiName") - self.log(self.msg) + self.log(self.msg, "INFO") self.result['response'] = self.msg break elif execution_details.get("bapiError"): self.msg = execution_details.get("bapiError") - self.log(self.msg) + self.log(self.msg, "ERROR") break except Exception as e: device_id = self.get_device_ids([device_ip]) @@ -2746,8 +2796,9 @@ def get_diff_deleted(self, config): execution_details = self.get_task_details(task_id) if 'success' in execution_details.get("progress"): - self.msg = "Device Deleted Successfully from Cisco DNA Center" self.status = "success" + self.msg = "Device deleted successfully from Cisco Catalyst Center" + self.log(self.msg, "INFO") self.result['changed'] = True self.result['response'] = execution_details break @@ -2755,10 +2806,10 @@ def get_diff_deleted(self, config): self.status = "failed" failure_reason = execution_details.get("failureReason") if failure_reason: - self.msg = "Device Deletion get failed because of {0}".format(failure_reason) + self.msg = "Device deletion get failed because of {0}".format(failure_reason) else: - self.msg = "Device Deletion get failed." - self.log(self.msg) + self.msg = "Device deletion get failed." + self.log(self.msg, "ERROR") break self.result['msg'] = self.msg @@ -2766,19 +2817,19 @@ def get_diff_deleted(self, config): def verify_diff_merged(self, config): """ - Verify the merged status(Addition/Updation) of Devices in Cisco DNA Center. - Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + Verify the merged status(Addition/Updation) of Devices in Cisco Catalyst Center. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): The configuration details to be verified. Return: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This method checks the merged status of a configuration in Cisco DNA Center by retrieving the current state + This method checks the merged status of a configuration in Cisco Catalyst Center by retrieving the current state (have) and desired state (want) of the configuration, logs the states, and validates whether the specified - site exists in the DNA Center configuration. + site exists in the Catalyst Center configuration. The function performs the following verifications: - - Checks for devices added to Cisco DNA Center and logs the status. + - Checks for devices added to Cisco Catalyst Center and logs the status. - Verifies updated device roles and logs the status. - Verifies updated interface details and logs the status. - Verifies updated device credentials and logs the status. @@ -2787,8 +2838,8 @@ def verify_diff_merged(self, config): """ self.get_have(config) - self.log("Current config in Cisco DNA Center: {0}".format(str(self.have))) - self.log("Input paramter given in Playbook config: {0}".format(str(self.want))) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") devices_to_add = self.have["device_not_in_dnac"] device_added = self.config[0].get("device_added", False) @@ -2800,10 +2851,10 @@ def verify_diff_merged(self, config): if device_added: if not devices_to_add: self.status = "success" - msg = "Requested Devices - {0} Added in Cisco DNA Center and Addition verified.".format(str(device_ips)) - self.log(msg) + msg = "Requested Device(s) '{0}' Added in Cisco Catalyst Center and Addition verified.".format(str(device_ips)) + self.log(msg, "INFO") else: - self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning device addition task not executed successfully.") + self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning device addition task not executed successfully.", "INFO") if device_updated and self.config[0].get('update_interface_details'): interface_update_flag = True @@ -2817,9 +2868,10 @@ def verify_diff_merged(self, config): if interface_update_flag: self.status = "success" msg = "Interface details updated and verified successfully for devices {0}.".format(device_ips) - self.log(msg) + self.log(msg, "INFO") else: - self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning update interface details task not executed successfully.") + self.log("""Playbook parameter does not match with Cisco Catalyst Center, meaning update interface details + task not executed successfully.""", "INFO") if device_updated and credential_update and device_type == "NETWORK_DEVICE": credential_update_flag = self.check_credential_update() @@ -2827,11 +2879,11 @@ def verify_diff_merged(self, config): if credential_update_flag: self.status = "success" msg = "Device credentials and details updated and verified successfully in Cisco Catalyst Center." - self.log(msg) + self.log(msg, "INFO") else: - self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning device updation task not executed properly.") + self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning device updation task not executed properly.", "INFO") elif device_type != "NETWORK_DEVICE": - self.log("Cannot compare the parameter for device type {0} in the playbook with Cisco Catalyst Center.".format(device_type)) + self.log("Cannot compare the parameter for device type {0} in the playbook with Cisco Catalyst Center.".format(device_type), "WARNING") if self.config[0].get('add_user_defined_field'): field_name = self.config[0].get('add_user_defined_field').get('name') @@ -2840,9 +2892,9 @@ def verify_diff_merged(self, config): if udf_exist: self.status = "success" msg = "Global UDF {0} created and verified successfully".format(field_name) - self.log(msg) + self.log(msg, "INFO") else: - self.log("Playbook paramater doesnot match with the Cisco DNA Center means creating Global UDF task not executed successfully.") + self.log("Playbook paramater doesnot match with the Cisco Catalyst Center means creating Global UDF task not executed successfully.", "INFO") if device_updated and self.config[0].get('update_device_role'): device_role_flag = True @@ -2855,9 +2907,9 @@ def verify_diff_merged(self, config): if device_role_flag: self.status = "success" msg = "Device roles updated and verified successfully." - self.log(msg) + self.log(msg, "INFO") else: - self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning update device role task not executed successfully.") + self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning update device role task not executed successfully.", "INFO") if self.config[0].get('provision_wired_device'): provision_wired_flag = True @@ -2870,28 +2922,28 @@ def verify_diff_merged(self, config): if provision_wired_flag: self.status = "success" msg = "Wired devices {0} get provisioned and verified successfully.".format(device_ips) - self.log(msg) + self.log(msg, "INFO") else: - self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning provisioning task not executed successfully.") + self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning provisioning task not executed successfully.", "INFO") return self def verify_diff_deleted(self, config): """ - Verify the deletion status of Device and Global UDF in Cisco DNA Center. - Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + Verify the deletion status of Device and Global UDF in Cisco Catalyst Center. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): The configuration details to be verified. Return: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This method checks the deletion status of a configuration in Cisco DNA Center. - It validates whether the specified Devices or Global UDF deleted from Cisco DNA Center. + This method checks the deletion status of a configuration in Cisco Catalyst Center. + It validates whether the specified Devices or Global UDF deleted from Cisco Catalyst Center. """ self.get_have(config) - self.log(str(self.have)) - self.log(str(self.want)) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") input_devices = self.have["want_device"] device_in_dnac = self.device_exists_in_dnac() @@ -2901,8 +2953,8 @@ def verify_diff_deleted(self, config): if udf_id is None: self.status = "success" - msg = "Global UDF - {0} deleted from Cisco DNA Center and verified successfully.".format(field_name) - self.log(msg) + msg = "Global UDF '{0}' deleted from Cisco Catalyst Center and verified successfully.".format(field_name) + self.log(msg, "INFO") return self device_delete_flag = True @@ -2913,10 +2965,10 @@ def verify_diff_deleted(self, config): if device_delete_flag: self.status = "success" - self.msg = "Requested Devices - {0} Deleted from Cisco DNA Center and Deletion verified.".format(str(input_devices)) - self.log(self.msg) + self.msg = "Requested Devices '{0}' Deleted from Cisco Catalyst Center and Deletion verified.".format(str(input_devices)) + self.log(self.msg, "INFO") else: - self.log("Playbook paramater doesnot match with the Cisco DNA Center means Device Deletion task not executed successfully.") + self.log("Playbook paramater doesnot match with the Cisco Catalyst Center means Device Deletion task not executed successfully.", "INFO") return self diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 8b330d7a87..85f02e6643 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -355,7 +355,7 @@ def validate_input(self): if not self.config: self.status = "success" - self.msg = "config not available in playbook for validattion" + self.msg = "Configuration is not available in the playbook for validation" self.log(self.msg, "ERROR") return self @@ -483,14 +483,14 @@ def site_exists(self): ) except Exception as e: - self.log("The input site {0} is not valid or site is not present.".format(self.want.get("site_name")), "WARNING") - + self.log("The provided site name '{0}' is either invalid or not present in the Cisco Catalyst Center." + .format(self.want.get("site_name")), "WARNING") if response: response = response.get("response") self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") current_site = self.get_current_site(response) site_exists = True - self.log("Site {0} exist in Cisco Catalyst Center".format(self.want.get("site_name")), "INFO") + self.log("Site '{0}' exist in Cisco Catalyst Center".format(self.want.get("site_name")), "INFO") return (site_exists, current_site) @@ -544,13 +544,13 @@ def get_site_params(self, params): try: site_info["floor"]["rfModel"] = floor_details.get("rf_model") except Exception as e: - self.log("Floor {0} doesnot have rfModel attribute".format(floor_details.get('name')), "WARNING") + self.log("The attribute 'rf_model' is missing in floor '{0}'.".format(floor_details.get('name')), "WARNING") site_params = dict( type=typeinfo, site=site_info, ) - self.log("Parameters for site is: {0}".format(str(site_params)), "DEBUG") + self.log("Site parameters: {0}".format(str(site_params)), "DEBUG") return site_params @@ -572,7 +572,7 @@ def get_site_name(self, site): parent_name = site.get("site").get(site_type).get("parent_name") name = site.get("site").get(site_type).get("name") site_name = '/'.join([parent_name, name]) - self.log("Name of the site is: {0}".format(site_name), "INFO") + self.log("Site name: {0}".format(site_name), "INFO") return site_name @@ -714,7 +714,7 @@ def get_have(self, config): # check if given site exits, if exists store current site info (site_exists, current_site) = self.site_exists() - self.log("Current Site details: {0}".format(str(current_site)), "DEBUG") + self.log("Current Site details (have): {0}".format(str(current_site)), "DEBUG") if site_exists: have["site_id"] = current_site.get("siteId") @@ -832,9 +832,9 @@ def get_diff_merged(self, config): (site_exists, current_site) = self.site_exists() if site_exists: - log_msg = "Site {0} created Successfully".format(self.want.get("site_name")) + log_msg = "Site '{0}' created successfully".format(self.want.get("site_name")) self.log(log_msg, "INFO") - self.log("Current site: {0}".format(str(current_site)), "DEBUG") + self.log("Current site (have): {0}".format(str(current_site)), "DEBUG") self.result['msg'] = log_msg self.result['response'].update({"siteId": current_site.get('site_id')}) @@ -870,21 +870,21 @@ def delete_single_site(self, site_id, site_name): while True: execution_details = self.get_execution_details(executionid) if execution_details.get("status") == "SUCCESS": - self.msg = "Site - {0} deleted successfully".format(site_name) + self.msg = "Site '{0}' deleted successfully".format(site_name) self.result['changed'] = True self.result['response'] = self.msg self.status = "success" self.log(self.msg, "INFO") break elif execution_details.get("bapiError"): - self.log("Execution error response for 'delete_site': {0}".format(execution_details.get("bapiError")), "ERROR") + self.log("Error response for 'delete_site' execution: {0}".format(execution_details.get("bapiError")), "ERROR") self.module.fail_json(msg=execution_details.get("bapiError"), response=execution_details) break except Exception as e: self.status = "failed" - self.msg = "Cannot Delete device from Inventory because of {0}".format(str(e)) - self.log(self.msg, "CRITICAL") + self.msg = "Exception occurred while deleting device from inventory: {0}".format(str(e)) + self.log(self.msg, "ERROR") return self @@ -910,11 +910,11 @@ def get_diff_deleted(self, config): site_name = self.want.get("site_name") if not site_exists: self.status = "success" - self.msg = "Cannot delete Site - {0} as it's not found in Cisco Catalyst Center".format(site_name) + self.msg = "Unable to delete site '{0}' as it's not found in Cisco Catalyst Center".format(site_name) self.result.update({'changed': False, 'response': self.msg, 'msg': self.msg}) - self.log(self.msg, "WARNING") + self.log(self.msg, "INFO") return self @@ -942,7 +942,7 @@ def get_diff_deleted(self, config): # Delete the final parent site self.delete_single_site(site_id, site_name) - self.msg = "Site - {0} and it's child sites deleted successfully".format(site_name) + self.msg = "The site '{0}' and its child sites have been deleted successfully".format(site_name) self.result['response'] = self.msg self.log(self.msg, "INFO") @@ -972,17 +972,18 @@ def verify_diff_merged(self, config): if site_exist: self.status = "success" - self.msg = "Requested Site - {0} present in Cisco Catalyst Center and creation verified.".format(site_name) + self.msg = "The requested site '{0}' is present in the Cisco Catalyst Center and its creation has been verified.".format(site_name) self.log(self.msg, "INFO") require_update = self.site_requires_update() if not require_update: - self.log("Site - {0} Updation Verified Successfully.".format(site_name), "INFO") + self.log("The update for site '{0}' has been successfully verified.".format(site_name), "INFO") self. status = "success" return self - self.log("Playbook paramater doesnot match with the Cisco Catalyst Center means Merged task not executed successfully.", "INFO") + self.log("""The playbook input parameter does not align with the Cisco Catalyst Center, indicating that the merge task + was not executed successfully.""", "INFO") return self @@ -1008,11 +1009,12 @@ def verify_diff_deleted(self, config): if not site_exist: self.status = "success" - msg = "Requested Site - {0} already deleted from Cisco Catalyst Center and verified successfully.".format(self.want.get("site_name")) + msg = """The requested site '{0}' has already been deleted from the Cisco Catalyst Center and this has been + successfully verified.""".format(self.want.get("site_name")) self.log(msg, "INFO") return self - self.log("Playbook paramater doesnot match with the Cisco Catalyst Center means Deletion not executed successfully.", "INFO") + self.log("Playbook parameter did not match with the Cisco Catalyst Center, indicating that the deletion was not executed successfully.", "INFO") return self diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index fbb5bba19f..e25ba14ead 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -418,7 +418,7 @@ def validate_input(self): if not self.config: self.status = "success" - self.msg = "config not available in playbook for validation" + self.msg = "Configuration is not available in the playbook for validation" self.log(self.msg, "ERROR") return self @@ -472,7 +472,7 @@ def site_exists(self, site_name): params={"name": site_name}, ) except Exception as e: - self.log("Exception occured- Site {0} doesnot exist in Cisco Catalyst Center".format(site_name), "CRITICAL") + self.log("An exception occurred: Site '{0}' does not exist in the Cisco Catalyst Center".format(site_name), "ERROR") self.module.fail_json(msg="Site not found") if response: @@ -509,11 +509,11 @@ def get_image_id(self, name): if (len(image_list) == 1): image_id = image_list[0].get("imageUuid") - self.log("SWIM image {0} having the Id: {1}".format(name, image_id), "INFO") + self.log("SWIM image '{0}' has the ID: {1}".format(name, image_id), "INFO") else: - error_message = "SWIM image {0} not found".format(name) - self.log(error_message, "WARNING") - self.module.fail_json(msg="SWIM image not found", response=image_response) + error_message = "SWIM image '{0}' could not be found".format(name) + self.log(error_message, "ERROR") + self.module.fail_json(msg=error_message, response=image_response) return image_id @@ -716,7 +716,7 @@ def get_have(self): if site_exists: have["site_id"] = site_id - self.log("Site {0} exists having the site id: {1}".format(site_name, str(site_id)), "DEBUG") + self.log("Site '{0}' exists and has the site ID: {1}".format(site_name, str(site_id)), "DEBUG") # check if image for distributon is available if distribution_details.get("image_name"): @@ -728,8 +728,8 @@ def get_have(self): have["distribution_image_id"] = self.have.get("imported_image_id") else: - self.log("Image details for distribution not provided", "CRITICAL") - self.module.fail_json(msg="Image details for distribution not provided", response=[]) + self.log("Image details required for distribution have not been provided", "ERROR") + self.module.fail_json(msg="Image details required for distribution have not been provided", response=[]) device_params = dict( hostname=distribution_details.get("device_hostname"), @@ -754,8 +754,8 @@ def get_have(self): elif self.have.get("imported_image_id"): have["activation_image_id"] = self.have.get("imported_image_id") else: - self.log("Image details for activation not provided", "CRITICAL") - self.module.fail_json(msg="Image details for activation not provided", response=[]) + self.log("Image details required for activation have not been provided", "ERROR") + self.module.fail_json(msg="Image details required for activation have not been provided", response=[]) site_name = activation_details.get("site_name") if site_name: @@ -763,7 +763,7 @@ def get_have(self): (site_exists, site_id) = self.site_exists(site_name) if site_exists: have["site_id"] = site_id - self.log("Site {0} exists having the site id: {1}".format(site_name, str(site_id)), "INFO") + self.log("The site '{0}' exists and has the site ID '{1}'".format(site_name, str(site_id)), "INFO") device_params = dict( hostname=activation_details.get("device_hostname"), @@ -862,7 +862,7 @@ def get_diff_import(self): if image_exist: image_id = self.get_image_id(name) self.have["imported_image_id"] = image_id - self.msg = "Image {0} already exists in the Cisco Catalyst Center".format(name) + self.msg = "Image '{0}' already exists in the Cisco Catalyst Center".format(name) self.result['msg'] = self.msg self.log(self.msg, "INFO") self.status = "success" @@ -979,7 +979,7 @@ def get_diff_tagging(self): deviceFamilyIdentifier=self.have.get("device_family_identifier"), deviceRole=tagging_details.get("device_role") ) - self.log("Image params for tagging image as golden: {0}".format(str(image_params)), "INFO") + self.log("Parameters for tagging the image as golden: {0}".format(str(image_params)), "INFO") response = self.dnac._exec( family="software_image_management_swim", @@ -996,7 +996,7 @@ def get_diff_tagging(self): device_family_identifier=self.have.get("device_family_identifier"), device_role=tagging_details.get("device_role") ) - self.log("Image params for tagging image as golden: {0}".format(str(image_params)), "INFO") + self.log("Parameters for un-tagging the image as golden: {0}".format(str(image_params)), "INFO") response = self.dnac._exec( family="software_image_management_swim", @@ -1048,7 +1048,7 @@ def get_device_ip_from_id(self, device_id): return device_ip except Exception as e: - error_message = "Error while getting the response of device from Cisco Catalyst Center - {0}".format(str(e)) + error_message = "Error occurred while getting the response of device from Cisco Catalyst Center: {0}".format(str(e)) self.log(error_message, "ERROR") raise Exception(error_message) @@ -1116,12 +1116,12 @@ def get_diff_distribution(self): if len(device_uuid_list) == 0: self.status = "failed" - self.msg = "No devices found for Image Distribution" + self.msg = "Image Distribution cannot proceed due to the absence of device(s)" self.result['msg'] = self.msg self.log(self.msg, "WARNING") return self - self.log("List of device UUID's for Image Distribution: {0}".format(str(device_uuid_list)), "INFO") + self.log("Device UUIDs involved in Image Distribution: {0}".format(str(device_uuid_list)), "INFO") device_distribution_count = 0 device_ips_list = [] @@ -1257,7 +1257,7 @@ def get_diff_activation(self): self.log(self.msg, "WARNING") return self - self.log("List of device UUID's for Image Activation: {0}".format(str(device_uuid_list)), "INFO") + self.log("Device UUIDs involved in Image Activation: {0}".format(str(device_uuid_list)), "INFO") device_activation_count = 0 device_ips_list = [] From deb9edccd2b32d7abdf1138a2930d8d83bb7a1f1 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Sun, 28 Jan 2024 15:44:59 +0530 Subject: [PATCH 15/76] add log messages with severity level and more readable for site, swim and inventory module --- plugins/modules/inventory_intent.py | 77 ++++++++++++++++------------- plugins/modules/site_intent.py | 12 ++--- plugins/modules/swim_intent.py | 34 ++++++------- 3 files changed, 66 insertions(+), 57 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 08eafea631..8b97793c8b 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -224,16 +224,17 @@ description: Ip Address allocated to the interface type: str interface_netmask_in_cidr: - description: Interface mask in cidr. + description: The netmask of the interface, given in CIDR notation. This is an integer that represents the + number of bits set in the netmask type: int interface_gateway: - description: name of the gateway for the interface + description: The name identifier for the gateway associated with the interface. type: str lag_or_port_number: - description: Lag or port number of interface + description: The Link Aggregation Group (LAG) number or port number assigned to the interface. type: int vlan_id: - description: Vlan Ifd of interface + description: The VLAN (Virtual Local Area Network) ID associated with the network interface. type: int interface_name: description: Name of the interface. @@ -887,7 +888,7 @@ def create_user_defined_field(self): self.status = "success" except Exception as e: - error_message = "Error while creating Global UDF in Cisco Catalyst Center: {0}".format(str(e)) + error_message = "Error while creating Global UDF(User Defined Field) in Cisco Catalyst Center: {0}".format(str(e)) log(error_message, "ERROR") return self @@ -1062,7 +1063,7 @@ def export_device_details(self): if not device_ips: self.status = "failed" - self.msg = "No Devices are given in the playbook so can't export device details" + self.msg = "Cannot export device details as no devices are specified in the playbook" self.log(self.msg, "ERROR") return self @@ -1233,16 +1234,16 @@ def resync_devices(self): self.status = "success" self.result['changed'] = True self.result['response'] = execution_details - self.msg = "Device Resynced Successfully and Resynced devices are: {0}".format(str(input_device_ips)) + self.msg = "Devices have been successfully resynced. Devices resynced: {0}".format(str(input_device_ips)) self.log(self.msg, "INFO") break elif execution_details.get("isError"): self.status = "failed" failure_reason = execution_details.get("failureReason") if failure_reason: - self.msg = "Device Resynced get failed because of {0}".format(failure_reason) + self.msg = "Device resynced get failed because of {0}".format(failure_reason) else: - self.msg = "Device Resynced get failed." + self.msg = "Device resynced get failed." self.log(self.msg, "ERROR") break @@ -1818,7 +1819,7 @@ def get_udf_id(self, field_name): udf_id = udf[0].get("id") except Exception as e: - error_message = "Exception occurred while getting Global UDF ID from Cisco Catalyst Center: {0}".format(str(e)) + error_message = "Exception occurred while getting Global User Defined Fields(UDF) ID from Cisco Catalyst Center: {0}".format(str(e)) self.log(error_message, "ERROR") return udf_id @@ -2002,9 +2003,9 @@ def get_interface_from_id_and_name(self, device_id, interface_name): Returns: str: The interface ID for the specified device and interface name. Description: - The function sends a request to Cisco Catalyst Center to retrieve the interface information - for the device with the provided device id and interface name and extracts the interface ID from the - response, and returns the interface ID. + The function sends a request to Cisco Catalyst Center to retrieve the interface information + for the device with the provided device id and interface name and extracts the interface ID from the + response, and returns the interface ID. """ try: @@ -2040,9 +2041,9 @@ def get_interface_from_ip(self, device_ip): Returns: str: The interface ID for the specified device. Description: - The function sends a request to Cisco DNA Center to retrieve the interface information - for the device with the provided IP address and extracts the interface ID from the - response, and returns the interface ID. + The function sends a request to Cisco DNA Center to retrieve the interface information + for the device with the provided IP address and extracts the interface ID from the + response, and returns the interface ID. """ try: @@ -2147,7 +2148,7 @@ def check_interface_details(self, device_ip, interface_name): response = response.get("response") if not response: - self.log("Doesnot get the response of 'get_interface_details' api.", "DEBUG") + self.log("No response received from the API 'get_interface_details'.", "DEBUG") return False response_params = { @@ -2324,7 +2325,7 @@ def get_diff_merged(self, config): self.add_field_to_devices(device_ids).check_return_status() self.result['changed'] = True - self.msg = "Global User Defined Added with name '{0}' added to device Successfully !".format(field_name) + self.msg = "Global User Defined Field(UDF) named '{0}' has been successfully added to the device.".format(field_name) self.log(self.msg, "INFO") config['type'] = device_type @@ -2520,7 +2521,7 @@ def get_diff_merged(self, config): if response.get('role') == device_role_args.get('role'): self.status = "success" self.result['changed'] = False - log_msg = "Device Role '{0}' same in Cisco Catalyst Center as well, no updation needed".format(device_role_args.get('role')) + log_msg = "The device role '{0}' is already set in Cisco Catalyst Center, no update is needed.".format(device_role_args.get('role')) self.log(log_msg, "INFO") continue @@ -2725,7 +2726,7 @@ def get_diff_deleted(self, config): self.status = "failed" failure_reason = execution_details.get("failureReason") if failure_reason: - self.msg = "Global UDF deletion get failed because of {0}".format(failure_reason) + self.msg = "Failed to delete Global User Defined Field(UDF) due to: {0}".format(failure_reason) else: self.msg = "Global UDF deletion get failed." self.log(self.msg, "ERROR") @@ -2797,7 +2798,7 @@ def get_diff_deleted(self, config): if 'success' in execution_details.get("progress"): self.status = "success" - self.msg = "Device deleted successfully from Cisco Catalyst Center" + self.msg = "Device '{0}' was successfully deleted from Cisco Catalyst Center".format(device_ip) self.log(self.msg, "INFO") self.result['changed'] = True self.result['response'] = execution_details @@ -2806,9 +2807,9 @@ def get_diff_deleted(self, config): self.status = "failed" failure_reason = execution_details.get("failureReason") if failure_reason: - self.msg = "Device deletion get failed because of {0}".format(failure_reason) + self.msg = "Device '{0}' deletion get failed due to: {1}".format(device_ip, failure_reason) else: - self.msg = "Device deletion get failed." + self.msg = "Device '{0}' deletion get failed.".format(device_ip) self.log(self.msg, "ERROR") break self.result['msg'] = self.msg @@ -2851,10 +2852,12 @@ def verify_diff_merged(self, config): if device_added: if not devices_to_add: self.status = "success" - msg = "Requested Device(s) '{0}' Added in Cisco Catalyst Center and Addition verified.".format(str(device_ips)) + msg = """Requested device(s) '{0}' have been successfully added to the Cisco Catalyst Center and their + addition has been verified.""".format(str(device_ips)) self.log(msg, "INFO") else: - self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning device addition task not executed successfully.", "INFO") + self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that the device addition + task may not have executed successfully.""", "INFO") if device_updated and self.config[0].get('update_interface_details'): interface_update_flag = True @@ -2870,8 +2873,8 @@ def verify_diff_merged(self, config): msg = "Interface details updated and verified successfully for devices {0}.".format(device_ips) self.log(msg, "INFO") else: - self.log("""Playbook parameter does not match with Cisco Catalyst Center, meaning update interface details - task not executed successfully.""", "INFO") + self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that the update + interface details task may not have executed successfully.""", "INFO") if device_updated and credential_update and device_type == "NETWORK_DEVICE": credential_update_flag = self.check_credential_update() @@ -2883,7 +2886,8 @@ def verify_diff_merged(self, config): else: self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning device updation task not executed properly.", "INFO") elif device_type != "NETWORK_DEVICE": - self.log("Cannot compare the parameter for device type {0} in the playbook with Cisco Catalyst Center.".format(device_type), "WARNING") + self.log("""Unable to compare the parameter for device type '{0}' in the playbook with the one in Cisco Catalyst Center.""" + .format(device_type), "WARNING") if self.config[0].get('add_user_defined_field'): field_name = self.config[0].get('add_user_defined_field').get('name') @@ -2894,7 +2898,8 @@ def verify_diff_merged(self, config): msg = "Global UDF {0} created and verified successfully".format(field_name) self.log(msg, "INFO") else: - self.log("Playbook paramater doesnot match with the Cisco Catalyst Center means creating Global UDF task not executed successfully.", "INFO") + self.log("""Mismatch between playbook parameter and Cisco Catalyst Center detected, indicating that + the task of creating Global UDF may not have executed successfully.""", "INFO") if device_updated and self.config[0].get('update_device_role'): device_role_flag = True @@ -2909,7 +2914,8 @@ def verify_diff_merged(self, config): msg = "Device roles updated and verified successfully." self.log(msg, "INFO") else: - self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning update device role task not executed successfully.", "INFO") + self.log("""Mismatch between playbook parameter 'role' and Cisco Catalyst Center detected, indicating the + device role update task may not have executed successfully.""", "INFO") if self.config[0].get('provision_wired_device'): provision_wired_flag = True @@ -2924,7 +2930,8 @@ def verify_diff_merged(self, config): msg = "Wired devices {0} get provisioned and verified successfully.".format(device_ips) self.log(msg, "INFO") else: - self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning provisioning task not executed successfully.", "INFO") + self.log("""Mismatch between playbook's input and Cisco Catalyst Center detected, indicating that + the provisioning task may not have executed successfully.""", "INFO") return self @@ -2953,22 +2960,24 @@ def verify_diff_deleted(self, config): if udf_id is None: self.status = "success" - msg = "Global UDF '{0}' deleted from Cisco Catalyst Center and verified successfully.".format(field_name) + msg = "Global UDF named '{0}' has been successfully deleted from Cisco Catalyst Center and the deletion has been verified.".format(field_name) self.log(msg, "INFO") return self device_delete_flag = True for device_ip in input_devices: if device_ip in device_in_dnac: + device_after_deletion = device_ip device_delete_flag = False break if device_delete_flag: self.status = "success" - self.msg = "Requested Devices '{0}' Deleted from Cisco Catalyst Center and Deletion verified.".format(str(input_devices)) + self.msg = "Requested device(s) '{0}' deleted from Cisco Catalyst Center and the deletion has been verified.".format(str(input_devices)) self.log(self.msg, "INFO") else: - self.log("Playbook paramater doesnot match with the Cisco Catalyst Center means Device Deletion task not executed successfully.", "INFO") + self.log("""Mismatch between playbook parameter device({0}) and Cisco Catalyst Center detected, indicating that + the device deletion task may not have executed successfully.""".format(device_after_deletion), "INFO") return self diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 85f02e6643..b6bba01cbf 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -490,7 +490,7 @@ def site_exists(self): self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") current_site = self.get_current_site(response) site_exists = True - self.log("Site '{0}' exist in Cisco Catalyst Center".format(self.want.get("site_name")), "INFO") + self.log("Site '{0}' exists in Cisco Catalyst Center".format(self.want.get("site_name")), "INFO") return (site_exists, current_site) @@ -883,7 +883,7 @@ def delete_single_site(self, site_id, site_name): except Exception as e: self.status = "failed" - self.msg = "Exception occurred while deleting device from inventory: {0}".format(str(e)) + self.msg = "Exception occurred while deleting site '{0}' due to: {1}".format(site_name, str(e)) self.log(self.msg, "ERROR") return self @@ -982,8 +982,8 @@ def verify_diff_merged(self, config): self. status = "success" return self - self.log("""The playbook input parameter does not align with the Cisco Catalyst Center, indicating that the merge task - was not executed successfully.""", "INFO") + self.log("""The playbook input for site '{0}' does not align with the Cisco Catalyst Center, indicating that the merge task + may not have executed successfully.""".format(site_name), "INFO") return self @@ -1013,8 +1013,8 @@ def verify_diff_deleted(self, config): successfully verified.""".format(self.want.get("site_name")) self.log(msg, "INFO") return self - - self.log("Playbook parameter did not match with the Cisco Catalyst Center, indicating that the deletion was not executed successfully.", "INFO") + self.log("""Mismatch between the playbook input for site '{0}' and the Cisco Catalyst Center indicates that + the deletion was not executed successfully.""".format(self.want.get("site_name")), "INFO") return self diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index e25ba14ead..22bfb52b0d 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -90,7 +90,7 @@ source_url: description: Swim Import Image Via Url. type: str - third_party: + is_third_party: description: ThirdParty flag. type: bool vendor: @@ -243,7 +243,7 @@ url_details: payload: - source_url: string - third_party: bool + is_third_party: bool image_family: string vendor: string application_type: string @@ -805,7 +805,7 @@ def get_want(self, config): elif want["import_type"] == "local": want["local_import_details"] = config.get("import_image_details").get("local_image_details") else: - self.log("Incorrect import type. Supported Values: local or url", "CRITICAL") + self.log("The import type '{0}' provided is incorrect. Only 'local' or 'url' are supported.".format(want["import_type"]), "CRITICAL") self.module.fail_json(msg="Incorrect import type. Supported Values: local or url") want["tagging_details"] = config.get("tagging_details") @@ -836,10 +836,10 @@ def get_diff_import(self): import_type = self.want.get("import_type") if not import_type: + self.status = "success" self.msg = "Error: Details required for importing SWIM image. Please provide the necessary information." self.result['msg'] = self.msg self.log(self.msg, "WARNING") - self.status = "success" self.result['changed'] = False return self @@ -856,7 +856,7 @@ def get_diff_import(self): 'source_url': 'sourceURL', 'image_family': 'imageFamily', 'application_type': 'applicationType', - 'third_party': 'thirdParty', + 'is_third_party': 'thirdParty', } if image_exist: @@ -1153,12 +1153,12 @@ def get_diff_distribution(self): ("completed successfully" in task_details.get("progress")): self.result['changed'] = True self.status = "success" - self.result['msg'] = "Image with Id {0} Distributed Successfully".format(image_id) + self.result['msg'] = "Image with Id '{0}' Distributed successfully".format(image_id) device_distribution_count += 1 break if task_details.get("isError"): - error_msg = "Image with Id {0} Distribution Failed".format(image_id) + error_msg = "Image with Id '{0}' Distribution failed".format(image_id) self.log(error_msg, "WARNING") self.result['response'] = task_details device_ips_list.append(device_management_ip) @@ -1174,8 +1174,8 @@ def get_diff_distribution(self): else: self.result['changed'] = True self.status = "success" - self.msg = "Image with Id {0} Distributed and partially Successfull".format(image_id) - self.log("For Devices {0} Image Distribution gets Failed".format(str(device_ips_list)), "CRITICAL") + self.msg = "Image with Id '{0}' Distributed and partially successfull".format(image_id) + self.log("For device(s) {0} image Distribution gets failed".format(str(device_ips_list)), "CRITICAL") self.result['msg'] = self.msg self.log(self.msg, "INFO") @@ -1239,7 +1239,7 @@ def get_diff_activation(self): break if task_details.get("isError"): - error_msg = "Activation for Image with Id - {0} gets Failed".format(image_id) + error_msg = "Activation for Image with Id '{0}' gets failed".format(image_id) self.status = "failed" self.result['response'] = task_details self.msg = error_msg @@ -1252,7 +1252,7 @@ def get_diff_activation(self): if len(device_uuid_list) == 0: self.status = "failed" - self.msg = "No Devices found for Image Activation" + self.msg = "No devices found for Image Activation" self.result['msg'] = self.msg self.log(self.msg, "WARNING") return self @@ -1296,12 +1296,12 @@ def get_diff_activation(self): ("completed successfully" in task_details.get("progress")): self.result['changed'] = True self.status = "success" - self.result['msg'] = "Image with Id {0} Activated Successfully".format(image_id) + self.result['msg'] = "Image with Id '{0}' activated successfully".format(image_id) device_activation_count += 1 break if task_details.get("isError"): - error_msg = "Image with Id {0} Activation Failed".format(image_id) + error_msg = "Image with Id '{0}' activation failed".format(image_id) self.log(error_msg, "WARNING") self.result['response'] = task_details device_ips_list.append(device_management_ip) @@ -1309,16 +1309,16 @@ def get_diff_activation(self): if device_activation_count == 0: self.status = "failed" - msg = "Image with Id {0} Activation Failed for all devices".format(image_id) + msg = "Image with Id '{0}' activation failed for all devices".format(image_id) elif device_activation_count == len(device_uuid_list): self.result['changed'] = True self.status = "success" - msg = "Image with Id {0} Activated Successfully for all devices".format(image_id) + msg = "Image with Id '{0}' activated successfully for all devices".format(image_id) else: self.result['changed'] = True self.status = "success" - msg = "Image with Id {0} Activated and partially Successfull".format(image_id) - self.log("For Devices {0} Image Activation gets Failed".format(str(device_ips_list)), "CRITICAL") + msg = "Image with Id '{0}' activated and partially successfull".format(image_id) + self.log("For Device(s) {0} Image activation gets Failed".format(str(device_ips_list)), "CRITICAL") self.result['msg'] = msg self.log(msg, "INFO") From 8ae2f1023fa3f4d15aa3be9c243ffb25080366f9 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Sun, 28 Jan 2024 21:31:57 -0500 Subject: [PATCH 16/76] Optimized code to not perform validation of paramters at every self.log --- plugins/module_utils/dnac.py | 90 ++++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 24 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 4e44c11bdf..61f5f67f77 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -28,6 +28,7 @@ import inspect import re +_first_log_written = False class DnacBase(): @@ -64,15 +65,13 @@ def __init__(self, module): 'parsed': self.verify_diff_parsed } self.dnac_log = dnac_params.get("dnac_log") - - self.dnac_log_level = dnac_params.get("dnac_log_level") - if self.dnac_log_level is None: - self.dnac_log_level = "INFO" + self.dnac_log_level = dnac_params.get("dnac_log_level") or 'INFO' + self.dnac_log_file_path = dnac_params.get("dnac_log_file_path") or 'dnac.log' + if dnac_params.get("dnac_append_logs") == None: + self.dnac_append_logs = True else: - self.dnac_log_level = self.dnac_log_level.upper() - self.is_valid_log_level() - - log(str(dnac_params)) + self.dnac_append_logs = dnac_params.get("dnac_append_logs") + self.log('Dnac parameters: {}'.format(str(dnac_params)), 'DEBUG') self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -153,7 +152,7 @@ def verify_diff_parsed(self): self.parsed = True return self - def log(self, message, level="info", frameIncrement=0): + def log(self, message, level="INFO", frameIncrement=0): """Logs/Appends messages into dnac.log file if logging is enabled and the log level is appropriate Args: self (obj, required): An instance of the DnacBase Class. @@ -162,14 +161,28 @@ def log(self, message, level="info", frameIncrement=0): The log level can be one of 'DEBUG', 'INFO', 'WARNING', 'ERROR', or 'CRITICAL'. frameIncrement (int, optional): The number of frames to increment in the call stack, default is 0. """ + global _first_log_written + + + if _first_log_written == False: + log_config = LogConfig(self.dnac_log_level, self.dnac_log_file_path) + self.is_valid_level(message, level) level = level.upper() + if ( self.dnac_log - and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level) + and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level.upper()) ): message = "Module: " + self.__class__.__name__ + ", " + message - log(message, level, (1 + frameIncrement)) + log(message, level, self.dnac_log_file_path, self.dnac_append_logs, (1 + frameIncrement)) + + def is_valid_level(self, message, level): + if not isinstance(level, str): + raise ValueError("Invalid log level type passed when logging the following msg: {0} level:{1}." + " Expected a string.".format(message, level)) + if level.upper() not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): + raise ValueError("Invalid log level passed when logging the following msg: {0} level:{1}.".format(message, level)) def check_return_status(self): """API to check the return status value and exit/fail the module""" @@ -213,7 +226,9 @@ def get_dnac_params(self, params): "dnac_verify": params.get("dnac_verify"), "dnac_debug": params.get("dnac_debug"), "dnac_log": params.get("dnac_log"), - "dnac_log_level": params.get("dnac_log_level") + "dnac_log_level": params.get("dnac_log_level"), + "dnac_log_file_path": params.get("dnac_log_file_path"), + "dnac_append_logs": params.get("dnac_append_logs") } return dnac_params @@ -237,7 +252,7 @@ def get_task_details(self, task_id): params={"task_id": task_id} ) - log(str(response)) + self.log('Tas Details: {}'.format(str(response)), 'DEBUG') if response and isinstance(response, dict): result = response.get('response') @@ -276,7 +291,7 @@ def check_task_response_status(self, response, validation_string, data=False): task_id = response.get("taskId") while True: task_details = self.get_task_details(task_id) - self.log(str(task_details)) + self.log('Task details: {}'.format( str(task_details)), 'DEBUG') if task_details.get("isError") is True: if task_details.get("failureReason"): @@ -406,21 +421,49 @@ def camel_to_snake_case(self, config): return config return new_config - def is_valid_log_level(self): - valid_log_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') - if self.dnac_log_level not in valid_log_levels: + +class LogConfig(): + def __init__(self, dnac_log_level='INFO', dnac_log_file_path='dnac.log'): + self.valid_log_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') + self.dnac_log_level = dnac_log_level.upper() + self.dnac_log_file_path = dnac_log_file_path + self.validate_dnac_log_level() + self.validate_dnac_log_file_path() + + def validate_dnac_log_level(self): + """Validates if the logging level is string and of expected value""" + if self.dnac_log_level not in self.valid_log_levels: raise ValueError("Invalid log level: 'dnac_log_level:{0}'." - " Expected one of {1}.".format(self.dnac_log_level, valid_log_levels)) + " Expected one of {1}.".format(self.dnac_log_level, self.valid_log_levels)) + + def validate_dnac_log_file_path(self): + """ + Validates the specified log file path, ensuring it is either absolute or relative, + the directory exists, and has a .log extension. + """ + # Convert the path to absolute if it's relative + dnac_log_file_path = os.path.abspath(self.dnac_log_file_path) + + # Validate if the directory exists + log_directory = os.path.dirname(dnac_log_file_path) + if not os.path.exists(log_directory): + raise FileNotFoundError("The directory for log file '{0}' does not exist.".format(dnac_log_file_path)) -def log(msg, level='info', frameIncrement=0): - with open('dnac.log', 'a') as of: +def log(message, level='INFO', dnac_log_file_path='dnac.log', dnac_append_logs=True, frameIncrement=0): + global _first_log_written + + if _first_log_written == False and dnac_append_logs == False: + mode = 'w' + else: + mode = 'a' + with open(dnac_log_file_path, mode) as of: callerframerecord = inspect.stack()[1 + frameIncrement] frame = callerframerecord[0] info = inspect.getframeinfo(frame) - d = datetime.datetime.now().replace(microsecond=0).isoformat() - of.write("---- %s ---- %s@%s ---- %s: %s\n" % (d, info.lineno, info.function, level.upper(), msg)) - + current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() + of.write("{5}-{6}---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level, message, _first_log_written, dnac_append_logs )) + _first_log_written = True def is_list_complex(x): return isinstance(x[0], dict) or isinstance(x[0], list) @@ -697,7 +740,6 @@ def validate_list_of_dicts(param_list, spec, module=None): break for param in spec: item = list_entry.get(param) - log(str(item)) if item is None: if spec[param].get("required"): invalid_params.append( From ba8b66767845e425a398d16586f5f2dd27f1bf78 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Sun, 28 Jan 2024 21:43:57 -0500 Subject: [PATCH 17/76] Optimized code to not perform validation of paramters at every self.log --- plugins/module_utils/dnac.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 61f5f67f77..63d7b4d3b8 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -67,7 +67,7 @@ def __init__(self, module): self.dnac_log = dnac_params.get("dnac_log") self.dnac_log_level = dnac_params.get("dnac_log_level") or 'INFO' self.dnac_log_file_path = dnac_params.get("dnac_log_file_path") or 'dnac.log' - if dnac_params.get("dnac_append_logs") == None: + if dnac_params.get("dnac_append_logs") is None: self.dnac_append_logs = True else: self.dnac_append_logs = dnac_params.get("dnac_append_logs") @@ -75,6 +75,7 @@ def __init__(self, module): self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} + @abstractmethod def validate_input(self): if not self.config: @@ -163,9 +164,8 @@ def log(self, message, level="INFO", frameIncrement=0): """ global _first_log_written - - if _first_log_written == False: - log_config = LogConfig(self.dnac_log_level, self.dnac_log_file_path) + if _first_log_written is False: + LogConfig(self.dnac_log_level, self.dnac_log_file_path) self.is_valid_level(message, level) level = level.upper() @@ -421,8 +421,10 @@ def camel_to_snake_case(self, config): return config return new_config - + class LogConfig(): + """Configuration class for validating logging parameters""" + def __init__(self, dnac_log_level='INFO', dnac_log_file_path='dnac.log'): self.valid_log_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') self.dnac_log_level = dnac_log_level.upper() @@ -453,7 +455,7 @@ def validate_dnac_log_file_path(self): def log(message, level='INFO', dnac_log_file_path='dnac.log', dnac_append_logs=True, frameIncrement=0): global _first_log_written - if _first_log_written == False and dnac_append_logs == False: + if _first_log_written is False and dnac_append_logs is False: mode = 'w' else: mode = 'a' @@ -462,7 +464,7 @@ def log(message, level='INFO', dnac_log_file_path='dnac.log', dnac_append_logs=T frame = callerframerecord[0] info = inspect.getframeinfo(frame) current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() - of.write("{5}-{6}---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level, message, _first_log_written, dnac_append_logs )) + of.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level, message)) _first_log_written = True def is_list_complex(x): From bd54506b077c2092bfcbbc4e3022f1364552d983 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Sun, 28 Jan 2024 21:53:36 -0500 Subject: [PATCH 18/76] Optimized code to not perform validation of paramters at every self.log --- plugins/module_utils/dnac.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 63d7b4d3b8..a8e4463b06 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -30,6 +30,7 @@ _first_log_written = False + class DnacBase(): """Class contains members which can be reused for all intent modules""" @@ -75,7 +76,6 @@ def __init__(self, module): self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} - @abstractmethod def validate_input(self): if not self.config: @@ -175,13 +175,13 @@ def log(self, message, level="INFO", frameIncrement=0): and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level.upper()) ): message = "Module: " + self.__class__.__name__ + ", " + message - log(message, level, self.dnac_log_file_path, self.dnac_append_logs, (1 + frameIncrement)) + log(message, level, self.dnac_log_file_path, self.dnac_append_logs, (1 + frameIncrement)) def is_valid_level(self, message, level): if not isinstance(level, str): raise ValueError("Invalid log level type passed when logging the following msg: {0} level:{1}." - " Expected a string.".format(message, level)) - if level.upper() not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): + " Expected a string.".format(message, level)) + if level.upper() not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): raise ValueError("Invalid log level passed when logging the following msg: {0} level:{1}.".format(message, level)) def check_return_status(self): @@ -291,7 +291,7 @@ def check_task_response_status(self, response, validation_string, data=False): task_id = response.get("taskId") while True: task_details = self.get_task_details(task_id) - self.log('Task details: {}'.format( str(task_details)), 'DEBUG') + self.log('Task details: {}'.format(str(task_details)), 'DEBUG') if task_details.get("isError") is True: if task_details.get("failureReason"): @@ -436,11 +436,11 @@ def validate_dnac_log_level(self): """Validates if the logging level is string and of expected value""" if self.dnac_log_level not in self.valid_log_levels: raise ValueError("Invalid log level: 'dnac_log_level:{0}'." - " Expected one of {1}.".format(self.dnac_log_level, self.valid_log_levels)) + "Expected one of {1}.".format(self.dnac_log_level, self.valid_log_levels)) def validate_dnac_log_file_path(self): """ - Validates the specified log file path, ensuring it is either absolute or relative, + Validates the specified log file path, ensuring it is either absolute or relative, the directory exists, and has a .log extension. """ # Convert the path to absolute if it's relative @@ -467,6 +467,7 @@ def log(message, level='INFO', dnac_log_file_path='dnac.log', dnac_append_logs=T of.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level, message)) _first_log_written = True + def is_list_complex(x): return isinstance(x[0], dict) or isinstance(x[0], list) From 9b8050bab8aeeb9d4d44b4c3884fe719744c5d9d Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Sun, 28 Jan 2024 22:01:47 -0500 Subject: [PATCH 19/76] Optimized code to not perform validation of paramters at every self.log --- plugins/module_utils/dnac.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index a8e4463b06..2dd9d3eed0 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -180,7 +180,7 @@ def log(self, message, level="INFO", frameIncrement=0): def is_valid_level(self, message, level): if not isinstance(level, str): raise ValueError("Invalid log level type passed when logging the following msg: {0} level:{1}." - " Expected a string.".format(message, level)) + " Expected a string.".format(message, level)) if level.upper() not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): raise ValueError("Invalid log level passed when logging the following msg: {0} level:{1}.".format(message, level)) @@ -436,7 +436,7 @@ def validate_dnac_log_level(self): """Validates if the logging level is string and of expected value""" if self.dnac_log_level not in self.valid_log_levels: raise ValueError("Invalid log level: 'dnac_log_level:{0}'." - "Expected one of {1}.".format(self.dnac_log_level, self.valid_log_levels)) + "Expected one of {1}.".format(self.dnac_log_level, self.valid_log_levels)) def validate_dnac_log_file_path(self): """ From 8b82090a4e4683971f2706dcd23b98c88ccf0dbe Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Sun, 28 Jan 2024 22:22:49 -0500 Subject: [PATCH 20/76] Sanity test bug fix --- plugins/module_utils/dnac.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 2dd9d3eed0..d84b833b60 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -176,11 +176,11 @@ def log(self, message, level="INFO", frameIncrement=0): ): message = "Module: " + self.__class__.__name__ + ", " + message log(message, level, self.dnac_log_file_path, self.dnac_append_logs, (1 + frameIncrement)) + _first_log_written = True def is_valid_level(self, message, level): if not isinstance(level, str): - raise ValueError("Invalid log level type passed when logging the following msg: {0} level:{1}." - " Expected a string.".format(message, level)) + raise ValueError("Invalid log level type passed when logging the following msg: {0} level:{1}. Expected a string.".format(message, level)) if level.upper() not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): raise ValueError("Invalid log level passed when logging the following msg: {0} level:{1}.".format(message, level)) @@ -435,8 +435,7 @@ def __init__(self, dnac_log_level='INFO', dnac_log_file_path='dnac.log'): def validate_dnac_log_level(self): """Validates if the logging level is string and of expected value""" if self.dnac_log_level not in self.valid_log_levels: - raise ValueError("Invalid log level: 'dnac_log_level:{0}'." - "Expected one of {1}.".format(self.dnac_log_level, self.valid_log_levels)) + raise ValueError("Invalid log level: 'dnac_log_level:{0}'. Expected one of {1}.".format(self.dnac_log_level, self.valid_log_levels)) def validate_dnac_log_file_path(self): """ @@ -465,8 +464,6 @@ def log(message, level='INFO', dnac_log_file_path='dnac.log', dnac_append_logs=T info = inspect.getframeinfo(frame) current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() of.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level, message)) - _first_log_written = True - def is_list_complex(x): return isinstance(x[0], dict) or isinstance(x[0], list) From 5974e43f83a8db7459f33e5afdee7189861607cc Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Sun, 28 Jan 2024 22:34:37 -0500 Subject: [PATCH 21/76] Fixed sanity test bugs --- plugins/module_utils/dnac.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index d84b833b60..6a71fb117d 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -154,7 +154,7 @@ def verify_diff_parsed(self): return self def log(self, message, level="INFO", frameIncrement=0): - """Logs/Appends messages into dnac.log file if logging is enabled and the log level is appropriate + """Logs formatted messages with specified log level and incrementing the call stack frame Args: self (obj, required): An instance of the DnacBase Class. message (str, required): The log message to be recorded. @@ -179,6 +179,7 @@ def log(self, message, level="INFO", frameIncrement=0): _first_log_written = True def is_valid_level(self, message, level): + """Validates if the specified log level is a string and one of the expected values""" if not isinstance(level, str): raise ValueError("Invalid log level type passed when logging the following msg: {0} level:{1}. Expected a string.".format(message, level)) if level.upper() not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): @@ -452,8 +453,7 @@ def validate_dnac_log_file_path(self): def log(message, level='INFO', dnac_log_file_path='dnac.log', dnac_append_logs=True, frameIncrement=0): - global _first_log_written - + """Writes/Appends logs to the specified log file""" if _first_log_written is False and dnac_append_logs is False: mode = 'w' else: @@ -465,6 +465,7 @@ def log(message, level='INFO', dnac_log_file_path='dnac.log', dnac_append_logs=T current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() of.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level, message)) + def is_list_complex(x): return isinstance(x[0], dict) or isinstance(x[0], list) From 721e7080484be5c91176510e09e3e4d1f9c688be Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Sun, 28 Jan 2024 22:45:50 -0500 Subject: [PATCH 22/76] Sanity test bug fixes --- plugins/module_utils/dnac.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 6a71fb117d..5382a9120a 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -72,7 +72,7 @@ def __init__(self, module): self.dnac_append_logs = True else: self.dnac_append_logs = dnac_params.get("dnac_append_logs") - self.log('Dnac parameters: {}'.format(str(dnac_params)), 'DEBUG') + self.log('Dnac parameters: {0}'.format(str(dnac_params)), 'DEBUG') self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -253,7 +253,7 @@ def get_task_details(self, task_id): params={"task_id": task_id} ) - self.log('Tas Details: {}'.format(str(response)), 'DEBUG') + self.log('Tas Details: {0}'.format(str(response)), 'DEBUG') if response and isinstance(response, dict): result = response.get('response') @@ -292,7 +292,7 @@ def check_task_response_status(self, response, validation_string, data=False): task_id = response.get("taskId") while True: task_details = self.get_task_details(task_id) - self.log('Task details: {}'.format(str(task_details)), 'DEBUG') + self.log('Task details: {0}'.format(str(task_details)), 'DEBUG') if task_details.get("isError") is True: if task_details.get("failureReason"): From 32c7c71c390b37039b8b8c76bde54eb953044cf1 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 29 Jan 2024 12:29:49 +0530 Subject: [PATCH 23/76] Added the documetation for dnac_log_level to the doc_fragments/intetn_params.py --- plugins/doc_fragments/intent_params.py | 6 ++++++ plugins/modules/device_credential_intent.py | 7 +------ plugins/modules/network_settings_intent.py | 7 +------ plugins/modules/site_intent.py | 7 +------ plugins/modules/template_intent.py | 7 +------ 5 files changed, 10 insertions(+), 24 deletions(-) diff --git a/plugins/doc_fragments/intent_params.py b/plugins/doc_fragments/intent_params.py index fe95d684da..afaae9f79f 100644 --- a/plugins/doc_fragments/intent_params.py +++ b/plugins/doc_fragments/intent_params.py @@ -54,6 +54,12 @@ class ModuleDocFragment(object): If set to true the log file will be created at the location of the execution with the name dnac.log type: bool default: false + dnac_log_level: + description: + - Specifies the desired log level for Cisco Catalyst Center logging. + Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] + type: str + default: WARNING validate_response_schema: description: - Flag for Cisco DNA Center SDK to enable the validation of request bodies against a JSON schema. diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index d1f860ba7e..f7d92f5701 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -25,11 +25,6 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: - dnac_log_level: - description: Specifies the desired log level for Cisco Catalyst Center logging. - Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: INFO config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -2572,7 +2567,7 @@ def main(): "dnac_version": {"type": 'str', "default": '2.2.3.3'}, "dnac_debug": {"type": 'bool', "default": False}, "dnac_log": {"type": 'bool', "default": False}, - "dnac_log_level": {"type": "str", "default": "INFO"}, + "dnac_log_level": {"type": 'str', "default": 'WARNING'}, "config_verify": {"type": 'bool', "default": False}, "config": {"type": 'list', "required": True, "elements": 'dict'}, "state": {"default": 'merged', "choices": ['merged', 'deleted']}, diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 8f24cd5055..a32dc71aeb 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -25,11 +25,6 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: - dnac_log_level: - description: Specifies the desired log level for Cisco Catalyst Center logging. - Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: INFO config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -2129,7 +2124,7 @@ def main(): "dnac_version": {"type": 'str', "default": '2.2.3.3'}, "dnac_debug": {"type": 'bool', "default": False}, "dnac_log": {"type": 'bool', "default": False}, - "dnac_log_level": {"type": "str", "default": "INFO"}, + "dnac_log_level": {"type": 'str', "default": 'WARNING'}, "config_verify": {"type": 'bool', "default": False}, "config": {"type": 'list', "required": True, "elements": 'dict'}, "state": {"default": 'merged', "choices": ['merged', 'deleted']}, diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index b6bba01cbf..d0c4db7f6a 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -29,11 +29,6 @@ description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool default: False - dnac_log_level: - description: Specifies the log level for Cisco Catalyst Center logging, categorizing logs by severity. - Options- [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: INFO state: description: The state of Catalyst Center after module completion. type: str @@ -1030,7 +1025,7 @@ def main(): 'dnac_verify': {'type': 'bool', 'default': 'True'}, 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, - 'dnac_log_level': {'type': 'str', 'default': 'INFO'}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config_verify': {'type': 'bool', "default": False}, diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 540eb6f058..1e9fde74b8 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -30,11 +30,6 @@ Akash Bhaskaran (@akabhask) Muthu Rakesh (@MUTHU-RAKESH-27) options: - dnac_log_level: - description: Specifies the desired log level for Cisco Catalyst Center logging. - Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: INFO config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -2766,7 +2761,7 @@ def main(): 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, - "dnac_log_level": {"type": "str", "default": "INFO"}, + "dnac_log_level": {"type": 'str', "default": 'WARNING'}, 'validate_response_schema': {'type': 'bool', 'default': True}, "config_verify": {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, From 6aa1a52a815a97ecbb0ff6f028a9264e1c5bfa65 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 29 Jan 2024 12:56:43 +0530 Subject: [PATCH 24/76] Added the documentation for the dnac_log_level in intent_params.py --- plugins/modules/device_credential_intent.py | 7 ++++++- plugins/modules/network_settings_intent.py | 7 ++++++- plugins/modules/site_intent.py | 7 ++++++- plugins/modules/template_intent.py | 7 ++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index f7d92f5701..d1f860ba7e 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -25,6 +25,11 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: + dnac_log_level: + description: Specifies the desired log level for Cisco Catalyst Center logging. + Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] + type: str + default: INFO config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -2567,7 +2572,7 @@ def main(): "dnac_version": {"type": 'str', "default": '2.2.3.3'}, "dnac_debug": {"type": 'bool', "default": False}, "dnac_log": {"type": 'bool', "default": False}, - "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "dnac_log_level": {"type": "str", "default": "INFO"}, "config_verify": {"type": 'bool', "default": False}, "config": {"type": 'list', "required": True, "elements": 'dict'}, "state": {"default": 'merged', "choices": ['merged', 'deleted']}, diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index a32dc71aeb..8f24cd5055 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -25,6 +25,11 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: + dnac_log_level: + description: Specifies the desired log level for Cisco Catalyst Center logging. + Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] + type: str + default: INFO config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -2124,7 +2129,7 @@ def main(): "dnac_version": {"type": 'str', "default": '2.2.3.3'}, "dnac_debug": {"type": 'bool', "default": False}, "dnac_log": {"type": 'bool', "default": False}, - "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "dnac_log_level": {"type": "str", "default": "INFO"}, "config_verify": {"type": 'bool', "default": False}, "config": {"type": 'list', "required": True, "elements": 'dict'}, "state": {"default": 'merged', "choices": ['merged', 'deleted']}, diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index d0c4db7f6a..b6bba01cbf 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -29,6 +29,11 @@ description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool default: False + dnac_log_level: + description: Specifies the log level for Cisco Catalyst Center logging, categorizing logs by severity. + Options- [CRITICAL, ERROR, WARNING, INFO, DEBUG] + type: str + default: INFO state: description: The state of Catalyst Center after module completion. type: str @@ -1025,7 +1030,7 @@ def main(): 'dnac_verify': {'type': 'bool', 'default': 'True'}, 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, - 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + 'dnac_log_level': {'type': 'str', 'default': 'INFO'}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config_verify': {'type': 'bool', "default": False}, diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 1e9fde74b8..540eb6f058 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -30,6 +30,11 @@ Akash Bhaskaran (@akabhask) Muthu Rakesh (@MUTHU-RAKESH-27) options: + dnac_log_level: + description: Specifies the desired log level for Cisco Catalyst Center logging. + Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] + type: str + default: INFO config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -2761,7 +2766,7 @@ def main(): 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, - "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "dnac_log_level": {"type": "str", "default": "INFO"}, 'validate_response_schema': {'type': 'bool', 'default': True}, "config_verify": {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, From bff82500e5487c892981d436a5a7f5f1b5ab7a7d Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 29 Jan 2024 14:36:34 +0530 Subject: [PATCH 25/76] Restored the plugins/doc_fragments/intent_params.py --- plugins/doc_fragments/intent_params.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/doc_fragments/intent_params.py b/plugins/doc_fragments/intent_params.py index afaae9f79f..fe95d684da 100644 --- a/plugins/doc_fragments/intent_params.py +++ b/plugins/doc_fragments/intent_params.py @@ -54,12 +54,6 @@ class ModuleDocFragment(object): If set to true the log file will be created at the location of the execution with the name dnac.log type: bool default: false - dnac_log_level: - description: - - Specifies the desired log level for Cisco Catalyst Center logging. - Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: WARNING validate_response_schema: description: - Flag for Cisco DNA Center SDK to enable the validation of request bodies against a JSON schema. From f96894c162b2a01fc459af49c21da2572c88f387 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 29 Jan 2024 15:38:28 +0530 Subject: [PATCH 26/76] address swim issue of giving default device role, error while untagging golden image if it's inherited, support snmp_priv_protocol as both AES128, CISCOAES128 in code itself. --- plugins/modules/inventory_intent.py | 24 ++++++++++++++++++--- plugins/modules/swim_intent.py | 33 ++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 8b97793c8b..32eb98c0de 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -1012,7 +1012,10 @@ def decrypt_and_read_csv(self, response, password): encryption_dict = { 'AES128': 'pyzipper.WZ_AES128', 'AES192': 'pyzipper.WZ_AES192', - 'AES256': 'pyzipper.WZ_AES' + 'AES256': 'pyzipper.WZ_AES', + 'CISCOAES128': 'pyzipper.WZ_AES128', + 'CISCOAES192': 'pyzipper.WZ_AES192', + 'CISCOAES256': 'pyzipper.WZ_AES' } try: encryption_method = encryption_dict.get(snmp_protocol) @@ -1526,7 +1529,7 @@ def provisioned_wired_device(self): ): break count = count + 1 - if count > 200: + if count > 400: managed_flag = False break @@ -2392,6 +2395,13 @@ def get_diff_merged(self, config): playbook_params['snmpPrivPassphrase'] = csv_data_dict['snmp_priv_passphrase'] playbook_params[mapped_key] = csv_data_dict[key] + snmp_protocol_mapping = { + 'AES128': 'CISCOAES128', + 'AES192': 'CISCOAES192', + 'AES256': 'CISCOAES256' + } + protocol_type = playbook_params['snmpPrivProtocol'] + playbook_params['snmpPrivProtocol'] = snmp_protocol_mapping[protocol_type] try: response = self.dnac._exec( family="devices", @@ -2571,13 +2581,21 @@ def get_diff_merged(self, config): # If we want to add device in inventory if device_added: config['ip_address'] = devices_to_add + device_params = self.want.get("device_params") + snmp_protocol_mapping = { + 'AES128': 'CISCOAES128', + 'AES192': 'CISCOAES192', + 'AES256': 'CISCOAES256' + } + protocol_type = device_params['snmpPrivProtocol'] + device_params['snmpPrivProtocol'] = snmp_protocol_mapping[protocol_type] self.mandatory_parameter().check_return_status() try: response = self.dnac._exec( family="devices", function='add_device', op_modifies=True, - params=self.want.get("device_params"), + params=device_params, ) self.log("Received API response from 'add_device': {0}".format(str(response)), "DEBUG") diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 22bfb52b0d..2643bf46fc 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -34,7 +34,7 @@ description: Specifies the log level for Cisco Catalyst Center logging, categorizing logs by severity. Options- [CRITICAL, ERROR, WARNING, INFO, DEBUG] type: str - default: INFO + default: WARNING state: description: The state of Catalyst Center after module completion. type: str @@ -309,7 +309,6 @@ - tagging_details: image_name: string device_role: string - device_family_name: string device_type: string site_name: string tagging: bool @@ -584,7 +583,7 @@ def get_device_uuids(self, site_name, device_family, device_role): self (object): An instance of a class used for interacting with Cisco Catalyst Center. site_name (str): The name of the site for which device UUIDs are requested. device_family (str): The family/type of devices to filter on. - device_role (str): The role of devices to filter on. If None, all roles are considered. + device_role (str): The role of devices to filter on. If None, 'ALL' roles are considered. Returns: list: A list of device UUIDs that match the specified criteria. Description: @@ -977,7 +976,7 @@ def get_diff_tagging(self): imageId=self.have.get("tagging_image_id"), siteId=self.have.get("site_id"), deviceFamilyIdentifier=self.have.get("device_family_identifier"), - deviceRole=tagging_details.get("device_role") + deviceRole=tagging_details.get("device_role", "ALL").upper() ) self.log("Parameters for tagging the image as golden: {0}".format(str(image_params)), "INFO") @@ -994,7 +993,7 @@ def get_diff_tagging(self): image_id=self.have.get("tagging_image_id"), site_id=self.have.get("site_id"), device_family_identifier=self.have.get("device_family_identifier"), - device_role=tagging_details.get("device_role") + device_role=tagging_details.get("device_role", "ALL").upper() ) self.log("Parameters for un-tagging the image as golden: {0}".format(str(image_params)), "INFO") @@ -1014,8 +1013,22 @@ def get_diff_tagging(self): self.result['changed'] = True self.result['msg'] = task_details.get("progress") self.status = "success" - - self.result['response'] = task_details if task_details else response + self.result['response'] = task_details if task_details else response + elif task_details.get("isError"): + failure_reason = task_details.get("failureReason", "") + if failure_reason and "An inheritted tag cannot be un-tagged" in failure_reason: + self.status = "success" + self.result['changed'] = False + self.msg = failure_reason + self.result['msg'] = failure_reason + self.log(self.msg, "WARNING") + else: + error_message = task_details.get("failureReason", "Error: while tagging/un-tagging the golden swim image.") + self.status = "failed" + self.msg = error_message + self.result['msg'] = error_message + self.log(self.msg, "ERROR") + self.result['response'] = self.msg return self @@ -1068,7 +1081,7 @@ def get_diff_distribution(self): distribution_details = self.want.get("distribution_details") site_name = distribution_details.get("site_name") device_family = distribution_details.get("device_family_name") - device_role = distribution_details.get("device_role") + device_role = distribution_details.get("device_role", "ALL") device_uuid_list = self.get_device_uuids(site_name, device_family, device_role) image_id = self.have.get("distribution_image_id") @@ -1198,7 +1211,7 @@ def get_diff_activation(self): activation_details = self.want.get("activation_details") site_name = activation_details.get("site_name") device_family = activation_details.get("device_family_name") - device_role = activation_details.get("device_role") + device_role = activation_details.get("device_role", "ALL") device_uuid_list = self.get_device_uuids(site_name, device_family, device_role) image_id = self.have.get("activation_image_id") @@ -1363,7 +1376,7 @@ def main(): 'dnac_verify': {'type': 'bool', 'default': 'True'}, 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, - 'dnac_log_level': {'type': 'str', 'default': 'INFO'}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, From 8f9c03340e2205924a4749d1f61eaa82c79e0267 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Mon, 29 Jan 2024 20:16:58 -0500 Subject: [PATCH 27/76] Reverted to old logging mechanism --- plugins/module_utils/dnac.py | 85 +++++++++++++++--------------------- 1 file changed, 34 insertions(+), 51 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 5382a9120a..c9b335bc5b 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -153,7 +153,7 @@ def verify_diff_parsed(self): self.parsed = True return self - def log(self, message, level="INFO", frameIncrement=0): + def log(self, message, level="WARNING", frameIncrement=0): """Logs formatted messages with specified log level and incrementing the call stack frame Args: self (obj, required): An instance of the DnacBase Class. @@ -165,26 +165,52 @@ def log(self, message, level="INFO", frameIncrement=0): global _first_log_written if _first_log_written is False: - LogConfig(self.dnac_log_level, self.dnac_log_file_path) + self.validate_dnac_log_level() + self.validate_dnac_log_file_path() - self.is_valid_level(message, level) + self.validate_level(message, level) level = level.upper() + self.dnac_log_level = self.dnac_log_level.upper() if ( self.dnac_log - and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level.upper()) + and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level) ): message = "Module: " + self.__class__.__name__ + ", " + message - log(message, level, self.dnac_log_file_path, self.dnac_append_logs, (1 + frameIncrement)) - _first_log_written = True - - def is_valid_level(self, message, level): + mode = 'w' if not _first_log_written and not self.dnac_append_logs else 'a' + with open(self.dnac_log_file_path, mode) as of: + callerframerecord = inspect.stack()[frameIncrement] + frame = callerframerecord[0] + info = inspect.getframeinfo(frame) + current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() + of.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level, message)) + _first_log_written = True + + def validate_level(self, message, level): """Validates if the specified log level is a string and one of the expected values""" if not isinstance(level, str): raise ValueError("Invalid log level type passed when logging the following msg: {0} level:{1}. Expected a string.".format(message, level)) if level.upper() not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): raise ValueError("Invalid log level passed when logging the following msg: {0} level:{1}.".format(message, level)) + def validate_dnac_log_level(self): + """Validates if the logging level is string and of expected value""" + if self.dnac_log_level not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): + raise ValueError("Invalid log level: 'dnac_log_level:{0}'".format(self.dnac_log_level)) + + def validate_dnac_log_file_path(self): + """ + Validates the specified log file path, ensuring it is either absolute or relative, + the directory exists, and has a .log extension. + """ + # Convert the path to absolute if it's relative + dnac_log_file_path = os.path.abspath(self.dnac_log_file_path) + + # Validate if the directory exists + log_directory = os.path.dirname(dnac_log_file_path) + if not os.path.exists(log_directory): + raise FileNotFoundError("The directory for log file '{0}' does not exist.".format(dnac_log_file_path)) + def check_return_status(self): """API to check the return status value and exit/fail the module""" @@ -423,49 +449,6 @@ def camel_to_snake_case(self, config): return new_config -class LogConfig(): - """Configuration class for validating logging parameters""" - - def __init__(self, dnac_log_level='INFO', dnac_log_file_path='dnac.log'): - self.valid_log_levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') - self.dnac_log_level = dnac_log_level.upper() - self.dnac_log_file_path = dnac_log_file_path - self.validate_dnac_log_level() - self.validate_dnac_log_file_path() - - def validate_dnac_log_level(self): - """Validates if the logging level is string and of expected value""" - if self.dnac_log_level not in self.valid_log_levels: - raise ValueError("Invalid log level: 'dnac_log_level:{0}'. Expected one of {1}.".format(self.dnac_log_level, self.valid_log_levels)) - - def validate_dnac_log_file_path(self): - """ - Validates the specified log file path, ensuring it is either absolute or relative, - the directory exists, and has a .log extension. - """ - # Convert the path to absolute if it's relative - dnac_log_file_path = os.path.abspath(self.dnac_log_file_path) - - # Validate if the directory exists - log_directory = os.path.dirname(dnac_log_file_path) - if not os.path.exists(log_directory): - raise FileNotFoundError("The directory for log file '{0}' does not exist.".format(dnac_log_file_path)) - - -def log(message, level='INFO', dnac_log_file_path='dnac.log', dnac_append_logs=True, frameIncrement=0): - """Writes/Appends logs to the specified log file""" - if _first_log_written is False and dnac_append_logs is False: - mode = 'w' - else: - mode = 'a' - with open(dnac_log_file_path, mode) as of: - callerframerecord = inspect.stack()[1 + frameIncrement] - frame = callerframerecord[0] - info = inspect.getframeinfo(frame) - current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() - of.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level, message)) - - def is_list_complex(x): return isinstance(x[0], dict) or isinstance(x[0], list) From d576fdedeee8962e6bda039257b8d7ce016bc810 Mon Sep 17 00:00:00 2001 From: SHUBHAM VARFA Date: Tue, 30 Jan 2024 04:45:35 +0000 Subject: [PATCH 28/76] Adding DNAC log levels for Discovery and PnP --- plugins/modules/discovery_intent.py | 78 ++++++++++++------ plugins/modules/pnp_intent.py | 121 ++++++++++++++++++++-------- 2 files changed, 140 insertions(+), 59 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 5ed392921d..dea4d8620d 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +\#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2022, Cisco Systems @@ -26,6 +26,11 @@ description: Set to True to verify the Cisco DNA Center config after applying the playbook config. type: bool default: False + dnac_log_level: + description: Specifies the log level for Cisco Catalyst Center logging, categorizing logs by severity. + Options- [CRITICAL, ERROR, WARNING, INFO, DEBUG] + type: str + default: WARNING state: description: The state of DNAC after module completion. type: str @@ -179,6 +184,7 @@ dnac_version: "{{dnac_version}}" dnac_debug: "{{dnac_debug}}" dnac_log: True + dnac_log_level: "{{dnac_log_level}}" state: merged config: - devices_list: @@ -190,8 +196,8 @@ start_index: integer enable_password_list: list records_to_return: integer - http_read_credential: string - http_write_credential: string + http_read_credential: dict + http_write_credential: dict ip_filter_list: list discovery_name: string password_list: list @@ -373,13 +379,13 @@ def validate_input(self): if invalid_params: self.msg = "Invalid parameters in playbook: {0}".format( "\n".join(invalid_params)) + self.log(str(self.msg), "ERROR") self.status = "failed" return self self.validated_config = valid_discovery - self.log(str(valid_discovery)) - - self.msg = "Successfully validated input" + self.msg = "Successfully validated playbook configuration parameters using 'validate_input': {0}".format(str(valid_discovery)) + self.log(str(self.msg), "INFO") self.status = "success" return self @@ -393,6 +399,7 @@ def get_creds_ids_list(self): the class instance. """ + self.log("Credential IDs list is {0}".format(str(self.creds_ids_list)), "INFO") return self.creds_ids_list def get_dnac_global_credentials_v2_info(self): @@ -415,14 +422,15 @@ def get_dnac_global_credentials_v2_info(self): params=self.validated_config[0].get('headers'), ) response = response.get('response') - self.log(response) + self.log("The Global credentials response from 'get all global credentials v2' API is {0}".format(str(response)), "DEBUG") for value in response.values(): if not value: continue self.creds_ids_list.extend(element.get('id') for element in value) if not self.creds_ids_list: - msg = 'Not found any credentials to discover' + msg = 'Not found any credentials to perform discovery' + self.log(msg, "CRITICAL") self.module.fail_json(msg=msg) self.result.update(dict(credential_ids=self.creds_ids_list)) @@ -438,6 +446,7 @@ def get_devices_list_info(self): """ devices_list = self.validated_config[0].get('devices_list') self.result.update(dict(devices_info=devices_list)) + self.log("Devices list info passed is {0}".format(str(devices_list)), "INFO") return devices_list def preprocessing_devices_info(self, devices_list=None): @@ -461,16 +470,19 @@ def preprocessing_devices_info(self, devices_list=None): ip_address_list = [device['ip'] for device in devices_list] + self.log("Discovery type passed for the discovery is {0}".format(self.validated_config[0].get('discovery_type')), "INFO") if self.validated_config[0].get('discovery_type') in ["SINGLE", "CDP", "LLDP"]: if len(ip_address_list) == 1: ip_address_list = ip_address_list[0] else: + self.log("Device list's length is longer than 1", "ERROR") self.module.fail_json(msg="Device list's length is longer than 1", response=[]) elif self.validated_config[0].get('discovery_type') == "CIDR": if len(ip_address_list) == 1 and self.validated_config[0].get('prefix_length'): ip_address_list = ip_address_list[0] ip_address_list = str(ip_address_list) + "/" + str(self.validated_config[0].get('prefix_length')) else: + self.log("Device list's length is longer than 1", "ERROR") self.module.fail_json(msg="Device list's length is longer than 1", response=[]) else: ip_address_list = list( @@ -481,7 +493,7 @@ def preprocessing_devices_info(self, devices_list=None): ) ip_address_list = ','.join(ip_address_list) - self.log("Collected IP address/addresses are {0}".format(ip_address_list)) + self.log("Collected IP address/addresses are {0}".format(str(ip_address_list)), "INFO") return ip_address_list def create_params(self, credential_ids=None, ip_address_list=None): @@ -545,7 +557,7 @@ def create_params(self, credential_ids=None, ip_address_list=None): new_object_params['snmpVersion'] = self.validated_config[0].get('snmp_version') new_object_params['timeout'] = self.validated_config[0].get('timeout') new_object_params['userNameList'] = self.validated_config[0].get('user_name_list') - self.log(new_object_params) + self.log("The payload/object created for calling the start discovery API is {0}".format(str(new_object_params)), "INFO") return new_object_params @@ -577,9 +589,10 @@ def create_discovery(self, credential_ids=None, ip_address_list=None): op_modifies=True, ) - self.log(result) + self.log("The response received post discovery creation API called is {0}".format(str(result)), "DEBUG") self.result.update(dict(discovery_result=result)) + self.log("Task Id of the API task created is {0}".format(result.response.get('taskId')), "INFO") return result.response.get('taskId') def get_task_status(self, task_id=None): @@ -605,17 +618,19 @@ def get_task_status(self, task_id=None): params=params, ) response = response.response - self.log(response) + self.log("Task status for the task id {0} is {1}".format(str(task_id), str(response)), "INFO") if response.get('isError') or re.search( 'failed', response.get('progress'), flags=re.IGNORECASE ): msg = 'Discovery task with id {0} has not completed - Reason: {1}'.format( task_id, response.get("failureReason")) + self.log(msg, "CRITICAL") self.module.fail_json(msg=msg) return False if response.get('progress') != 'In Progress': result = True + self.log("The Process is completed", "INFO") break time.sleep(3) @@ -644,7 +659,7 @@ def lookup_discovery_by_range_via_name(self): params=params ) - self.log(response) + self.log("Response of the get discoveries via range API is {0}".format(str(response)), "DEBUG") return next( filter( @@ -670,8 +685,8 @@ def get_discoveries_by_range_until_success(self): if not discovery: msg = 'Cannot find any discovery task with name {0} -- Discovery result: {1}'.format( - self.validated_config[0].get("discovery_name"), discovery) - self.log(msg) + str(self.validated_config[0].get("discovery_name")), str(discovery)) + self.log(msg, "INFO") self.module.fail_json(msg=msg) while True: @@ -684,8 +699,8 @@ def get_discoveries_by_range_until_success(self): if not result: msg = 'Cannot find any discovery task with name {0} -- Discovery result: {1}'.format( - self.validated_config[0].get("discovery_name"), discovery) - self.log(msg) + str(self.validated_config[0].get("discovery_name")), str(discovery)) + self.log(msg, "CRITICAL") self.module.fail_json(msg=msg) self.result.update(dict(discovery_range=discovery)) @@ -721,7 +736,7 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): ) devices = response.response - self.log(devices) + self.log("Devices Information retrieved from the get discovered network devices by discovery id API is {0}".format(str(devices)), "DEBUG") if all(res.get('reachabilityStatus') == 'Success' for res in devices): result = True break @@ -734,8 +749,10 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): if not result: msg = 'Discovery network device with id {0} has not completed'.format(discovery_id) + self.log(msg, "CRITICAL") self.module.fail_json(msg=msg) + self.log('Discovery network device with id {0} has not completed'.format(discovery_id), "INFO") self.result.update(dict(discovery_device_info=devices)) return result @@ -777,8 +794,9 @@ def delete_exist_discovery(self, params): params=params, ) - self.log(response) + self.log("Response collected from delete discovery by id API is {0}".format(str(response)), "DEBUG") self.result.update(dict(delete_discovery=response)) + self.log("Task Id of the delteion task is {0}".format(response.response.get('taskId')), "INFO") return response.response.get('taskId') def get_diff_merged(self): @@ -812,6 +830,7 @@ def get_diff_merged(self): self.result['diff'] = self.validated_config self.result['response'] = discovery_task_id self.result.update(dict(msg='Discovery Created Successfully')) + self.log(self.result['msg'], "INFO") return self def get_diff_deleted(self): @@ -829,6 +848,7 @@ def get_diff_deleted(self): if not exist_discovery: self.result['msg'] = "Discovery {0} Not Found".format( self.validated_config[0].get("discovery_name")) + self.log(self.result['msg'], "ERROR") return self params = dict(id=exist_discovery.get('id')) @@ -838,7 +858,7 @@ def get_diff_deleted(self): self.result['msg'] = "Discovery Deleted Successfully" self.result['diff'] = self.validated_config self.result['response'] = discovery_task_id - + self.log(self.result['msg'], "INFO") return self def verify_diff_merged(self, config): @@ -856,7 +876,8 @@ def verify_diff_merged(self, config): Center configuration's Discovery Database. """ - self.log(str(self.have)) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(config)), "INFO") # Code to validate dnac config for merged state discovery_task_info = self.get_discoveries_by_range_until_success() discovery_id = discovery_task_info.get('id') @@ -871,10 +892,10 @@ def verify_diff_merged(self, config): if response: discovery_name = response.get('response').get('name') - self.log("Requested Discovery with name {0} is completed".format(discovery_name)) + self.log("Requested Discovery with name {0} is completed".format(discovery_name), "INFO") else: - self.log("Requested Discovery with name {0} is not completed".format(discovery_name)) + self.log("Requested Discovery with name {0} is not completed".format(discovery_name), "WARNING") self.status = "success" return self @@ -893,7 +914,8 @@ def verify_diff_deleted(self, config): Discovery Database. """ - self.log(str(self.have)) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(config)), "INFO") # Code to validate dnac config for deleted state discovery_task_info = self.get_discoveries_by_range_until_success() discovery_id = discovery_task_info.get('id') @@ -908,10 +930,10 @@ def verify_diff_deleted(self, config): if response: discovery_name = response.get('response').get('name') - self.log("Requested Discovery with name {0} is present".format(discovery_name)) + self.log("Requested Discovery with name {0} is present".format(discovery_name), "WARNING") else: - self.log("Requested Discovery with name {0} is not present and deleted".format(discovery_name)) + self.log("Requested Discovery with name {0} is not present and deleted".format(discovery_name), "INFO") self.status = "success" return self @@ -929,6 +951,7 @@ def main(): 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config_verify': {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, @@ -949,7 +972,10 @@ def main(): dnac_discovery.validate_input().check_return_status() for config in dnac_discovery.validated_config: + dnac_discovery.reset_values() dnac_discovery.get_diff_state_apply[state]().check_return_status() + if config_verify: + dnac_discovery.verify_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_discovery.result) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 95bedf6404..a6890a1809 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +\#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2022, Cisco Systems @@ -28,6 +28,11 @@ description: Set to True to verify the Cisco DNA Center config after applying the playbook config. type: bool default: False + dnac_log_level: + description: Specifies the log level for Cisco Catalyst Center logging, categorizing logs by severity. + Options- [CRITICAL, ERROR, WARNING, INFO, DEBUG] + type: str + default: WARNING state: description: The state of DNAC after module completion. type: str @@ -149,6 +154,7 @@ dnac_port: "{{dnac_port}}" dnac_version: "{{dnac_version}}" dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" dnac_log: True state: merged config_verify: True @@ -280,13 +286,12 @@ def validate_input(self): if invalid_params: self.msg = "Invalid parameters in playbook: {0}".format( "\n".join(invalid_params)) + self.log(str(self.msg), "ERROR") self.status = "failed" return self - self.validated_config = valid_pnp - self.log(str(valid_pnp)) - - self.msg = "Successfully validated input" + self.msg = "Successfully validated playbook config params: {0}".format(str(valid_pnp)) + self.log(str(self.msg), "INFO") self.status = "success" return self @@ -318,14 +323,16 @@ def get_site_details(self): params={"name": self.want.get("site_name")}, ) except Exception: + self.log("Site {0} not found".format(self.want.get("site_name")), "CRITICAL") self.module.fail_json(msg="Site not found", response=[]) if response: - self.log(str(response)) + self.log("Site details of {0} received are {1}".format(self.want.get("site_name"), str(response)), "DEBUG") site = response.get("response") if len(site) == 1: site_id = site[0].get("id") site_exists = True + self.log("Site Id is {0} and site name is {1}".format(site_id, self.want.get("site_name")), "INFO") return (site_exists, site_id) @@ -352,15 +359,17 @@ def get_site_type(self): params={"name": self.want.get("site_name")}, ) except Exception: + self.log("Site {0} not found".format(self.want.get("site_name")), "CRITICAL") self.module.fail_json(msg="Site not found", response=[]) if response: - self.log(str(response)) + self.log("Site details of {0} received are {1}".format(self.want.get("site_name"), str(response)), "DEBUG") site = response.get("response") site_additional_info = site[0].get("additionalInfo") for item in site_additional_info: if item["nameSpace"] == "Location": site_type = item.get("attributes").get("type") + self.log("Site Type is {0}".format(site_type), "INFO") return site_type @@ -392,6 +401,7 @@ def get_pnp_params(self, params): device_dict["deviceInfo"] = param device_info_list.append(device_dict) + self.log("PnP paramters passed are {0}".format(str(params_list)), "INFO") return device_info_list def get_image_params(self, params): @@ -416,6 +426,8 @@ def get_image_params(self, params): 'image_name': params.get('image_name'), 'is_tagged_golden': params.get('golden_image') } + + self.log("Image details are {0}".format(str(image_params)), "INFO") return image_params def get_claim_params(self): @@ -479,6 +491,7 @@ def get_claim_params(self): claim_params["type"] = "AccessPoint" claim_params["rfProfile"] = self.validated_config[0]["rf_profile"] + self.log("Paramters used for claiming are {0}".format(str(claim_params)), "INFO") return claim_params def get_reset_params(self): @@ -517,6 +530,7 @@ def get_reset_params(self): ] } + self.log("Paramters used for resetting from deleted state are {0}".format(str(reset_params)), "INFO") return reset_params def get_have(self): @@ -545,9 +559,10 @@ def get_have(self): function='get_device_list', params={"serial_number": self.want.get("serial_number")} ) - self.log(str(device_response)) + self.log("Device details of the device with serial number {0} are {1}".format(self.want.get("serial_number"), str(device_response)), "DEBUG") if not (device_response and (len(device_response) == 1)): + self.log("Device with with serial number {0} is not found in the inventory".format(self.want.get("serial_number")), "WARNING") self.msg = "Adding the device to database" self.status = "success" self.have = have @@ -566,7 +581,7 @@ def get_have(self): params=self.want.get("image_params"), ) image_list = image_response.get("response") - self.log(str(image_response)) + self.log("Image details obtained from the API 'get software image details' are {0}".format(str(image_response)), "DEBUG") # check if project has templates or not template_list = self.dnac_apply['exec']( @@ -574,21 +589,23 @@ def get_have(self): function='gets_the_templates_available', params={"project_names": self.want.get("project_name")}, ) - self.log(str(template_list)) + self.log("Templates list under the project {0} is {1}".format(self.want.get("project_name"), str(template_list)), "DEBUG") dev_details_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function="get_device_by_id", params={"id": device_response[0].get("id")} ) - + self.log("Device details of the existing device after calling the API 'get device by id' are {0}".format(str(dev_details_response)), "DEBUG") install_mode = dev_details_response.get("deviceInfo").get("mode") + self.log("Installation mode of the device with the serial no. {0} is {1}".format(self.want.get("serial_number"), install_mode), "INFO") # check if given site exits, if exists store current site info site_exists = False if not isinstance(self.want.get("site_name"), str) and \ not self.want.get('pnp_params')[0].get('deviceInfo'): self.msg = "Name of the site must be a string" + self.log(str(self.msg), "ERROR") self.status = "failed" return self @@ -597,12 +614,13 @@ def get_have(self): if site_exists: have["site_id"] = site_id - self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) - self.log("Site Name:" + str(site_name)) + self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id), "INFO") + self.log("Site Name:" + str(site_name), "INFO") if self.want.get("pnp_type") == "access_point": if self.get_site_type() != "floor": self.msg = "Type of the site must \ be a floor for claiming an AP" + self.log(str(self.msg), "ERROR") self.status = "failed" return self @@ -611,17 +629,19 @@ def get_have(self): self.msg = "Installation mode must be in \ INSTALL mode to upgrade the image. Current mode is\ {0}".format(install_mode) + self.log(str(self.msg), "CRITICAL") self.status = "failed" return self have["image_id"] = image_list[0].get("imageUuid") - self.log("Image Id: " + str(have["image_id"])) + self.log("Image Id is {0}".format(str(have["image_id"])), "INFO") template_name = self.want.get("template_name") if template_name: if not (template_list and isinstance(template_list, list)): self.msg = "Project Not Found \ or Project is Empty" + self.log(self.msg, "CRITICAL") self.status = "failed" return self @@ -629,18 +649,21 @@ def get_have(self): if template_details: have["template_id"] = template_details.get("templateId") else: - self.msg = "Template Not found" + self.msg = "Template {0} Not found".format(template_name) + self.log(self.msg, "CRITICAL") self.status = "failed" return self else: if not self.want.get('pnp_params')[0].get('deviceInfo'): self.msg = "Either Site Name or Device details must be added" + self.log(self.msg, "ERROR") self.status = "failed" return self self.msg = "Successfully collected all project and template \ parameters from dnac for comparison" + self.log(self.msg, "INFO") self.status = "success" self.have = have return self @@ -694,6 +717,7 @@ def get_want(self, config): self.want["rf_profile"] = config.get("rf_profile") self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" + self.log(self.msg, "INFO") self.status = "success" return self @@ -717,6 +741,7 @@ class instance for further use. if not isinstance(self.want.get("pnp_params"), list): self.msg = "Device Info must be passed as a list" + self.log(self.msg, "ERROR") self.status = "failed" return self @@ -728,13 +753,16 @@ class instance for further use. function='get_device_list', params={"serial_number": device["deviceInfo"]["serialNumber"]} ) - + self.log("Device details for the serial number {0}\ + obtained from the API 'get device list'\ + are {1}".format(device["deviceInfo"]["serialNumber"], str(multi_device_response)), "DEBUG") if (multi_device_response and (len(multi_device_response) == 1)): devices_added.append(device) - + self.log("Details of the added device is {0}".format(str(device)), "INFO") if (len(self.want.get("pnp_params")) - len(devices_added)) == 0: self.result['response'] = [] self.result['msg'] = "Devices are already added" + self.log(self.result['msg'], "WARNING") return self bulk_list = [ @@ -748,15 +776,18 @@ class instance for further use. params={"payload": bulk_list}, op_modifies=True, ) + self.log("Devices imported response from the API 'import_devices_in_bulk' is {0}".format(bulk_params), "DEBUG") if len(bulk_params.get("successList")) > 0: 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 return self self.msg = "Bulk import failed" + self.log(self.msg, "CRITICAL") self.status = "failed" return self @@ -773,11 +804,12 @@ class instance for further use. if not self.have.get("device_found"): if not self.want['pnp_params']: self.msg = "Device needs to be added before claiming. Please add device_info" + self.log(self.msg, "ERROR") self.status = "failed" return self if not self.want["site_name"]: - self.log("Adding device to pnp database") + self.log("Adding device to pnp database", "INFO") dev_add_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function="add_device", @@ -786,15 +818,17 @@ class instance for further use. ) self.have["deviceInfo"] = dev_add_response.get("deviceInfo") - self.log(str(dev_add_response)) + self.log("Single device addition response from the API 'add device' is {0}".format(str(dev_add_response)), "DEBUG") if self.have["deviceInfo"]: self.result['msg'] = "Only Device Added Successfully" + self.log(self.result['msg'], "INFO") self.result['response'] = dev_add_response self.result['diff'] = self.validated_config self.result['changed'] = True else: self.msg = "Device Addition Failed" + self.log(self.result['msg'], "CRITICAL") self.status = "failed" return self @@ -808,7 +842,7 @@ class instance for further use. op_modifies=True, ) self.have["deviceInfo"] = dev_add_response.get("deviceInfo") - self.log(str(dev_add_response)) + self.log("Single device addition response from the API 'add device'is {0}".format(str(dev_add_response)), "DEBUG") claim_params = self.get_claim_params() claim_params["deviceId"] = dev_add_response.get("id") claim_response = self.dnac_apply['exec']( @@ -818,16 +852,18 @@ class instance for further use. params=claim_params, ) - self.log(str(claim_response)) + self.log("Single claiming response from the API 'claim a device to a site' is {0}".format(str(dev_add_response)), "DEBUG") if claim_response.get("response") == "Device Claimed" \ and self.have["deviceInfo"]: self.result['msg'] = "Device Added and Claimed Successfully" + self.log(self.result['msg'], "INFO") self.result['response'] = claim_response self.result['diff'] = self.validated_config self.result['changed'] = True else: self.msg = "Device Claim Failed" + self.log(self.result['msg'], "CRITICAL") self.status = "failed" return self @@ -838,23 +874,30 @@ class instance for further use. op_modifies=True, params=provisioned_count_params, ) + self.log("Devices which are provisioned response from the API 'get device count' is {0}".format(str(prov_dev_response)), "DEBUG") + plan_dev_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function='get_device_count', op_modifies=True, params=planned_count_params, ) + self.log("Devices which are in planned state response from the API 'get device count' is {0}".format(str(plan_dev_response)), "DEBUG") + dev_details_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function="get_device_by_id", params={"id": self.have["device_id"]} ) + self.log("Device's details from the API 'get device by id' is {0}".format(str(dev_details_response)), "DEBUG") pnp_state = dev_details_response.get("deviceInfo").get("state") + self.log("PnP state of the device is {0}".format(pnp_state), "INFO") if not self.want["site_name"]: self.result['response'] = self.have.get("device_found") self.result['msg'] = "Device is already added" + self.log(self.result['msg'], "WARNING") return self update_payload = {"deviceInfo": self.want.get('pnp_params')[0].get("deviceInfo")} @@ -865,7 +908,7 @@ class instance for further use. "payload": update_payload}, op_modifies=True, ) - self.log(str(update_response)) + self.log("Update in the device's config API response is {0}".format(str(update_response)), "DEBUG") if pnp_state == "Error": reset_paramters = self.get_reset_params() @@ -875,8 +918,9 @@ class instance for further use. params={"payload": reset_paramters}, op_modifies=True, ) - self.log(str(reset_response)) + self.log("Errored state resolution response from the API is {0}".format(str(reset_response)), "DEBUG") self.result['msg'] = "Device reset done Successfully" + self.log(self.result['msg'], "INFO") self.result['response'] = reset_response self.result['diff'] = self.validated_config self.result['changed'] = True @@ -888,12 +932,13 @@ class instance for further use. ): self.result['response'] = self.have.get("device_found") self.result['msg'] = "Device is already claimed" + self.log(self.result['msg'], "WARNING") if update_response.get("deviceInfo"): self.result['changed'] = True return self claim_params = self.get_claim_params() - self.log(str(claim_params)) + self.log("Paramters for claiming the device are {0}".format(str(claim_params)), "DEBUG") claim_response = self.dnac_apply['exec']( family="device_onboarding_pnp", @@ -901,9 +946,10 @@ class instance for further use. op_modifies=True, params=claim_params, ) - self.log(str(claim_response)) + self.log("Claiming response obtained from the API 'claim a device to a site' is {0}".format(str(claim_response)), "DEBUG") if claim_response.get("response") == "Device Claimed": self.result['msg'] = "Only Device Claimed Successfully" + self.log(self.result['msg'], "INFO") self.result['response'] = claim_response self.result['diff'] = self.validated_config self.result['changed'] = True @@ -934,7 +980,7 @@ def get_diff_deleted(self): function='get_device_list', params={"serial_number": device["deviceInfo"]["serialNumber"]} ) - + self.log("Device details for the serial number {0} are {1}".format(device["deviceInfo"]["serialNumber"], str(multi_device_response)), "DEBUG") if multi_device_response and len(multi_device_response) == 1: device_id = multi_device_response[0].get("id") @@ -945,7 +991,8 @@ def get_diff_deleted(self): params={"id": device_id}, ) - self.log(str(response)) + self.log("Device details for the device deleted with \ + serial number {0} are {1}".format(device["deviceInfo"]["serialNumber"], str(response)), "DEBUG") if response.get("deviceInfo", {}).get("state") == "Deleted": devices_deleted.append(device["deviceInfo"]["serialNumber"]) @@ -953,14 +1000,17 @@ def get_diff_deleted(self): else: self.result['response'] = response self.result['msg'] = "Error while deleting the device" + self.log(self.result['msg'], "CRITICAL") if len(devices_deleted) > 0: self.result['changed'] = True self.result['response'] = devices_deleted self.result['diff'] = self.want.get("pnp_params") self.result['msg'] = "{0} Device(s) Deleted Successfully".format(len(devices_deleted)) + self.log(self.result['msg'], "INFO") else: self.result['msg'] = "Device(s) Not Found" + self.log(self.result['msg'], "WARNING") self.result['response'] = devices_deleted return self @@ -980,8 +1030,8 @@ def verify_diff_merged(self, config): Center configuration's PnP Database. """ - self.log("Current State (have): {0}".format(self.have)) - self.log("Desired State (want): {0}".format(self.want)) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(config)), "INFO") # Code to validate dnac config for merged state for device in self.want.get("pnp_params"): device_response = self.dnac_apply['exec']( @@ -994,12 +1044,14 @@ def verify_diff_merged(self, config): "Requested Device with Serial No. {0} is " "present in Cisco DNA Center and" " addition verified.".format(device["deviceInfo"]["serialNumber"])) - self.log(msg) + self.log(msg, "INFO") + else: msg = ( "Requested Device with Serial No. {0} is " "not present in Cisco DNA " "Center".format(device["deviceInfo"]["serialNumber"])) + self.log(msg, "WARNING") self.status = "success" return self @@ -1018,8 +1070,8 @@ def verify_diff_deleted(self, config): PnP Database. """ - self.log("Current State (have): {0}".format(self.have)) - self.log("Desired State (want): {0}".format(self.want)) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(config)), "INFO") # Code to validate dnac config for deleted state for device in self.want.get("pnp_params"): device_response = self.dnac_apply['exec']( @@ -1032,11 +1084,13 @@ def verify_diff_deleted(self, config): "Requested Device with Serial No. {0} is " "not present in the Cisco DNA" "Center.".format(device["deviceInfo"]["serialNumber"])) - self.log(msg) + self.log(msg, "INFO") + else: msg = ( "Requested Device with Serial No. {0} is " "present in Cisco DNA Center".format(device["deviceInfo"]["serialNumber"])) + self.log(msg, "WARNING") self.status = "success" return self @@ -1055,6 +1109,7 @@ def main(): 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config_verify': {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, From 6e5ab2bf587fe65ef38d071f9bb9815fe07b7b15 Mon Sep 17 00:00:00 2001 From: SHUBHAM VARFA Date: Tue, 30 Jan 2024 04:58:11 +0000 Subject: [PATCH 29/76] Adding DNAC log levels for Discovery and PnP --- plugins/modules/discovery_intent.py | 2 +- plugins/modules/pnp_intent.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index dea4d8620d..a0eb483c30 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -1,4 +1,4 @@ -\#!/usr/bin/python +#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2022, Cisco Systems diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index a6890a1809..d088afe259 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -1,4 +1,4 @@ -\#!/usr/bin/python +#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2022, Cisco Systems From 1b85d6dcb80607e6d0e68c1b844e08ca2a54e009 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 30 Jan 2024 11:30:28 +0530 Subject: [PATCH 30/76] Handle the case of dump interface name while updating the interface details, Address the type error of 'Parameter', Handle the code for edit device common info --- plugins/modules/inventory_intent.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 32eb98c0de..3bf70e3cf9 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -1097,7 +1097,7 @@ def export_device_details(self): "deviceUuids": device_uuids, "password": password, "operationEnum": export_device_list.get("operation_enum", "0"), - "paramters": export_device_list.get("paramters") + "parameters": export_device_list.get("parameters") } response = self.trigger_export_api(payload_params) @@ -2025,6 +2025,7 @@ def get_interface_from_id_and_name(self, device_id, interface_name): response = response.get("response") if response: + self.status = "success" interface_id = response["id"] self.log("""Successfully fetched interface ID ({0}) by using device id {1} and interface name {2}.""" .format(interface_id, device_id, interface_name), "INFO") @@ -2033,7 +2034,9 @@ def get_interface_from_id_and_name(self, device_id, interface_name): except Exception as e: error_message = "Error while fetching interface id for interface({0}) from Cisco Catalyst Center: {1}".format(interface_name, str(e)) self.log(error_message, "ERROR") - raise Exception(error_message) + self.msg = error_message + self.status = "failed" + return self def get_interface_from_ip(self, device_ip): """ @@ -2402,6 +2405,16 @@ def get_diff_merged(self, config): } protocol_type = playbook_params['snmpPrivProtocol'] playbook_params['snmpPrivProtocol'] = snmp_protocol_mapping[protocol_type] + + if playbook_params['snmpMode'] == "NOAUTHNOPRIV": + playbook_params.pop('snmpAuthPassphrase', None) + playbook_params.pop('snmpPrivPassphrase', None) + playbook_params.pop('snmpPrivProtocol', None) + playbook_params.pop('snmpAuthProtocol', None) + elif playbook_params['snmpMode'] == "AUTHNOPRIV": + playbook_params.pop('snmpPrivPassphrase', None) + playbook_params.pop('snmpPrivProtocol', None) + try: response = self.dnac._exec( family="devices", @@ -2450,6 +2463,7 @@ def get_diff_merged(self, config): interface_name = interface_params.get('interface_name') device_id = self.get_device_ids([device_ip]) interface_id = self.get_interface_from_id_and_name(device_id[0], interface_name) + self.check_return_status() # Now we call update interface details api with required parameter try: From e0ea781d0a416f39835a6644e298d5efd6a03feb Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 30 Jan 2024 13:55:21 +0530 Subject: [PATCH 31/76] update keyword in examples for device_updated in inventory module --- plugins/modules/inventory_intent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 3bf70e3cf9..29ea3c9511 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -438,7 +438,7 @@ snmp_username: string snmp_version: string type: string - device_update: true + device_updated: true credential_update: true update_mgmt_ipaddresslist: - exist_mgmt_ipaddress: string From 7bc0b0eb47b7f46346f151a4199fe73b6d4bac6d Mon Sep 17 00:00:00 2001 From: SHUBHAM VARFA Date: Tue, 30 Jan 2024 08:25:59 +0000 Subject: [PATCH 32/76] Adding DNAC log levels for Discovery and PnP --- plugins/modules/discovery_intent.py | 8 +-- plugins/modules/pnp_intent.py | 85 +++++++++++++++-------------- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index a0eb483c30..e43000d47e 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -736,7 +736,7 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): ) devices = response.response - self.log("Devices Information retrieved from the get discovered network devices by discovery id API is {0}".format(str(devices)), "DEBUG") + self.log("Retrieved device details using the API 'get_discovered_network_devices_by_discovery_id': {0}".format(str(devices)), "DEBUG") if all(res.get('reachabilityStatus') == 'Success' for res in devices): result = True break @@ -752,7 +752,7 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): self.log(msg, "CRITICAL") self.module.fail_json(msg=msg) - self.log('Discovery network device with id {0} has not completed'.format(discovery_id), "INFO") + self.log('Discovery network device with id {0} got completed'.format(discovery_id), "INFO") self.result.update(dict(discovery_device_info=devices)) return result @@ -794,9 +794,9 @@ def delete_exist_discovery(self, params): params=params, ) - self.log("Response collected from delete discovery by id API is {0}".format(str(response)), "DEBUG") + self.log("Response collected from API 'delete_discovery_by_id': {0}".format(str(response)), "DEBUG") self.result.update(dict(delete_discovery=response)) - self.log("Task Id of the delteion task is {0}".format(response.response.get('taskId')), "INFO") + self.log("Task Id of the deletion task is {0}".format(response.response.get('taskId')), "INFO") return response.response.get('taskId') def get_diff_merged(self): diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index d088afe259..18b06a3a23 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -323,16 +323,18 @@ def get_site_details(self): params={"name": self.want.get("site_name")}, ) except Exception: - self.log("Site {0} not found".format(self.want.get("site_name")), "CRITICAL") + self.log("Exception occurred as site \ + '{0}' was not found".format(self.want.get("site_name")), "CRITICAL") self.module.fail_json(msg="Site not found", response=[]) if response: - self.log("Site details of {0} received are {1}".format(self.want.get("site_name"), str(response)), "DEBUG") + self.log("Received site details \ + for '{0}': {1}".format(self.want.get("site_name"), str(response)), "DEBUG") site = response.get("response") if len(site) == 1: site_id = site[0].get("id") site_exists = True - self.log("Site Id is {0} and site name is {1}".format(site_id, self.want.get("site_name")), "INFO") + self.log("Site Name: {1}, Site ID: {0}".format(site_id, self.want.get("site_name")), "INFO") return (site_exists, site_id) @@ -359,17 +361,19 @@ def get_site_type(self): params={"name": self.want.get("site_name")}, ) except Exception: - self.log("Site {0} not found".format(self.want.get("site_name")), "CRITICAL") + self.log("Exception occurred as \ + site '{0}' was not found".format(self.want.get("site_name")), "CRITICAL") self.module.fail_json(msg="Site not found", response=[]) if response: - self.log("Site details of {0} received are {1}".format(self.want.get("site_name"), str(response)), "DEBUG") + self.log("Received site details\ + for '{0}': {1}".format(self.want.get("site_name"), str(response)), "DEBUG") site = response.get("response") site_additional_info = site[0].get("additionalInfo") for item in site_additional_info: if item["nameSpace"] == "Location": site_type = item.get("attributes").get("type") - self.log("Site Type is {0}".format(site_type), "INFO") + self.log("Site type for site name '{1}' : {0}".format(site_type, self.want.get("site_name")), "INFO") return site_type @@ -530,7 +534,7 @@ def get_reset_params(self): ] } - self.log("Paramters used for resetting from deleted state are {0}".format(str(reset_params)), "INFO") + self.log("Paramters used for resetting from errored state:{0}".format(str(reset_params)), "INFO") return reset_params def get_have(self): @@ -559,7 +563,8 @@ def get_have(self): function='get_device_list', params={"serial_number": self.want.get("serial_number")} ) - self.log("Device details of the device with serial number {0} are {1}".format(self.want.get("serial_number"), str(device_response)), "DEBUG") + self.log("Device details for the device with serial \ + number '{0}': {1}".format(self.want.get("serial_number"), str(device_response)), "DEBUG") if not (device_response and (len(device_response) == 1)): self.log("Device with with serial number {0} is not found in the inventory".format(self.want.get("serial_number")), "WARNING") @@ -581,7 +586,7 @@ def get_have(self): params=self.want.get("image_params"), ) image_list = image_response.get("response") - self.log("Image details obtained from the API 'get software image details' are {0}".format(str(image_response)), "DEBUG") + self.log("Image details obtained from the API 'get_software_image_details': {0}".format(str(image_response)), "DEBUG") # check if project has templates or not template_list = self.dnac_apply['exec']( @@ -589,22 +594,22 @@ def get_have(self): function='gets_the_templates_available', params={"project_names": self.want.get("project_name")}, ) - self.log("Templates list under the project {0} is {1}".format(self.want.get("project_name"), str(template_list)), "DEBUG") + self.log("List of templates under the project '{0}': {1}".format(self.want.get("project_name"), str(template_list)), "DEBUG") dev_details_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function="get_device_by_id", params={"id": device_response[0].get("id")} ) - self.log("Device details of the existing device after calling the API 'get device by id' are {0}".format(str(dev_details_response)), "DEBUG") + self.log("Device details retrieved after calling the 'get_device_by_id' API: {0}".format(str(dev_details_response)), "DEBUG") install_mode = dev_details_response.get("deviceInfo").get("mode") - self.log("Installation mode of the device with the serial no. {0} is {1}".format(self.want.get("serial_number"), install_mode), "INFO") + self.log("Installation mode of the device with the serial no. '{0}':{1}".format(self.want.get("serial_number"), install_mode), "INFO") # check if given site exits, if exists store current site info site_exists = False if not isinstance(self.want.get("site_name"), str) and \ not self.want.get('pnp_params')[0].get('deviceInfo'): - self.msg = "Name of the site must be a string" + self.msg = "The site name must be a string" self.log(str(self.msg), "ERROR") self.status = "failed" return self @@ -614,12 +619,11 @@ def get_have(self): if site_exists: have["site_id"] = site_id - self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id), "INFO") - self.log("Site Name:" + str(site_name), "INFO") + self.log("Site Exists: {0}\nSite Name: {1}\nSite ID: {2}".format(site_exists, site_name, site_id), "INFO") if self.want.get("pnp_type") == "access_point": if self.get_site_type() != "floor": - self.msg = "Type of the site must \ - be a floor for claiming an AP" + self.msg = "The site type must be specified as 'floor'\ + for claiming an AP" self.log(str(self.msg), "ERROR") self.status = "failed" return self @@ -634,13 +638,13 @@ def get_have(self): return self have["image_id"] = image_list[0].get("imageUuid") - self.log("Image Id is {0}".format(str(have["image_id"])), "INFO") + self.log("Image ID for the image '{0}': {1}".format(self.want.get('image_params').get('image_name'), str(have["image_id"])), "INFO") template_name = self.want.get("template_name") if template_name: if not (template_list and isinstance(template_list, list)): - self.msg = "Project Not Found \ - or Project is Empty" + self.msg = "Either project not found \ + or it is Empty" self.log(self.msg, "CRITICAL") self.status = "failed" return self @@ -649,7 +653,7 @@ def get_have(self): if template_details: have["template_id"] = template_details.get("templateId") else: - self.msg = "Template {0} Not found".format(template_name) + self.msg = "Template '{0}' is not found.".format(template_name) self.log(self.msg, "CRITICAL") self.status = "failed" return self @@ -753,12 +757,11 @@ class instance for further use. function='get_device_list', params={"serial_number": device["deviceInfo"]["serialNumber"]} ) - self.log("Device details for the serial number {0}\ - obtained from the API 'get device list'\ - are {1}".format(device["deviceInfo"]["serialNumber"], str(multi_device_response)), "DEBUG") + self.log("Device details for serial number {0} \ + obtained from the API 'get_device_list': {1}".format(device["deviceInfo"]["serialNumber"], str(multi_device_response)), "DEBUG") if (multi_device_response and (len(multi_device_response) == 1)): devices_added.append(device) - self.log("Details of the added device is {0}".format(str(device)), "INFO") + self.log("Details of the added device:{0}".format(str(device)), "INFO") if (len(self.want.get("pnp_params")) - len(devices_added)) == 0: self.result['response'] = [] self.result['msg'] = "Devices are already added" @@ -776,7 +779,7 @@ class instance for further use. params={"payload": bulk_list}, op_modifies=True, ) - self.log("Devices imported response from the API 'import_devices_in_bulk' is {0}".format(bulk_params), "DEBUG") + self.log("Response from API 'import_devices_in_bulk' for imported devices: {0}".format(bulk_params), "DEBUG") if len(bulk_params.get("successList")) > 0: self.result['msg'] = "{0} device(s) imported successfully".format( len(bulk_params.get("successList"))) @@ -818,7 +821,7 @@ class instance for further use. ) self.have["deviceInfo"] = dev_add_response.get("deviceInfo") - self.log("Single device addition response from the API 'add device' is {0}".format(str(dev_add_response)), "DEBUG") + self.log("Response from API 'add device' for a single device addition: {0}".format(str(dev_add_response)), "DEBUG") if self.have["deviceInfo"]: self.result['msg'] = "Only Device Added Successfully" self.log(self.result['msg'], "INFO") @@ -842,7 +845,7 @@ class instance for further use. op_modifies=True, ) self.have["deviceInfo"] = dev_add_response.get("deviceInfo") - self.log("Single device addition response from the API 'add device'is {0}".format(str(dev_add_response)), "DEBUG") + self.log("Response from API 'add device' for single device addition: {0}".format(str(dev_add_response)), "DEBUG") claim_params = self.get_claim_params() claim_params["deviceId"] = dev_add_response.get("id") claim_response = self.dnac_apply['exec']( @@ -852,7 +855,7 @@ class instance for further use. params=claim_params, ) - self.log("Single claiming response from the API 'claim a device to a site' is {0}".format(str(dev_add_response)), "DEBUG") + self.log("Response from API 'claim a device to a site' for a single claiming: {0}".format(str(dev_add_response)), "DEBUG") if claim_response.get("response") == "Device Claimed" \ and self.have["deviceInfo"]: self.result['msg'] = "Device Added and Claimed Successfully" @@ -874,7 +877,7 @@ class instance for further use. op_modifies=True, params=provisioned_count_params, ) - self.log("Devices which are provisioned response from the API 'get device count' is {0}".format(str(prov_dev_response)), "DEBUG") + self.log("Response from 'get device count' API for provisioned devices: {0}".format(str(prov_dev_response)), "DEBUG") plan_dev_response = self.dnac_apply['exec']( family="device_onboarding_pnp", @@ -882,17 +885,17 @@ class instance for further use. op_modifies=True, params=planned_count_params, ) - self.log("Devices which are in planned state response from the API 'get device count' is {0}".format(str(plan_dev_response)), "DEBUG") + self.log("Response from 'get_device_count' API for devices in planned state: {0}".format(str(plan_dev_response)), "DEBUG") dev_details_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function="get_device_by_id", params={"id": self.have["device_id"]} ) - self.log("Device's details from the API 'get device by id' is {0}".format(str(dev_details_response)), "DEBUG") + self.log("Response from 'get_device_by_id' API for device details: {0}".format(str(dev_details_response)), "DEBUG") pnp_state = dev_details_response.get("deviceInfo").get("state") - self.log("PnP state of the device is {0}".format(pnp_state), "INFO") + self.log("PnP state of the device: {0}".format(pnp_state), "INFO") if not self.want["site_name"]: self.result['response'] = self.have.get("device_found") @@ -908,7 +911,7 @@ class instance for further use. "payload": update_payload}, op_modifies=True, ) - self.log("Update in the device's config API response is {0}".format(str(update_response)), "DEBUG") + self.log("Response from 'update_device' API for device's config update: {0}".format(str(update_response)), "DEBUG") if pnp_state == "Error": reset_paramters = self.get_reset_params() @@ -918,7 +921,7 @@ class instance for further use. params={"payload": reset_paramters}, op_modifies=True, ) - self.log("Errored state resolution response from the API is {0}".format(str(reset_response)), "DEBUG") + self.log("Response from 'update_device' API for errored state resolution: {0}".format(str(reset_response)), "DEBUG") self.result['msg'] = "Device reset done Successfully" self.log(self.result['msg'], "INFO") self.result['response'] = reset_response @@ -938,7 +941,7 @@ class instance for further use. return self claim_params = self.get_claim_params() - self.log("Paramters for claiming the device are {0}".format(str(claim_params)), "DEBUG") + self.log("Parameters for claiming the device: {0}".format(str(claim_params)), "DEBUG") claim_response = self.dnac_apply['exec']( family="device_onboarding_pnp", @@ -946,7 +949,7 @@ class instance for further use. op_modifies=True, params=claim_params, ) - self.log("Claiming response obtained from the API 'claim a device to a site' is {0}".format(str(claim_response)), "DEBUG") + self.log("Response from 'claim_a_device_to_a_site' API for claiming: {0}".format(str(claim_response)), "DEBUG") if claim_response.get("response") == "Device Claimed": self.result['msg'] = "Only Device Claimed Successfully" self.log(self.result['msg'], "INFO") @@ -980,7 +983,7 @@ def get_diff_deleted(self): function='get_device_list', params={"serial_number": device["deviceInfo"]["serialNumber"]} ) - self.log("Device details for the serial number {0} are {1}".format(device["deviceInfo"]["serialNumber"], str(multi_device_response)), "DEBUG") + self.log("Response from 'get_device_list' API for claiming: {0}".format(str(multi_device_response)), "DEBUG") if multi_device_response and len(multi_device_response) == 1: device_id = multi_device_response[0].get("id") @@ -990,10 +993,8 @@ def get_diff_deleted(self): op_modifies=True, params={"id": device_id}, ) - - self.log("Device details for the device deleted with \ - serial number {0} are {1}".format(device["deviceInfo"]["serialNumber"], str(response)), "DEBUG") - + self.log("Device details for the deleted device with \ + serial number '{0}': {1}".format(device["deviceInfo"]["serialNumber"], str(response)), "DEBUG") if response.get("deviceInfo", {}).get("state") == "Deleted": devices_deleted.append(device["deviceInfo"]["serialNumber"]) self.want.get("pnp_params").remove(device) From 621a8a02c7d157d3e80158b11586f56b1231936c Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 30 Jan 2024 14:41:30 +0530 Subject: [PATCH 33/76] Added the dnac_log_level and config_verify in the documentation and elem_spec --- plugins/modules/provision_intent.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/modules/provision_intent.py b/plugins/modules/provision_intent.py index c055eb1c08..418c265ea5 100644 --- a/plugins/modules/provision_intent.py +++ b/plugins/modules/provision_intent.py @@ -21,6 +21,16 @@ - cisco.dnac.intent_params author: Abinash Mishra (@abimishr) options: + dnac_log_level: + description: + - Specifies the desired log level for Cisco Catalyst Center logging. + Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] + type: str + default: WARNING + config_verify: + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. + type: bool + default: False state: description: The state of DNAC after module completion. type: str @@ -582,6 +592,8 @@ def main(): 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, + "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "config_verify": {"type": 'bool', "default": False}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} From c6481bdf3f35a37a925119c4f7b8166e468150c3 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 30 Jan 2024 15:10:43 +0530 Subject: [PATCH 34/76] Added the dnac_log_level documentation in the intent_params.py --- plugins/doc_fragments/intent_params.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/doc_fragments/intent_params.py b/plugins/doc_fragments/intent_params.py index fe95d684da..afaae9f79f 100644 --- a/plugins/doc_fragments/intent_params.py +++ b/plugins/doc_fragments/intent_params.py @@ -54,6 +54,12 @@ class ModuleDocFragment(object): If set to true the log file will be created at the location of the execution with the name dnac.log type: bool default: false + dnac_log_level: + description: + - Specifies the desired log level for Cisco Catalyst Center logging. + Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] + type: str + default: WARNING validate_response_schema: description: - Flag for Cisco DNA Center SDK to enable the validation of request bodies against a JSON schema. From 459843be44154f890ce8bac90a373a937f23fa99 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 30 Jan 2024 15:48:28 +0530 Subject: [PATCH 35/76] Removed the dnac_log_level from the documentation of each module --- plugins/modules/device_credential_intent.py | 7 +------ plugins/modules/network_settings_intent.py | 7 +------ plugins/modules/provision_intent.py | 6 ------ plugins/modules/site_intent.py | 7 +------ plugins/modules/template_intent.py | 7 +------ 5 files changed, 4 insertions(+), 30 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index d1f860ba7e..f7d92f5701 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -25,11 +25,6 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: - dnac_log_level: - description: Specifies the desired log level for Cisco Catalyst Center logging. - Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: INFO config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -2572,7 +2567,7 @@ def main(): "dnac_version": {"type": 'str', "default": '2.2.3.3'}, "dnac_debug": {"type": 'bool', "default": False}, "dnac_log": {"type": 'bool', "default": False}, - "dnac_log_level": {"type": "str", "default": "INFO"}, + "dnac_log_level": {"type": 'str', "default": 'WARNING'}, "config_verify": {"type": 'bool', "default": False}, "config": {"type": 'list', "required": True, "elements": 'dict'}, "state": {"default": 'merged', "choices": ['merged', 'deleted']}, diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 8f24cd5055..a32dc71aeb 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -25,11 +25,6 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: - dnac_log_level: - description: Specifies the desired log level for Cisco Catalyst Center logging. - Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: INFO config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -2129,7 +2124,7 @@ def main(): "dnac_version": {"type": 'str', "default": '2.2.3.3'}, "dnac_debug": {"type": 'bool', "default": False}, "dnac_log": {"type": 'bool', "default": False}, - "dnac_log_level": {"type": "str", "default": "INFO"}, + "dnac_log_level": {"type": 'str', "default": 'WARNING'}, "config_verify": {"type": 'bool', "default": False}, "config": {"type": 'list', "required": True, "elements": 'dict'}, "state": {"default": 'merged', "choices": ['merged', 'deleted']}, diff --git a/plugins/modules/provision_intent.py b/plugins/modules/provision_intent.py index 418c265ea5..9998e8c89d 100644 --- a/plugins/modules/provision_intent.py +++ b/plugins/modules/provision_intent.py @@ -21,12 +21,6 @@ - cisco.dnac.intent_params author: Abinash Mishra (@abimishr) options: - dnac_log_level: - description: - - Specifies the desired log level for Cisco Catalyst Center logging. - Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: WARNING config_verify: description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index b6bba01cbf..d0c4db7f6a 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -29,11 +29,6 @@ description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool default: False - dnac_log_level: - description: Specifies the log level for Cisco Catalyst Center logging, categorizing logs by severity. - Options- [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: INFO state: description: The state of Catalyst Center after module completion. type: str @@ -1030,7 +1025,7 @@ def main(): 'dnac_verify': {'type': 'bool', 'default': 'True'}, 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, - 'dnac_log_level': {'type': 'str', 'default': 'INFO'}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config_verify': {'type': 'bool', "default": False}, diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 540eb6f058..1e9fde74b8 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -30,11 +30,6 @@ Akash Bhaskaran (@akabhask) Muthu Rakesh (@MUTHU-RAKESH-27) options: - dnac_log_level: - description: Specifies the desired log level for Cisco Catalyst Center logging. - Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: INFO config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -2766,7 +2761,7 @@ def main(): 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, - "dnac_log_level": {"type": "str", "default": "INFO"}, + "dnac_log_level": {"type": 'str', "default": 'WARNING'}, 'validate_response_schema': {'type': 'bool', 'default': True}, "config_verify": {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, From f7b0818416213b9929fd6dd7fef6814a634eb21a Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Tue, 30 Jan 2024 21:22:17 -0500 Subject: [PATCH 36/76] Implemented class variable and logging without with open --- plugins/module_utils/dnac.py | 107 +++++++++++++++------------- plugins/modules/inventory_intent.py | 1 - 2 files changed, 57 insertions(+), 51 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index c9b335bc5b..82a6c04068 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -28,14 +28,13 @@ import inspect import re -_first_log_written = False - class DnacBase(): """Class contains members which can be reused for all intent modules""" __metaclass__ = ABCMeta + __is_log_init = False def __init__(self, module): self.module = module @@ -63,15 +62,25 @@ def __init__(self, module): 'overridden': self.verify_diff_overridden, 'gathered': self.verify_diff_gathered, 'rendered': self.verify_diff_rendered, - 'parsed': self.verify_diff_parsed + 'parsed': self.verify_diff_parsed } self.dnac_log = dnac_params.get("dnac_log") - self.dnac_log_level = dnac_params.get("dnac_log_level") or 'INFO' - self.dnac_log_file_path = dnac_params.get("dnac_log_file_path") or 'dnac.log' - if dnac_params.get("dnac_append_logs") is None: - self.dnac_append_logs = True - else: - self.dnac_append_logs = dnac_params.get("dnac_append_logs") + + if self.dnac_log and DnacBase.__is_log_init is False: + self.dnac_log_level = dnac_params.get("dnac_log_level") or 'INFO' + self.validate_dnac_log_level() + self.dnac_log_file_path = dnac_params.get("dnac_log_file_path") or 'dnac.log' + self.validate_dnac_log_file_path() + + if dnac_params.get("dnac_log_append") is False: + self.dnac_log_append = False + self.dnac_log_mode = 'w' + else: + self.dnac_log_append = True + self.dnac_log_mode = 'a' + self.log_file = open(self.dnac_log_file_path, self.dnac_log_mode) + DnacBase.__is_log_init = True + self.log('Dnac parameters: {0}'.format(str(dnac_params)), 'DEBUG') self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -153,46 +162,6 @@ def verify_diff_parsed(self): self.parsed = True return self - def log(self, message, level="WARNING", frameIncrement=0): - """Logs formatted messages with specified log level and incrementing the call stack frame - Args: - self (obj, required): An instance of the DnacBase Class. - message (str, required): The log message to be recorded. - level (str, optional): The log level, default is "info". - The log level can be one of 'DEBUG', 'INFO', 'WARNING', 'ERROR', or 'CRITICAL'. - frameIncrement (int, optional): The number of frames to increment in the call stack, default is 0. - """ - global _first_log_written - - if _first_log_written is False: - self.validate_dnac_log_level() - self.validate_dnac_log_file_path() - - self.validate_level(message, level) - level = level.upper() - self.dnac_log_level = self.dnac_log_level.upper() - - if ( - self.dnac_log - and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level) - ): - message = "Module: " + self.__class__.__name__ + ", " + message - mode = 'w' if not _first_log_written and not self.dnac_append_logs else 'a' - with open(self.dnac_log_file_path, mode) as of: - callerframerecord = inspect.stack()[frameIncrement] - frame = callerframerecord[0] - info = inspect.getframeinfo(frame) - current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() - of.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level, message)) - _first_log_written = True - - def validate_level(self, message, level): - """Validates if the specified log level is a string and one of the expected values""" - if not isinstance(level, str): - raise ValueError("Invalid log level type passed when logging the following msg: {0} level:{1}. Expected a string.".format(message, level)) - if level.upper() not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): - raise ValueError("Invalid log level passed when logging the following msg: {0} level:{1}.".format(message, level)) - def validate_dnac_log_level(self): """Validates if the logging level is string and of expected value""" if self.dnac_log_level not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): @@ -211,6 +180,40 @@ def validate_dnac_log_file_path(self): if not os.path.exists(log_directory): raise FileNotFoundError("The directory for log file '{0}' does not exist.".format(dnac_log_file_path)) + def validate_level(self, message, level): + """Validates if the specified log level is a string and one of the expected values""" + if not isinstance(level, str): + raise ValueError("Invalid log level type passed when logging the following msg: {0} level:{1}. Expected a string.".format(message, level)) + if level.upper() not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): + raise ValueError("Invalid log level passed when logging the following msg: {0} level:{1}.".format(message, level)) + + def log(self, message, level="WARNING", frameIncrement=0): + """Logs formatted messages with specified log level and incrementing the call stack frame + Args: + self (obj, required): An instance of the DnacBase Class. + message (str, required): The log message to be recorded. + level (str, optional): The log level, default is "info". + The log level can be one of 'DEBUG', 'INFO', 'WARNING', 'ERROR', or 'CRITICAL'. + frameIncrement (int, optional): The number of frames to increment in the call stack, default is 0. + """ + + if self.dnac_log: + self.validate_level(message, level) + level = level.upper() + self.dnac_log_level = self.dnac_log_level.upper() + + if logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level): + message = "Module: " + self.__class__.__name__ + ", " + message + callerframerecord = inspect.stack()[frameIncrement] + frame = callerframerecord[0] + info = inspect.getframeinfo(frame) + current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() + self.log_file.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level, message)) + + def close_log_file(self): + """Closes the open log file""" + self.log_file.close() + def check_return_status(self): """API to check the return status value and exit/fail the module""" @@ -255,7 +258,7 @@ def get_dnac_params(self, params): "dnac_log": params.get("dnac_log"), "dnac_log_level": params.get("dnac_log_level"), "dnac_log_file_path": params.get("dnac_log_file_path"), - "dnac_append_logs": params.get("dnac_append_logs") + "dnac_log_append": params.get("dnac_log_append") } return dnac_params @@ -448,6 +451,10 @@ def camel_to_snake_case(self, config): return config return new_config + def __del__(self): + """Destructor method to close the log file when the object is deleted""" + if hasattr(self, 'log_file') and self.log_file is not None: + self.close_log_file() def is_list_complex(x): return isinstance(x[0], dict) or isinstance(x[0], list) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 29ea3c9511..fe69946457 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -676,7 +676,6 @@ from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, validate_list_of_dicts, - log, ) From c3ebb93dde2e9a101cc7f375d2bf79942649a866 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Tue, 30 Jan 2024 21:27:03 -0500 Subject: [PATCH 37/76] sanity test bug fix --- plugins/module_utils/dnac.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 82a6c04068..aac29fa951 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -62,7 +62,7 @@ def __init__(self, module): 'overridden': self.verify_diff_overridden, 'gathered': self.verify_diff_gathered, 'rendered': self.verify_diff_rendered, - 'parsed': self.verify_diff_parsed + 'parsed': self.verify_diff_parsed } self.dnac_log = dnac_params.get("dnac_log") @@ -456,6 +456,7 @@ def __del__(self): if hasattr(self, 'log_file') and self.log_file is not None: self.close_log_file() + def is_list_complex(x): return isinstance(x[0], dict) or isinstance(x[0], list) From b2b115ecf6de2db3e11db86b851381a058e0688b Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Tue, 30 Jan 2024 21:33:07 -0500 Subject: [PATCH 38/76] changed log () to self.log in inventory_intent module --- plugins/modules/inventory_intent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index fe69946457..3301d777d4 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -888,7 +888,7 @@ def create_user_defined_field(self): except Exception as e: error_message = "Error while creating Global UDF(User Defined Field) in Cisco Catalyst Center: {0}".format(str(e)) - log(error_message, "ERROR") + self.log(error_message, "ERROR") return self @@ -2067,7 +2067,7 @@ def get_interface_from_ip(self, device_ip): except Exception as e: error_message = "Error while fetching Interface Id for device '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)) - log(error_message, "ERROR") + self.log(error_message, "ERROR") raise Exception(error_message) def get_device_response(self, device_ip): From a1e3f3d176b7bc4ae7e3fbb7d9c8bca1cf66f4ee Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 31 Jan 2024 15:11:47 +0530 Subject: [PATCH 39/76] Provide the support to perform all operation using device hostname, serial number, MAC address as a substitute of Ip Address, issue of getting default value of Cli transport, snmp private protocol --- plugins/modules/inventory_intent.py | 237 ++++++++++++++++++++++------ plugins/modules/swim_intent.py | 5 - 2 files changed, 187 insertions(+), 55 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 29ea3c9511..2699d4e4e1 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -28,11 +28,6 @@ description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool default: False - dnac_log_level: - description: Specifies the log level for Cisco Catalyst Center logging, categorizing logs by severity. - Options- [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: INFO state: description: The state of Cisco Catalyst Center after module completion. type: str @@ -72,6 +67,18 @@ description: Device's ipAddress. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. elements: str type: list + hostname_list: + description: A list of hostnames representing devices.Updation/Deletion/Resync/Reboot operation can be done as an alternative to ip_address. + elements: str + type: list + serial_number_list: + description: A list of serial number representing devices.Updation/Deletion/Resync/Reboot operation can be done as an alternative to ip_address. + elements: str + type: list + mac_address_list: + description: A list of mac address representing devices.Updation/Deletion/Resync/Reboot operation can be done as an alternative to ip_address. + elements: str + type: list netconf_port: description: Device's netconf port. type: str @@ -103,7 +110,6 @@ description: Device's snmp Private Protocol. Required for Adding Network, Compute, Third Party Devices. Must be given in playbook if you are updating the device credentails. type: str - default: "AES128" snmp_ro_community: description: Device's snmp ROCommunity. Required for Adding V2C Devices. type: str @@ -707,7 +713,7 @@ def validate_input(self): """ temp_spec = { - 'cli_transport': {'default': "telnet", 'type': 'str'}, + 'cli_transport': {'type': 'str'}, 'compute_device': {'type': 'bool'}, 'enable_password': {'type': 'str'}, 'extended_discovery_info': {'type': 'str'}, @@ -716,6 +722,9 @@ def validate_input(self): 'http_secure': {'type': 'bool'}, 'http_username': {'type': 'str'}, 'ip_address': {'type': 'list', 'elements': 'str'}, + 'hostname_list': {'type': 'list', 'elements': 'str'}, + 'serial_number_list': {'type': 'list', 'elements': 'str'}, + 'mac_address_list': {'type': 'list', 'elements': 'str'}, 'netconf_port': {'type': 'str'}, 'password': {'type': 'str'}, 'serial_number': {'type': 'str'}, @@ -723,7 +732,7 @@ def validate_input(self): 'snmp_auth_protocol': {'default': "SHA", 'type': 'str'}, 'snmp_mode': {'default': "AUTHPRIV", 'type': 'str'}, 'snmp_priv_passphrase': {'type': 'str'}, - 'snmp_priv_protocol': {'default': "AES128", 'type': 'str'}, + 'snmp_priv_protocol': {'type': 'str'}, 'snmp_ro_community': {'default': "public", 'type': 'str'}, 'snmp_rw_community': {'default': "private", 'type': 'str'}, 'snmp_retry': {'default': 3, 'type': 'int'}, @@ -795,6 +804,38 @@ def validate_input(self): return self + def get_device_ips_from_config_priority(self): + """ + Retrieve device IPs based on the configuration. + Args: + - self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. + Returns: + list: A list containing device IPs. + Description: + This method retrieves device IPs based on the priority order specified in the configuration. + It first checks if device IPs are available. If not, it checks hostnames, serial numbers, + and MAC addresses in order and retrieves IPs based on availability. + If none of the information is available, an empty list is returned. + """ + device_ips = self.config[0].get("ip_address") + + if device_ips: + return device_ips + + device_hostnames = self.config[0].get("hostname_list") + if device_hostnames: + return self.get_device_ips_from_hostname(device_hostnames) + + device_serial_numbers = self.config[0].get("serial_number_list") + if device_serial_numbers: + return self.get_device_ips_from_serial_number(device_serial_numbers) + + device_mac_addresses = self.config[0].get("mac_address_list") + if device_mac_addresses: + return self.get_device_ips_from_mac_address(device_mac_addresses) + + return [] + def device_exists_in_dnac(self): """ Check which devices already exists in Cisco Catalyst Center and return both device_exist and device_not_exist in dnac. @@ -1062,7 +1103,7 @@ def export_device_details(self): The CSV data is then parsed and written to a file. """ - device_ips = self.config[0].get("ip_address", []) + device_ips = self.get_device_ips_from_config_priority() if not device_ips: self.status = "failed" @@ -1188,7 +1229,7 @@ def resync_devices(self): """ # Code for triggers the resync operation using the retrieved device IDs and force sync parameter. - device_ips = self.config[0].get("ip_address", []) + device_ips = self.get_device_ips_from_config_priority() input_device_ips = device_ips.copy() device_in_dnac = self.device_exists_in_dnac() @@ -1270,7 +1311,7 @@ def reboot_access_points(self): the progress of the reboot operation. """ - device_ips = self.config[0].get("ip_address", []) + device_ips = self.get_device_ips_from_config_priority() input_device_ips = device_ips.copy() if input_device_ips: @@ -1490,7 +1531,7 @@ def provisioned_wired_device(self): site_name = self.config[0]['provision_wired_device']['site_name'] device_in_dnac = self.device_exists_in_dnac() - device_ips = self.config[0]['ip_address'] + device_ips = self.get_device_ips_from_config_priority() input_device_ips = device_ips.copy() for device_ip in input_device_ips: @@ -1711,7 +1752,7 @@ def provisioned_wireless_devices(self, device_ips): device_type = "Wireless" device_in_dnac = self.device_exists_in_dnac() - device_ips = self.config[0]['ip_address'] + device_ips = self.get_device_ips_from_config_priority() input_device_ips = device_ips.copy() for device_ip in input_device_ips: @@ -1883,7 +1924,7 @@ def get_have(self, config): """ have = {} - want_device = config.get("ip_address") + want_device = self.get_device_ips_from_config_priority() # Get the list of device that are present in Cisco Catalyst Center device_in_dnac = self.device_exists_in_dnac() @@ -1968,7 +2009,7 @@ def get_device_ids(self, device_ips): list: The list of unique device IDs for the specified devices. Description: Queries Cisco Catalyst Center to retrieve the unique device ID associated with a device having the specified - IP address. If the device is not found in Cisco Catalyst Center, it raises an exception. + IP address. If the device is not found in Cisco Catalyst Center, then print the log message with error severity. """ device_ids = [] @@ -1992,13 +2033,114 @@ def get_device_ids(self, device_ips): except Exception as e: error_message = "Error while fetching device '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)) self.log(error_message, "ERROR") - raise Exception(error_message) return device_ids + def get_device_ips_from_hostname(self, hostname_list): + """ + Get the list of unique device IDs for list of specified hostnames of devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + hostname_list (list): The hostnames of devices for which you want to retrieve the device IDs. + Returns: + list: The list of unique device IDs for the specified devices hostname list. + Description: + Queries Cisco Catalyst Center to retrieve the unique device ID associated with a device having the specified + list of hostnames. If the device is not found in Cisco Catalyst Center, tthen print the log message with error severity. + """ + + device_ips = [] + for hostname in hostname_list: + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"hostname": hostname} + ) + if response: + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") + response = response.get("response") + if not response: + continue + device_ip = response[0]["managementIpAddress"] + device_ips.append(device_ip) + except Exception as e: + error_message = "Error while fetching device from Cisco Catalyst Center - {0}".format(str(e)) + self.log(error_message, "ERROR") + + return device_ips + + def get_device_ips_from_serial_number(self, serial_number_list): + """ + Get the list of unique device IDs for list of specified serial number of devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + serial_number_list (list): The list of serial number of devices for which you want to retrieve the device IDs. + Returns: + list: The list of unique device IDs for the specified devices with serial number. + Description: + Queries Cisco Catalyst Center to retrieve the unique device ID associated with a device having the specified + serial number.If the device is not found in Cisco Catalyst Center, then print the log message with error severity. + """ + + device_ips = [] + for serial_number in serial_number_list: + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"serialNumber": serial_number} + ) + if response: + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") + response = response.get("response") + if not response: + continue + device_ip = response[0]["managementIpAddress"] + device_ips.append(device_ip) + except Exception as e: + error_message = "Error while fetching device from Cisco Catalyst Center - {0}".format(str(e)) + self.log(error_message, "ERROR") + + return device_ips + + def get_device_ips_from_mac_address(self, mac_address_list): + """ + Get the list of unique device IDs for list of specified mac address of devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + mac_address_list (list): The list of mac address of devices for which you want to retrieve the device IDs. + Returns: + list: The list of unique device IDs for the specified devices. + Description: + Queries Cisco Catalyst Center to retrieve the unique device ID associated with a device having the specified + mac addresses. If the device is not found in Cisco Catalyst Center, then print the log message with error severity. + """ + + device_ips = [] + for mac_address in mac_address_list: + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"macAddress": mac_address} + ) + if response: + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") + response = response.get("response") + if not response: + continue + device_ip = response[0]["managementIpAddress"] + device_ips.append(device_ip) + except Exception as e: + error_message = "Error while fetching device from Cisco Catalyst Center - {0}".format(str(e)) + self.log(error_message, "ERROR") + + return device_ips + def get_interface_from_id_and_name(self, device_id, interface_name): """ - Retrieve the interface ID for a device in Cisco DNA Center based on device id and interface name. + Retrieve the interface ID for a device in Cisco Catalyst Center based on device id and interface name. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_id (str): The id of the device. @@ -2040,14 +2182,14 @@ def get_interface_from_id_and_name(self, device_id, interface_name): def get_interface_from_ip(self, device_ip): """ - Get the interface ID for a device in Cisco DNA Center based on its IP address. + Get the interface ID for a device in Cisco Catalyst Center based on its IP address. Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ip (str): The IP address of the device. Returns: str: The interface ID for the specified device. Description: - The function sends a request to Cisco DNA Center to retrieve the interface information + The function sends a request to Cisco Catalyst Center to retrieve the interface information for the device with the provided IP address and extracts the interface ID from the response, and returns the interface ID. """ @@ -2193,7 +2335,7 @@ def check_credential_update(self): comparing them with the credentials specified in the configuration. """ - device_ips = self.config[0].get("ip_address") + device_ips = self.get_device_ips_from_config_priority() device_uuids = self.get_device_ids(device_ips) password = "Testing@123" payload_params = {"deviceUuids": device_uuids, "password": password, "operationEnum": "0"} @@ -2208,7 +2350,6 @@ def check_credential_update(self): csv_data_dict = { 'snmp_retry': device_data['snmp_retries'], - 'cli_transport': device_data['protocol'], 'username': device_data['cli_username'], 'password': device_data['cli_password'], 'enable_password': device_data['cli_enable_password'], @@ -2316,8 +2457,8 @@ def get_diff_merged(self, config): # Create the Global UDF self.create_user_defined_field().check_return_status() - # Get device Id with its IP Address - device_ips = self.config[0].get("ip_address") + # Get device Id based on config priority + device_ips = self.get_device_ips_from_config_priority() device_ids = self.get_device_ids(device_ips) if len(device_ids) == 0: @@ -2339,7 +2480,7 @@ def get_diff_merged(self, config): config['http_port'] = self.config[0].get("http_port", "443") if device_updated: - device_to_update = self.config[0].get("ip_address") + device_to_update = self.get_device_ips_from_config_priority() # First check if device present in Cisco Catalyst Center or not device_present = False for device in device_to_update: @@ -2366,9 +2507,14 @@ def get_diff_merged(self, config): self.check_return_status() device_data = next(csv_reader, None) playbook_params = self.want.get("device_params") + playbook_params['ipAddress'] = device_to_update + + if not playbook_params['cliTransport']: + playbook_params['cliTransport'] = device_data['protocol'] + if not playbook_params['snmpPrivProtocol']: + playbook_params['snmpPrivProtocol'] = device_data['snmpv3_privacy_type'] csv_data_dict = { - 'cli_transport': device_data['protocol'], 'username': device_data['cli_username'], 'password': device_data['cli_password'], 'enable_password': device_data['cli_enable_password'], @@ -2382,12 +2528,11 @@ def get_diff_merged(self, config): device_key_mapping = { 'username': 'userName', - 'cli_transport': 'cliTransport', 'password': 'password', 'enable_password': 'enablePassword', 'snmp_username': 'snmpUserName' } - device_update_key_list = ["username", "password", "enable_password", "cli_transport", "snmp_username"] + device_update_key_list = ["username", "password", "enable_password", "snmp_username"] for key in device_update_key_list: mapped_key = device_key_mapping[key] @@ -2398,14 +2543,6 @@ def get_diff_merged(self, config): playbook_params['snmpPrivPassphrase'] = csv_data_dict['snmp_priv_passphrase'] playbook_params[mapped_key] = csv_data_dict[key] - snmp_protocol_mapping = { - 'AES128': 'CISCOAES128', - 'AES192': 'CISCOAES192', - 'AES256': 'CISCOAES256' - } - protocol_type = playbook_params['snmpPrivProtocol'] - playbook_params['snmpPrivProtocol'] = snmp_protocol_mapping[protocol_type] - if playbook_params['snmpMode'] == "NOAUTHNOPRIV": playbook_params.pop('snmpAuthPassphrase', None) playbook_params.pop('snmpPrivPassphrase', None) @@ -2452,7 +2589,7 @@ def get_diff_merged(self, config): self.log(error_message, "ERROR") raise Exception(error_message) - self.msg = "Devices {0} present in Cisco Catalyst Center and updated successfully".format(config['ip_address']) + self.msg = "Devices {0} present in Cisco Catalyst Center and updated successfully".format(str(device_to_update)) self.log(self.msg, "INFO") self.status = "success" @@ -2596,13 +2733,13 @@ def get_diff_merged(self, config): if device_added: config['ip_address'] = devices_to_add device_params = self.want.get("device_params") - snmp_protocol_mapping = { - 'AES128': 'CISCOAES128', - 'AES192': 'CISCOAES192', - 'AES256': 'CISCOAES256' - } - protocol_type = device_params['snmpPrivProtocol'] - device_params['snmpPrivProtocol'] = snmp_protocol_mapping[protocol_type] + + if not device_params['cliTransport']: + device_params['cliTransport'] = "ssh" + + if not device_params['snmpPrivProtocol']: + device_params['snmpPrivProtocol'] = "AES128" + self.mandatory_parameter().check_return_status() try: response = self.dnac._exec( @@ -2666,8 +2803,8 @@ def get_diff_merged(self, config): # Create the Global UDF self.create_user_defined_field().check_return_status() - # Get device Id with its IP Address - device_ips = self.config[0].get("ip_address") + # Get device Id based on config priority + device_ips = self.get_device_ips_from_config_priority() device_ids = self.get_device_ids(device_ips) if not device_ids: @@ -2691,7 +2828,7 @@ def get_diff_merged(self, config): # Once Wireless device get added we will assign device to site and Provisioned it if self.config[0].get('provision_wireless_device'): - device_ips = self.config[0]['ip_address'] + device_ips = self.get_device_ips_from_config_priority() self.provisioned_wireless_devices(device_ips).check_return_status() if device_resynced: @@ -2719,7 +2856,7 @@ def get_diff_deleted(self, config): the Global User Defined Field that are associated to the devices. """ - device_to_delete = config.get("ip_address") + device_to_delete = self.get_device_ips_from_config_priority() self.result['msg'] = [] if self.config[0].get('add_user_defined_field'): @@ -2879,7 +3016,7 @@ def verify_diff_merged(self, config): device_updated = self.config[0].get("device_updated", False) credential_update = self.config[0].get("credential_update", False) device_type = self.config[0].get("type", "NETWORK_DEVICE") - device_ips = self.config[0].get("ip_address") + device_ips = self.get_device_ips_from_config_priority() if device_added: if not devices_to_add: @@ -3025,7 +3162,7 @@ def main(): 'dnac_verify': {'type': 'bool', 'default': 'True'}, 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, - 'dnac_log_level': {'type': 'str', 'default': 'INFO'}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config_verify': {'type': 'bool', "default": False}, diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 2643bf46fc..33e2e2514d 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -30,11 +30,6 @@ Rishita Chowdhary (@rishitachowdhary) Abhishek Maheshwari (@abmahesh) options: - dnac_log_level: - description: Specifies the log level for Cisco Catalyst Center logging, categorizing logs by severity. - Options- [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: WARNING state: description: The state of Catalyst Center after module completion. type: str From 89270f01c8b34f29fd74676fcef03d60b85b067d Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 31 Jan 2024 17:15:03 +0530 Subject: [PATCH 40/76] Addressed the issue of Swim for importing image from local having 2 extra argument, added review comments in code --- plugins/modules/inventory_intent.py | 74 ++++++++++++++++------------- plugins/modules/swim_intent.py | 5 +- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 2699d4e4e1..cb5ac36c65 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -68,15 +68,18 @@ elements: str type: list hostname_list: - description: A list of hostnames representing devices.Updation/Deletion/Resync/Reboot operation can be done as an alternative to ip_address. + description: "A list of hostnames representing devices. Operations such as updating, deleting, resyncing, or rebooting + can be performed as alternatives to using IP addresses." elements: str type: list serial_number_list: - description: A list of serial number representing devices.Updation/Deletion/Resync/Reboot operation can be done as an alternative to ip_address. + description: A list of serial numbers representing devices. Operations such as updating, deleting, resyncing, or rebooting + can be performed as alternatives to using IP addresses. elements: str type: list mac_address_list: - description: A list of mac address representing devices.Updation/Deletion/Resync/Reboot operation can be done as an alternative to ip_address. + description: "A list of MAC addresses representing devices. Operations such as updating, deleting, resyncing, or rebooting + can be performed as alternatives to using IP addresses." elements: str type: list netconf_port: @@ -817,23 +820,28 @@ def get_device_ips_from_config_priority(self): and MAC addresses in order and retrieves IPs based on availability. If none of the information is available, an empty list is returned. """ + # Retrieve device IPs from the configuration device_ips = self.config[0].get("ip_address") if device_ips: return device_ips + # If device IPs are not available, check hostnames device_hostnames = self.config[0].get("hostname_list") if device_hostnames: return self.get_device_ips_from_hostname(device_hostnames) + # If hostnames are not available, check serial numbers device_serial_numbers = self.config[0].get("serial_number_list") if device_serial_numbers: return self.get_device_ips_from_serial_number(device_serial_numbers) + # If serial numbers are not available, check MAC addresses device_mac_addresses = self.config[0].get("mac_address_list") if device_mac_addresses: return self.get_device_ips_from_mac_address(device_mac_addresses) + # If no information is available, return an empty list return [] def device_exists_in_dnac(self): @@ -2038,15 +2046,15 @@ def get_device_ids(self, device_ips): def get_device_ips_from_hostname(self, hostname_list): """ - Get the list of unique device IDs for list of specified hostnames of devices in Cisco Catalyst Center. + Get the list of unique device IPs for list of specified hostnames of devices in Cisco Catalyst Center. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - hostname_list (list): The hostnames of devices for which you want to retrieve the device IDs. + hostname_list (list): The hostnames of devices for which you want to retrieve the device IPs. Returns: - list: The list of unique device IDs for the specified devices hostname list. + list: The list of unique device IPs for the specified devices hostname list. Description: - Queries Cisco Catalyst Center to retrieve the unique device ID associated with a device having the specified - list of hostnames. If the device is not found in Cisco Catalyst Center, tthen print the log message with error severity. + Queries Cisco Catalyst Center to retrieve the unique device IP's associated with a device having the specified + list of hostnames. If a device is not found in Cisco Catalyst Center, an error log message is printed. """ device_ips = [] @@ -2060,27 +2068,27 @@ def get_device_ips_from_hostname(self, hostname_list): if response: self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") response = response.get("response") - if not response: - continue - device_ip = response[0]["managementIpAddress"] - device_ips.append(device_ip) + if response: + device_ip = response[0]["managementIpAddress"] + if device_ip: + device_ips.append(device_ip) except Exception as e: - error_message = "Error while fetching device from Cisco Catalyst Center - {0}".format(str(e)) + error_message = "Exception occurred while fetching device from Cisco Catalyst Center: {0}".format(str(e)) self.log(error_message, "ERROR") return device_ips def get_device_ips_from_serial_number(self, serial_number_list): """ - Get the list of unique device IDs for list of specified serial number of devices in Cisco Catalyst Center. + Get the list of unique device IPs for a specified list of serial numbers in Cisco Catalyst Center. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - serial_number_list (list): The list of serial number of devices for which you want to retrieve the device IDs. + serial_number_list (list): The list of serial number of devices for which you want to retrieve the device IPs. Returns: - list: The list of unique device IDs for the specified devices with serial number. + list: The list of unique device IPs for the specified devices with serial numbers. Description: - Queries Cisco Catalyst Center to retrieve the unique device ID associated with a device having the specified - serial number.If the device is not found in Cisco Catalyst Center, then print the log message with error severity. + Queries Cisco Catalyst Center to retrieve the unique device IPs associated with a device having the specified + serial numbers.If a device is not found in Cisco Catalyst Center, an error log message is printed. """ device_ips = [] @@ -2094,27 +2102,27 @@ def get_device_ips_from_serial_number(self, serial_number_list): if response: self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") response = response.get("response") - if not response: - continue - device_ip = response[0]["managementIpAddress"] - device_ips.append(device_ip) + if response: + device_ip = response[0]["managementIpAddress"] + if device_ip: + device_ips.append(device_ip) except Exception as e: - error_message = "Error while fetching device from Cisco Catalyst Center - {0}".format(str(e)) + error_message = "Exception occurred while fetching device from Cisco Catalyst Center - {0}".format(str(e)) self.log(error_message, "ERROR") return device_ips def get_device_ips_from_mac_address(self, mac_address_list): """ - Get the list of unique device IDs for list of specified mac address of devices in Cisco Catalyst Center. + Get the list of unique device IPs for list of specified mac address of devices in Cisco Catalyst Center. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - mac_address_list (list): The list of mac address of devices for which you want to retrieve the device IDs. + mac_address_list (list): The list of mac address of devices for which you want to retrieve the device IPs. Returns: - list: The list of unique device IDs for the specified devices. + list: The list of unique device IPs for the specified devices. Description: - Queries Cisco Catalyst Center to retrieve the unique device ID associated with a device having the specified - mac addresses. If the device is not found in Cisco Catalyst Center, then print the log message with error severity. + Queries Cisco Catalyst Center to retrieve the unique device IPs associated with a device having the specified + mac addresses. If a device is not found in Cisco Catalyst Center, an error log message is printed. """ device_ips = [] @@ -2128,12 +2136,12 @@ def get_device_ips_from_mac_address(self, mac_address_list): if response: self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") response = response.get("response") - if not response: - continue - device_ip = response[0]["managementIpAddress"] - device_ips.append(device_ip) + if response: + device_ip = response[0]["managementIpAddress"] + if device_ip: + device_ips.append(device_ip) except Exception as e: - error_message = "Error while fetching device from Cisco Catalyst Center - {0}".format(str(e)) + error_message = "Exception occurred while fetching device from Cisco Catalyst Center - {0}".format(str(e)) self.log(error_message, "ERROR") return device_ips diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 33e2e2514d..52f8955653 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -382,6 +382,7 @@ get_dict_result, ) from ansible.module_utils.basic import AnsibleModule +import os class DnacSwims(DnacBase): @@ -882,12 +883,14 @@ def get_diff_import(self): ) import_function = 'import_software_image_via_url' else: + file_path = self.want.get("local_import_details").get("file_path") import_params = dict( is_third_party=self.want.get("local_import_details").get("is_third_party"), third_party_vendor=self.want.get("local_import_details").get("third_party_vendor"), third_party_image_family=self.want.get("local_import_details").get("third_party_image_family"), third_party_application_type=self.want.get("local_import_details").get("third_party_application_type"), - file_path=self.want.get("local_import_details").get("file_path"), + multipart_fields={'file': (os.path.basename(file_path), open(file_path, 'rb'), 'application/octet-stream')}, + multipart_monitor_callback=None ) import_function = 'import_local_software_image' From 91c86e438b199329919e1bf84840c9eb5009617c Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Wed, 31 Jan 2024 21:16:00 -0500 Subject: [PATCH 41/76] defined setup_logger --- plugins/module_utils/dnac.py | 94 ++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index aac29fa951..dbb9d9aaad 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -66,22 +66,22 @@ def __init__(self, module): } self.dnac_log = dnac_params.get("dnac_log") - if self.dnac_log and DnacBase.__is_log_init is False: - self.dnac_log_level = dnac_params.get("dnac_log_level") or 'INFO' + if self.dnac_log and not DnacBase.__is_log_init: + self.dnac_log_level = dnac_params.get("dnac_log_level") or 'WARNING' self.validate_dnac_log_level() + self.dnac_log_level = self.dnac_log_level.upper() self.dnac_log_file_path = dnac_params.get("dnac_log_file_path") or 'dnac.log' self.validate_dnac_log_file_path() - - if dnac_params.get("dnac_log_append") is False: - self.dnac_log_append = False - self.dnac_log_mode = 'w' - else: - self.dnac_log_append = True - self.dnac_log_mode = 'a' - self.log_file = open(self.dnac_log_file_path, self.dnac_log_mode) + self.dnac_log_mode = 'w' if not dnac_params.get("dnac_log_append") else 'a' + self.setup_logger('dnac_file_logger') + self.dnac_file_logger = logging.getLogger('dnac_file_logger') DnacBase.__is_log_init = True + self.dnac_file_logger.debug('Logging configured and initiated') + elif not self.dnac_log: + # If dnac_log is False, return an empty logger + self.dnac_file_logger = logging.getLogger('empty_logger') - self.log('Dnac parameters: {0}'.format(str(dnac_params)), 'DEBUG') + self.dnac_file_logger.debug('Dnac parameters: {0}'.format(str(dnac_params))) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -162,6 +162,26 @@ def verify_diff_parsed(self): self.parsed = True return self + def setup_logger(self, logger_name): + """Set up a logger with specified name and configuration based on dnac_log_level""" + level_mapping = { + 'INFO': logging.INFO, + 'DEBUG': logging.DEBUG, + 'WARNING': logging.WARNING, + 'ERROR': logging.ERROR, + 'CRITICAL': logging.CRITICAL + } + level = level_mapping.get(self.dnac_log_level, logging.WARNING) + + logger = logging.getLogger(logger_name) + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(module)s:%(funcName)s:%(lineno)d --- %(message)s', datefmt='%m-%d-%Y %H:%M:%S') + + file_handler = logging.FileHandler(self.dnac_log_file_path, mode=self.dnac_log_mode) + file_handler.setFormatter(formatter) + + logger.setLevel(level) + logger.addHandler(file_handler) + def validate_dnac_log_level(self): """Validates if the logging level is string and of expected value""" if self.dnac_log_level not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): @@ -180,13 +200,6 @@ def validate_dnac_log_file_path(self): if not os.path.exists(log_directory): raise FileNotFoundError("The directory for log file '{0}' does not exist.".format(dnac_log_file_path)) - def validate_level(self, message, level): - """Validates if the specified log level is a string and one of the expected values""" - if not isinstance(level, str): - raise ValueError("Invalid log level type passed when logging the following msg: {0} level:{1}. Expected a string.".format(message, level)) - if level.upper() not in ('INFO', 'DEBUG', 'WARNING', 'ERROR', 'CRITICAL'): - raise ValueError("Invalid log level passed when logging the following msg: {0} level:{1}.".format(message, level)) - def log(self, message, level="WARNING", frameIncrement=0): """Logs formatted messages with specified log level and incrementing the call stack frame Args: @@ -194,30 +207,22 @@ def log(self, message, level="WARNING", frameIncrement=0): message (str, required): The log message to be recorded. level (str, optional): The log level, default is "info". The log level can be one of 'DEBUG', 'INFO', 'WARNING', 'ERROR', or 'CRITICAL'. - frameIncrement (int, optional): The number of frames to increment in the call stack, default is 0. """ if self.dnac_log: - self.validate_level(message, level) - level = level.upper() - self.dnac_log_level = self.dnac_log_level.upper() - - if logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level): - message = "Module: " + self.__class__.__name__ + ", " + message - callerframerecord = inspect.stack()[frameIncrement] - frame = callerframerecord[0] - info = inspect.getframeinfo(frame) - current_datetime = datetime.datetime.now().replace(microsecond=0).isoformat() - self.log_file.write("---- {0} ---- {1}@{2} ---- {3}: {4}\n".format(current_datetime, info.lineno, info.function, level, message)) - - def close_log_file(self): - """Closes the open log file""" - self.log_file.close() + message = "Module: " + self.__class__.__name__ + ", " + message + callerframerecord = inspect.stack()[frameIncrement] + frame = callerframerecord[0] + info = inspect.getframeinfo(frame) + #log_message = "{0}@{1} --- {2}".format(info.lineno, info.function, message) + log_method = getattr(self.dnac_file_logger, level.lower()) + log_method(message) def check_return_status(self): """API to check the return status value and exit/fail the module""" - self.log("status: {0}, msg:{1}".format(self.status, self.msg), frameIncrement=1) + #self.log("status: {0}, msg:{1}".format(self.status, self.msg), frameIncrement=1) + self.dnac_file_logger.debug("status: {0}, msg:{1}".format(self.status, self.msg)) if "failed" in self.status: self.module.fail_json(msg=self.msg, response=[]) elif "exited" in self.status: @@ -282,7 +287,7 @@ def get_task_details(self, task_id): params={"task_id": task_id} ) - self.log('Tas Details: {0}'.format(str(response)), 'DEBUG') + log("Retrieving task details by the API 'get_task_by_id' using task ID: {0}, Response: {1}".format(str(response)), 'DEBUG') if response and isinstance(response, dict): result = response.get('response') @@ -321,7 +326,7 @@ def check_task_response_status(self, response, validation_string, data=False): task_id = response.get("taskId") while True: task_details = self.get_task_details(task_id) - self.log('Task details: {0}'.format(str(task_details)), 'DEBUG') + self.dnac_file_logger.debug('Getting task details from task ID {1}: {0}'.format(str(task_details), task_id)) if task_details.get("isError") is True: if task_details.get("failureReason"): @@ -338,7 +343,7 @@ def check_task_response_status(self, response, validation_string, data=False): self.status = "success" break - self.log("progress set to {0} for taskid: {1}" + self.dnac_file_logger.debug("progress set to {0} for taskid: {1}" .format(task_details.get('progress'), task_id)) return self @@ -360,13 +365,13 @@ def get_execution_details(self, execid): response (dict) - Status for API execution """ - self.log("Execution Id " + str(execid)) + self.dnac_file_logger.debug("Execution Id " + str(execid)) response = self.dnac._exec( family="task", function='get_business_api_execution_details', params={"execution_id": execid} ) - self.log("Response for the current execution" + str(response)) + self.dnac_file_logger.debug("Response for the current execution" + str(response)) return response def check_execution_response_status(self, response): @@ -380,7 +385,7 @@ def check_execution_response_status(self, response): self """ - self.log(str(response)) + self.dnac_file_logger.debug(str(response)) if not response: self.msg = "response is empty" self.status = "failed" @@ -442,7 +447,7 @@ def camel_to_snake_case(self, config): for key, value in config.items(): new_key = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', key).lower() if new_key != key: - self.log("{0} will be deprecated soon. Please use {1}.".format(key, new_key)) + self.dnac_file_logger.debug("{0} will be deprecated soon. Please use {1}.".format(key, new_key)) new_value = self.camel_to_snake_case(value) new_config[new_key] = new_value elif isinstance(config, list): @@ -451,11 +456,6 @@ def camel_to_snake_case(self, config): return config return new_config - def __del__(self): - """Destructor method to close the log file when the object is deleted""" - if hasattr(self, 'log_file') and self.log_file is not None: - self.close_log_file() - def is_list_complex(x): return isinstance(x[0], dict) or isinstance(x[0], list) From df16312d172d1f856e969fc0bd8fa9dba7fac6e2 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Wed, 31 Jan 2024 21:29:59 -0500 Subject: [PATCH 42/76] defined setup_logger --- plugins/module_utils/dnac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index dbb9d9aaad..c204cbc6d5 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -287,7 +287,7 @@ def get_task_details(self, task_id): params={"task_id": task_id} ) - log("Retrieving task details by the API 'get_task_by_id' using task ID: {0}, Response: {1}".format(str(response)), 'DEBUG') + self.dnac_file_logger.debug("Retrieving task details by the API 'get_task_by_id' using task ID: {0}, Response: {1}".format(str(response))) if response and isinstance(response, dict): result = response.get('response') From 1ff182f4ed383e2ce5542df9c60dfeaa6fed5e2b Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Wed, 31 Jan 2024 21:45:28 -0500 Subject: [PATCH 43/76] defined setup_logger --- plugins/module_utils/dnac.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index c204cbc6d5..96d8bd4626 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -24,7 +24,7 @@ import os.path import copy import json -import datetime +# import datetime import inspect import re @@ -81,7 +81,7 @@ def __init__(self, module): # If dnac_log is False, return an empty logger self.dnac_file_logger = logging.getLogger('empty_logger') - self.dnac_file_logger.debug('Dnac parameters: {0}'.format(str(dnac_params))) + self.dnac_file_logger.debug('Dnac parameters: %s', str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -214,15 +214,15 @@ def log(self, message, level="WARNING", frameIncrement=0): callerframerecord = inspect.stack()[frameIncrement] frame = callerframerecord[0] info = inspect.getframeinfo(frame) - #log_message = "{0}@{1} --- {2}".format(info.lineno, info.function, message) + # log_message = "{0}@{1} --- {2}".format(info.lineno, info.function, message) log_method = getattr(self.dnac_file_logger, level.lower()) log_method(message) def check_return_status(self): """API to check the return status value and exit/fail the module""" - #self.log("status: {0}, msg:{1}".format(self.status, self.msg), frameIncrement=1) - self.dnac_file_logger.debug("status: {0}, msg:{1}".format(self.status, self.msg)) + # self.log("status: {0}, msg:{1}".format(self.status, self.msg), frameIncrement=1) + self.dnac_file_logger.debug("status: %s, msg: %s", self.status, self.msg) if "failed" in self.status: self.module.fail_json(msg=self.msg, response=[]) elif "exited" in self.status: @@ -286,8 +286,7 @@ def get_task_details(self, task_id): function='get_task_by_id', params={"task_id": task_id} ) - - self.dnac_file_logger.debug("Retrieving task details by the API 'get_task_by_id' using task ID: {0}, Response: {1}".format(str(response))) + self.dnac_file_logger.debug("Retrieving task details by the API 'get_task_by_id' using task ID: %s, Response: %s", str(task_id), str(response)) if response and isinstance(response, dict): result = response.get('response') @@ -326,7 +325,8 @@ def check_task_response_status(self, response, validation_string, data=False): task_id = response.get("taskId") while True: task_details = self.get_task_details(task_id) - self.dnac_file_logger.debug('Getting task details from task ID {1}: {0}'.format(str(task_details), task_id)) + self.dnac_file_logger.debug('Getting task details from task ID %s: %s', task_id, str(task_details)) + if task_details.get("isError") is True: if task_details.get("failureReason"): @@ -343,8 +343,7 @@ def check_task_response_status(self, response, validation_string, data=False): self.status = "success" break - self.dnac_file_logger.debug("progress set to {0} for taskid: {1}" - .format(task_details.get('progress'), task_id)) + self.dnac_file_logger.debug("progress set to %s for taskid: %s", task_details.get('progress'), task_id) return self @@ -365,13 +364,13 @@ def get_execution_details(self, execid): response (dict) - Status for API execution """ - self.dnac_file_logger.debug("Execution Id " + str(execid)) + self.dnac_file_logger.debug("Execution Id %s", str(execid)) response = self.dnac._exec( family="task", function='get_business_api_execution_details', params={"execution_id": execid} ) - self.dnac_file_logger.debug("Response for the current execution" + str(response)) + self.dnac_file_logger.debug("Response for the current execution %s", str(response)) return response def check_execution_response_status(self, response): @@ -447,7 +446,7 @@ def camel_to_snake_case(self, config): for key, value in config.items(): new_key = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', key).lower() if new_key != key: - self.dnac_file_logger.debug("{0} will be deprecated soon. Please use {1}.".format(key, new_key)) + self.dnac_file_logger.debug("%s will be deprecated soon. Please use %s.", key, new_key) new_value = self.camel_to_snake_case(value) new_config[new_key] = new_value elif isinstance(config, list): From 05647b03601441b5a22433758058b040cd3e1390 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Wed, 31 Jan 2024 21:50:07 -0500 Subject: [PATCH 44/76] defined setup_logger --- plugins/module_utils/dnac.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 96d8bd4626..88af3d709c 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -327,7 +327,6 @@ def check_task_response_status(self, response, validation_string, data=False): task_details = self.get_task_details(task_id) self.dnac_file_logger.debug('Getting task details from task ID %s: %s', task_id, str(task_details)) - if task_details.get("isError") is True: if task_details.get("failureReason"): self.msg = str(task_details.get("failureReason")) From d808ffaf134af1058611441d8c55417b0054f329 Mon Sep 17 00:00:00 2001 From: SHUBHAM VARFA Date: Thu, 1 Feb 2024 04:25:11 +0000 Subject: [PATCH 45/76] Removing config_verify, dnac_log_level from code along with fixing issues related to discovery --- playbooks/PnP.yml | 1 + plugins/modules/discovery_intent.py | 121 ++++++++++++++-------------- plugins/modules/pnp_intent.py | 9 --- 3 files changed, 62 insertions(+), 69 deletions(-) diff --git a/playbooks/PnP.yml b/playbooks/PnP.yml index 1c3dd206ad..b034bc3f81 100644 --- a/playbooks/PnP.yml +++ b/playbooks/PnP.yml @@ -16,6 +16,7 @@ dnac_port: "{{ dnac_port }}" dnac_version: "{{ dnac_version }}" dnac_debug: "{{ dnac_debug }}" + dnac_log_level: DEBUG tasks: diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index e43000d47e..e15df25204 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -22,15 +22,6 @@ author: Abinash Mishra (@abimishr) Phan Nguyen (phannguy) options: - config_verify: - description: Set to True to verify the Cisco DNA Center config after applying the playbook config. - type: bool - default: False - dnac_log_level: - description: Specifies the log level for Cisco Catalyst Center logging, categorizing logs by severity. - Options- [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: WARNING state: description: The state of DNAC after module completion. type: str @@ -43,19 +34,11 @@ elements: dict required: true suboptions: - devices_list: - description: List of devices with details necessary for discovering the devices. + ip_address_list: + description: List of IP addresses to be discoverred. type: list - elements: dict + elements: str required: true - suboptions: - name: - description: Hostname of the device - type: str - ip: - description: Management IP address of the device - type: str - required: true discovery_type: description: Type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP) type: str @@ -147,10 +130,13 @@ snmp_version: description: Version of SNMP (v2/v3) type: str - required: true timeout: description: Time to wait for device response in seconds type: int + cli_cred_len: + description: Total CLI credentials that needs to be used. This value can vary from 1 to 5. + type: int + default: 1 requirements: - dnacentersdk == 2.6.10 - python >= 3.5 @@ -186,10 +172,9 @@ dnac_log: True dnac_log_level: "{{dnac_log_level}}" state: merged + config_verify: True config: - - devices_list: - - name: string - ip: string + - ip_address_list: list discovery_type: string cdp_level: string lldp_level: string @@ -217,6 +202,7 @@ snmp_version: string timeout: integer username_list: list + cli_cred_length: integer - name: Delete disovery by name cisco.dnac.discovery_intent: dnac_host: "{{dnac_host}}" @@ -227,11 +213,11 @@ dnac_version: "{{dnac_version}}" dnac_debug: "{{dnac_debug}}" dnac_log: True + dnac_log_level: "{{dnac_log_level}}" state: deleted + config_verify: True config: - - devices_list: - - name: string - ip: string + - ip_address_list: list start_index: integer records_to_return: integer discovery_name: string @@ -335,8 +321,8 @@ def validate_input(self): 'discovery_type': {'type': 'str', 'required': True}, 'enable_password_list': {'type': 'list', 'required': False, 'elements': 'str'}, - 'devices_list': {'type': 'list', 'required': True, - 'elements': 'dict'}, + 'ip_address_list': {'type': 'list', 'required': True, + 'elements': 'str'}, 'start_index': {'type': 'int', 'required': False, 'default': 25}, 'records_to_return': {'type': 'int', 'required': False}, @@ -366,10 +352,12 @@ def validate_input(self): 'snmp_rw_community': {'type': 'str', 'required': False}, 'snmp_rw_community_desc': {'type': 'str', 'required': False}, 'snmp_username': {'type': 'str', 'required': False}, - 'snmp_version': {'type': 'str', 'required': True}, + 'snmp_version': {'type': 'str', 'required': False}, 'timeout': {'type': 'str', 'required': False}, 'username_list': {'type': 'list', 'required': False, - 'elements': 'str'} + 'elements': 'str'}, + 'cli_cred_len': {'type': 'int', 'required': False, + 'default': 1} } # Validate discovery params @@ -399,7 +387,7 @@ def get_creds_ids_list(self): the class instance. """ - self.log("Credential IDs list is {0}".format(str(self.creds_ids_list)), "INFO") + self.log("Credential Ids list passed is {0}".format(str(self.creds_ids_list)), "INFO") return self.creds_ids_list def get_dnac_global_credentials_v2_info(self): @@ -422,11 +410,19 @@ def get_dnac_global_credentials_v2_info(self): params=self.validated_config[0].get('headers'), ) response = response.get('response') + cli_len_inp = self.validated_config[0].get("cli_cred_len") + if cli_len_inp > 5: + cli_len_inp = 5 self.log("The Global credentials response from 'get all global credentials v2' API is {0}".format(str(response)), "DEBUG") - for value in response.values(): - if not value: - continue - self.creds_ids_list.extend(element.get('id') for element in value) + cli_len = 0 + for key in response.keys(): + if key == "cliCredential": + for element in response.get(key): + while cli_len < cli_len_inp: + self.creds_ids_list.append(element.get('id')) + cli_len += 1 + else: + self.creds_ids_list.extend(element.get('id') for element in response.get(key)) if not self.creds_ids_list: msg = 'Not found any credentials to perform discovery' @@ -441,22 +437,22 @@ def get_devices_list_info(self): It then updates the result attribute with this list. Returns: - - devices_list: The list of devices extracted from the + - ip_address_list: The list of devices extracted from the 'validated_config' attribute. """ - devices_list = self.validated_config[0].get('devices_list') - self.result.update(dict(devices_info=devices_list)) - self.log("Devices list info passed is {0}".format(str(devices_list)), "INFO") - return devices_list + ip_address_list = self.validated_config[0].get('ip_address_list') + self.result.update(dict(devices_info=ip_address_list)) + self.log("Devices list info passed: {0}".format(str(ip_address_list)), "INFO") + return ip_address_list - def preprocessing_devices_info(self, devices_list=None): + def preprocessing_devices_info(self, ip_address_list=None): """ Preprocess the devices' information. Extract the IP addresses from the list of devices and perform additional processing based on the 'discovery_type' in the validated configuration. Parameters: - - devices_list: The list of devices to preprocess. If not + - ip_address_list: The list of devices to preprocess. If not provided, an empty list is used. Returns: @@ -465,36 +461,41 @@ def preprocessing_devices_info(self, devices_list=None): of IP ranges separated by commas. """ - if devices_list is None: - devices_list = [] - - ip_address_list = [device['ip'] for device in devices_list] + if ip_address_list is None: + ip_address_list = [] self.log("Discovery type passed for the discovery is {0}".format(self.validated_config[0].get('discovery_type')), "INFO") if self.validated_config[0].get('discovery_type') in ["SINGLE", "CDP", "LLDP"]: if len(ip_address_list) == 1: ip_address_list = ip_address_list[0] else: - self.log("Device list's length is longer than 1", "ERROR") - self.module.fail_json(msg="Device list's length is longer than 1", response=[]) + self.log("IP Address list's length is longer than 1", "ERROR") + self.module.fail_json(msg="IP Address list's length is longer than 1", response=[]) elif self.validated_config[0].get('discovery_type') == "CIDR": if len(ip_address_list) == 1 and self.validated_config[0].get('prefix_length'): ip_address_list = ip_address_list[0] ip_address_list = str(ip_address_list) + "/" + str(self.validated_config[0].get('prefix_length')) else: - self.log("Device list's length is longer than 1", "ERROR") - self.module.fail_json(msg="Device list's length is longer than 1", response=[]) + self.log("IP Address list's length is longer than 1", "ERROR") + self.module.fail_json(msg="IP Address list's length is longer than 1", response=[]) + elif self.validated_config[0].get('discovery_type') == "RANGE": + if len(ip_address_list) == 1: + if len(str(ip_address_list[0]).split("-")) == 2: + ip_address_list = ip_address_list[0] + else: + ip_address_list = "{0}-{1}".format(ip_address_list[0], ip_address_list[0]) + else: + self.log("IP Address list's length is longer than 1", "ERROR") + self.module.fail_json(msg="IP Address list's length is longer than 1", response=[]) else: - ip_address_list = list( - map( - lambda x: '{0}-{value}'.format(x, value=x), - ip_address_list - ) - ) - ip_address_list = ','.join(ip_address_list) - + new_ip_collected = [] + for ip in ip_address_list: + if len(str(ip).split("-")) != 2: + ip_collected = "{0}-{0}".format(ip) + new_ip_collected.append(ip_collected) + ip_address_list = ','.join(new_ip_collected) self.log("Collected IP address/addresses are {0}".format(str(ip_address_list)), "INFO") - return ip_address_list + return str(ip_address_list) def create_params(self, credential_ids=None, ip_address_list=None): """ diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 18b06a3a23..43ac5e8d76 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -24,15 +24,6 @@ Rishita Chowdhary (@rishitachowdhary) Abinash Mishra (@abimishr) options: - config_verify: - description: Set to True to verify the Cisco DNA Center config after applying the playbook config. - type: bool - default: False - dnac_log_level: - description: Specifies the log level for Cisco Catalyst Center logging, categorizing logs by severity. - Options- [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: WARNING state: description: The state of DNAC after module completion. type: str From 9ef7277068580eca2697254a4504051ea40e3d18 Mon Sep 17 00:00:00 2001 From: SHUBHAM VARFA Date: Thu, 1 Feb 2024 04:36:42 +0000 Subject: [PATCH 46/76] Removing config_verify, dnac_log_level from code along with fixing issues related to discovery --- playbooks/discovery_intent.yml | 28 ++++++++-------------------- plugins/modules/discovery_intent.py | 4 ++++ plugins/modules/pnp_intent.py | 4 ++++ 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/playbooks/discovery_intent.yml b/playbooks/discovery_intent.yml index f285d71cdd..b8917b4423 100644 --- a/playbooks/discovery_intent.yml +++ b/playbooks/discovery_intent.yml @@ -16,6 +16,8 @@ dnac_port: "{{ dnac_port }}" dnac_version: "{{ dnac_version }}" dnac_debug: "{{ dnac_debug }}" + dnac_log: True + dnac_log_level: DEBUG tasks: - name: Execute discovery devices using MULTI RANGE @@ -24,21 +26,11 @@ state: merged config_verify: True config: - - devices_list: - - name: SJ-BN-9300 - site: Global/USA/SAN JOSE/BLD23 - role: MAPSERVER,BORDERNODE,INTERNAL,EXTERNAL,SDATRANSIT - l2interface: TenGigabitEthernet1/1/8 - ip: 204.1.2.1 - - name: NY-BN-9300 - site: Global/USA/New York/BLDNYC - role: MAPSERVER,BORDERNODE,INTERNAL,EXTERNAL,SDATRANSIT,NOIPTRANSIT,ECA - managed_ap_site: Global/USA/New York/BLDNYC/FLOOR1 - rolling_ap_count: 25 - l2interface: TenGigabitEthernet1/1/6 - ip: 204.1.2.3 + - ip_address_list: + - 204.1.2.1 #It will be taken as 204.1.2.1 - 204.1.2.1 + - 205.2.1.1-205.2.1.10 discovery_type: "MULTI RANGE" - discovery_name: Multi_Range_Discovery_Test + discovery_name: File_111 protocol_order: ssh start_index: 1 records_to_return: 25 @@ -50,12 +42,8 @@ state: merged config_verify: True config: - - devices_list: #List length should be one - - name: SJ-BN-9300 - site: Global/USA/SAN JOSE/BLD23 - role: MAPSERVER,BORDERNODE,INTERNAL,EXTERNAL,SDATRANSIT - l2interface: TenGigabitEthernet1/1/8 - ip: 204.1.2.1 + - ip_address_list: #List length should be one + - 204.1.2.1 discovery_type: "CDP" #Can be LLDP and CIDR cdp_level: 16 #Instead use lldp for LLDP and prefix length for CIDR discovery_name: CDP_Test_1 diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index e15df25204..65be8144a9 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -22,6 +22,10 @@ author: Abinash Mishra (@abimishr) Phan Nguyen (phannguy) options: + config_verify: + description: Set to True to verify the Cisco DNA Center config after applying the playbook config. + type: bool + default: False state: description: The state of DNAC after module completion. type: str diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 43ac5e8d76..489568cff1 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -24,6 +24,10 @@ Rishita Chowdhary (@rishitachowdhary) Abinash Mishra (@abimishr) options: + config_verify: + description: Set to True to verify the Cisco DNA Center config after applying the playbook config. + type: bool + default: False state: description: The state of DNAC after module completion. type: str From 0ae93acfb3a9c6d091583dc0f4d9c9c29981eb02 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 1 Feb 2024 11:03:29 +0530 Subject: [PATCH 47/76] Address the issue of editing the management IP of a device that already exists in the inventory, add log level as debug in dnac.py get_task_details api --- plugins/module_utils/dnac.py | 2 +- plugins/modules/inventory_intent.py | 68 +++++++++++++++-------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 4e44c11bdf..9e7c26a979 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -237,7 +237,7 @@ def get_task_details(self, task_id): params={"task_id": task_id} ) - log(str(response)) + log(str(response), "DEBUG") if response and isinstance(response, dict): result = response.get('response') diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index cb5ac36c65..cbd25d722d 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2561,46 +2561,50 @@ def get_diff_merged(self, config): playbook_params.pop('snmpPrivProtocol', None) try: - response = self.dnac._exec( - family="devices", - function='sync_devices', - op_modifies=True, - params=playbook_params, - ) - self.log("Received API response from 'sync_devices': {0}".format(str(response)), "DEBUG") - - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') + if playbook_params['updateMgmtIPaddressList']: + new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] + if new_mgmt_ipaddress in self.have['device_in_dnac']: + self.status = "failed" + self.msg = "Device with IP address {0} already exists in Inventory".format(new_mgmt_ipaddress) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + else: + response = self.dnac._exec( + family="devices", + function='sync_devices', + op_modifies=True, + params=playbook_params, + ) + self.log("Received API response from 'sync_devices': {0}".format(str(response)), "DEBUG") - while True: - execution_details = self.get_task_details(task_id) + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') - if execution_details.get("endTime"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Device(s) {0} updated successfully".format(str(device_to_update)) - self.log(self.msg, "INFO") - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Device Updation get failed because of {0}".format(failure_reason) - else: - self.msg = "Device Updation get failed" - self.log(self.msg, "ERROR") - break + while True: + execution_details = self.get_task_details(task_id) + if execution_details.get("endTime"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Devices {0} present in Cisco Catalyst Center and updated successfully".format(str(device_to_update)) + self.log(self.msg, "INFO") + self.status = "success" + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Updation get failed" + self.log(self.msg, "ERROR") + break except Exception as e: error_message = "Error while updating device in Cisco Catalyst Center: {0}".format(str(e)) self.log(error_message, "ERROR") raise Exception(error_message) - self.msg = "Devices {0} present in Cisco Catalyst Center and updated successfully".format(str(device_to_update)) - self.log(self.msg, "INFO") - self.status = "success" - if self.config[0].get('update_interface_details'): # Call the Get interface details by device IP API and fetch the interface Id for device_ip in device_to_update: From b10c79c0c4129db80c0b0bb3bb457dd642a391c6 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 1 Feb 2024 13:51:57 +0530 Subject: [PATCH 48/76] removed extra self.status, add IP in single quote --- plugins/modules/inventory_intent.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index cbd25d722d..cc19571a31 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2565,7 +2565,7 @@ def get_diff_merged(self, config): new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] if new_mgmt_ipaddress in self.have['device_in_dnac']: self.status = "failed" - self.msg = "Device with IP address {0} already exists in Inventory".format(new_mgmt_ipaddress) + self.msg = "Device with IP address '{0}' already exists in inventory".format(new_mgmt_ipaddress) self.log(self.msg, "ERROR") self.result['response'] = self.msg else: @@ -2589,7 +2589,6 @@ def get_diff_merged(self, config): self.result['response'] = execution_details self.msg = "Devices {0} present in Cisco Catalyst Center and updated successfully".format(str(device_to_update)) self.log(self.msg, "INFO") - self.status = "success" break elif execution_details.get("isError"): self.status = "failed" From d7a7b533e32a205ea8ba13e083914a07b610989d Mon Sep 17 00:00:00 2001 From: Abinash Date: Thu, 1 Feb 2024 09:42:40 +0000 Subject: [PATCH 49/76] Removing dnac_log_level from code along with fixing issues related to discovery --- plugins/modules/discovery_intent.py | 46 ++++++++++++++++------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 65be8144a9..4d951f29b8 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -138,7 +138,7 @@ description: Time to wait for device response in seconds type: int cli_cred_len: - description: Total CLI credentials that needs to be used. This value can vary from 1 to 5. + description: Specifies the total number of CLI credentials to be used, ranging from 1 to 5. type: int default: 1 requirements: @@ -446,51 +446,48 @@ def get_devices_list_info(self): """ ip_address_list = self.validated_config[0].get('ip_address_list') self.result.update(dict(devices_info=ip_address_list)) - self.log("Devices list info passed: {0}".format(str(ip_address_list)), "INFO") + self.log("Details of the device list passed: {0}".format(str(ip_address_list)), "INFO") return ip_address_list - def preprocessing_devices_info(self, ip_address_list=None): + def preprocess_device_discovery(self, ip_address_list=None): """ Preprocess the devices' information. Extract the IP addresses from the list of devices and perform additional processing based on the 'discovery_type' in the validated configuration. Parameters: - - ip_address_list: The list of devices to preprocess. If not - provided, an empty list is used. + - ip_address_list: The list of devices' IP addresses intended for preprocessing. + If not provided, an empty list will be used. Returns: - - ip_address_list: If 'discovery_type' is "SINGLE", it returns the - first IP address. Otherwise, it returns a string - of IP ranges separated by commas. + - ip_address_list: It returns IP address list for the API to process. The value passed + for single, CDP, LLDP, CIDR, Range and Multi Range varies depending + on the need. """ if ip_address_list is None: ip_address_list = [] - - self.log("Discovery type passed for the discovery is {0}".format(self.validated_config[0].get('discovery_type')), "INFO") - if self.validated_config[0].get('discovery_type') in ["SINGLE", "CDP", "LLDP"]: + discovery_type = self.validated_config[0].get('discovery_type') + self.log("Discovery type passed for the discovery is {0}".format(discovery_type), "INFO") + if discovery_type in ["SINGLE", "CDP", "LLDP"]: if len(ip_address_list) == 1: ip_address_list = ip_address_list[0] else: - self.log("IP Address list's length is longer than 1", "ERROR") - self.module.fail_json(msg="IP Address list's length is longer than 1", response=[]) - elif self.validated_config[0].get('discovery_type') == "CIDR": + self.preprocess_device_discovery_handle_error() + elif discovery_type == "CIDR": if len(ip_address_list) == 1 and self.validated_config[0].get('prefix_length'): ip_address_list = ip_address_list[0] ip_address_list = str(ip_address_list) + "/" + str(self.validated_config[0].get('prefix_length')) else: - self.log("IP Address list's length is longer than 1", "ERROR") - self.module.fail_json(msg="IP Address list's length is longer than 1", response=[]) - elif self.validated_config[0].get('discovery_type') == "RANGE": + self.preprocess_device_discovery_handle_error() + elif discovery_type == "RANGE": if len(ip_address_list) == 1: if len(str(ip_address_list[0]).split("-")) == 2: ip_address_list = ip_address_list[0] else: ip_address_list = "{0}-{1}".format(ip_address_list[0], ip_address_list[0]) else: - self.log("IP Address list's length is longer than 1", "ERROR") - self.module.fail_json(msg="IP Address list's length is longer than 1", response=[]) + self.preprocess_device_discovery_handle_error() else: new_ip_collected = [] for ip in ip_address_list: @@ -501,6 +498,15 @@ def preprocessing_devices_info(self, ip_address_list=None): self.log("Collected IP address/addresses are {0}".format(str(ip_address_list)), "INFO") return str(ip_address_list) + def preprocess_device_discovery_handle_error(self): + """ + Method for failing discovery based on the length of list of IP Addresses passed + for performing discovery. + """ + + self.log("IP Address list's length is longer than 1", "ERROR") + self.module.fail_json(msg="IP Address list's length is longer than 1", response=[]) + def create_params(self, credential_ids=None, ip_address_list=None): """ Create a new parameter object based on the validated configuration, @@ -817,7 +823,7 @@ def get_diff_merged(self): self.get_dnac_global_credentials_v2_info() devices_list_info = self.get_devices_list_info() - ip_address_list = self.preprocessing_devices_info(devices_list_info) + ip_address_list = self.preprocess_device_discovery(devices_list_info) exist_discovery = self.get_exist_discovery() if exist_discovery: params = dict(id=exist_discovery.get('id')) From bbccba50c685c3b0ac872642c2f82865fdd4c64a Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 2 Feb 2024 09:39:43 +0530 Subject: [PATCH 50/76] Changes the log message printing format --- plugins/module_utils/dnac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 7e0be8f99d..ca073f5bc6 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -174,7 +174,7 @@ def setup_logger(self, logger_name): level = level_mapping.get(self.dnac_log_level, logging.WARNING) logger = logging.getLogger(logger_name) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(module)s:%(funcName)s:%(lineno)d --- %(message)s', datefmt='%m-%d-%Y %H:%M:%S') + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(module)s: %(funcName)s: %(lineno)d --- %(message)s', datefmt='%m-%d-%Y %H:%M:%S') file_handler = logging.FileHandler(self.dnac_log_file_path, mode=self.dnac_log_mode) file_handler.setFormatter(formatter) From 4bd9a15d94b157fd820a7bc0c903101289e5d420 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 2 Feb 2024 11:11:39 +0530 Subject: [PATCH 51/76] validation of SWIM image importing, golden tagging status of image, image distribution on single or list of devices, image activation on single or list of devices, code for updating management IP address with some dump ip --- plugins/modules/inventory_intent.py | 50 ++++++++-- plugins/modules/swim_intent.py | 141 ++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 9 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index c3ef063783..c3ed62cdf6 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -40,7 +40,7 @@ required: True suboptions: cli_transport: - description: Device's cli transport protocol. Required for Adding Network Devices. + description: Device's cli transport protocol(ssh/telnet). Required for Adding Network Devices. type: str compute_device: description: Compute Device flag. @@ -2567,6 +2567,37 @@ def get_diff_merged(self, config): self.msg = "Device with IP address '{0}' already exists in inventory".format(new_mgmt_ipaddress) self.log(self.msg, "ERROR") self.result['response'] = self.msg + else: + response = self.dnac._exec( + family="devices", + function='sync_devices', + op_modifies=True, + params=playbook_params, + ) + self.log("Received API response from 'sync_devices': {0}".format(str(response)), "DEBUG") + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + if execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Updation get failed" + self.log(self.msg, "ERROR") + break + elif execution_details.get("endTime"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Devices {0} present in Cisco Catalyst Center and updated successfully".format(str(device_to_update)) + self.log(self.msg, "INFO") + break + else: response = self.dnac._exec( family="devices", @@ -2582,14 +2613,7 @@ def get_diff_merged(self, config): while True: execution_details = self.get_task_details(task_id) - if execution_details.get("endTime"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Devices {0} present in Cisco Catalyst Center and updated successfully".format(str(device_to_update)) - self.log(self.msg, "INFO") - break - elif execution_details.get("isError"): + if execution_details.get("isError"): self.status = "failed" failure_reason = execution_details.get("failureReason") if failure_reason: @@ -2598,6 +2622,14 @@ def get_diff_merged(self, config): self.msg = "Device Updation get failed" self.log(self.msg, "ERROR") break + elif execution_details.get("endTime"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Devices {0} present in Cisco Catalyst Center and updated successfully".format(str(device_to_update)) + self.log(self.msg, "INFO") + break + except Exception as e: error_message = "Error while updating device in Cisco Catalyst Center: {0}".format(str(e)) self.log(error_message, "ERROR") diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 52f8955653..b8263a92a6 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -30,6 +30,10 @@ Rishita Chowdhary (@rishitachowdhary) Abhishek Maheshwari (@abmahesh) options: + config_verify: + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. + type: bool + default: False state: description: The state of Catalyst Center after module completion. type: str @@ -1084,6 +1088,7 @@ def get_diff_distribution(self): image_id = self.have.get("distribution_image_id") if self.have.get("distribution_device_id"): + self.single_device_distribution = False distribution_params = dict( payload=[dict( deviceUuid=self.have.get("distribution_device_id"), @@ -1111,6 +1116,7 @@ def get_diff_distribution(self): ("completed successfully" in task_details.get("progress")): self.result['changed'] = True self.status = "success" + self.single_device_distribution = True self.result['msg'] = "Image with Id {0} Distributed Successfully".format(image_id) break @@ -1136,6 +1142,9 @@ def get_diff_distribution(self): device_distribution_count = 0 device_ips_list = [] + self.complete_successful_distribution = False + self.partial_successful_distribution = False + for device_uuid in device_uuid_list: device_management_ip = self.get_device_ip_from_id(device_uuid) distribution_params = dict( @@ -1181,10 +1190,12 @@ def get_diff_distribution(self): elif device_distribution_count == len(device_uuid_list): self.result['changed'] = True self.status = "success" + self.complete_successful_distribution = True self.msg = "Image with Id {0} Distributed Successfully for all devices".format(image_id) else: self.result['changed'] = True self.status = "success" + self.partial_successful_distribution = False self.msg = "Image with Id '{0}' Distributed and partially successfull".format(image_id) self.log("For device(s) {0} image Distribution gets failed".format(str(device_ips_list)), "CRITICAL") @@ -1214,6 +1225,7 @@ def get_diff_activation(self): image_id = self.have.get("activation_image_id") if self.have.get("activation_device_id"): + self.single_device_activation = False payload = [dict( activateLowerImageVersion=activation_details.get("activate_lower_image_version"), deviceUpgradeMode=activation_details.get("device_upgrade_mode"), @@ -1247,6 +1259,7 @@ def get_diff_activation(self): self.result['changed'] = True self.result['msg'] = "Image Activated successfully" self.status = "success" + self.single_device_activation = True break if task_details.get("isError"): @@ -1271,6 +1284,8 @@ def get_diff_activation(self): self.log("Device UUIDs involved in Image Activation: {0}".format(str(device_uuid_list)), "INFO") device_activation_count = 0 device_ips_list = [] + self.complete_successful_activation = False + self.partial_successful_activation = False for device_uuid in device_uuid_list: device_management_ip = self.get_device_ip_from_id(device_uuid) @@ -1324,10 +1339,12 @@ def get_diff_activation(self): elif device_activation_count == len(device_uuid_list): self.result['changed'] = True self.status = "success" + self.complete_successful_activation = True msg = "Image with Id '{0}' activated successfully for all devices".format(image_id) else: self.result['changed'] = True self.status = "success" + self.partial_successful_activation = True msg = "Image with Id '{0}' activated and partially successfull".format(image_id) self.log("For Device(s) {0} Image activation gets Failed".format(str(device_ips_list)), "CRITICAL") @@ -1362,6 +1379,125 @@ def get_diff_merged(self, config): return self + def verify_diff_merged(self, config): + """ + Verify the merged status(Importing/Tagging/Distributing/Actiavting) the SWIM Image in devices in Cisco Catalyst Center. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the merged status of a configuration in Cisco Catalyst Center by retrieving the current state + (have) and desired state (want) of the configuration, logs the states, and validates whether the specified + SWIM operation performed or not. + """ + + self.get_have() + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + + import_type = self.want.get("import_type") + + if import_type: + if import_type == "url": + image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") + else: + image_name = self.want.get("local_import_details").get("file_path") + + # Code to check if the image already exists in Catalyst Center + name = image_name.split('/')[-1] + image_exist = self.is_image_exist(name) + if image_exist: + self.status = "success" + self.msg = "The requested Image '{0}' imported in the Cisco Catalyst Center and Image presence has been verified.".format(name) + self.log(self.msg, "INFO") + else: + self.log("""The playbook input for SWIM Image '{0}' does not align with the Cisco Catalyst Center, indicating that image + may not have imported successfully.""".format(name), "INFO") + + if self.want.get("tagging_details"): + tagging_details = self.want.get("tagging_details") + tag_image_golden = tagging_details.get("tagging") + + image_params = dict( + image_id=self.have.get("tagging_image_id"), + site_id=self.have.get("site_id"), + device_family_identifier=self.have.get("device_family_identifier"), + device_role=tagging_details.get("device_role", "ALL").upper() + ) + self.log("Parameters for checking the status of image: {0}".format(str(image_params)), "INFO") + + response = self.dnac._exec( + family="software_image_management_swim", + function='get_golden_tag_status_of_an_image', + op_modifies=True, + params=image_params + ) + self.log("Received API response from 'get_golden_tag_status_of_an_image': {0}".format(str(response)), "DEBUG") + + response = response.get('response') + if response: + image_status = response['taggedGolden'] + if image_status == tag_image_golden: + if tag_image_golden: + self.msg = """The requested image '{0}' has been tagged as golden in the Cisco Catalyst Center and image status + has been verified.""".format(name) + self.log(self.msg, "INFO") + else: + self.msg = """The requested image '{0}' has been un-tagged as golden in the Cisco Catalyst Center and + image status has been verified.""".format(name) + self.log(self.msg, "INFO") + else: + self.log("""Mismatch between the playbook input for tagging/un-tagging image as golden and the Cisco Catalyst Center indicates that + the tagging/un-tagging task was not executed successfully.""", "INFO") + + if self.want.get("distribution_details"): + image_id = self.have.get("distribution_image_id") + + if self.have.get("distribution_device_id"): + if self.single_device_distribution: + self.msg = """The requested image with device id '{0}' has been distributed successfully in the Cisco Catalyst Center and + image status has been verified.""".format(self.have.get("distribution_device_id")) + self.log(self.msg, "INFO") + else: + self.log("""Mismatch between the playbook input for distributing image to device with id '{0}' and the Cisco Catalyst Center indicates that + the distribution task was not executed successfully.""".format(self.have.get("distribution_device_id")), "INFO") + elif self.complete_successful_distribution: + self.msg = """The requested image with id '{0}' distributed successfully on all devices in that particular site + in the Cisco Catalyst Center.""".format(image_id) + self.log(self.msg, "INFO") + elif self.partial_successful_distribution: + self.msg = "The requested image with id '{0}' distributed partially on some devices in the Cisco Catalyst Center.".format(image_id) + self.log(self.msg, "INFO") + else: + self.msg = "The requested image with id '{0}' failed to distributed on devices in the Cisco Catalyst Center.".format(image_id) + self.log(self.msg, "INFO") + + if self.want.get("activation_details"): + image_id = self.have.get("activation_image_id") + + if self.have.get("activation_device_id"): + if self.single_device_activation: + self.msg = """The requested image with device id '{0}' has been activated successfully in the Cisco Catalyst Center and + image status has been verified.""".format(self.have.get("activation_device_id")) + self.log(self.msg, "INFO") + else: + self.log("""Mismatch between the playbook input for activating image to device with id '{0}' and the Cisco Catalyst Center indicates that + the activation task was not executed successfully.""".format(self.have.get("activation_device_id")), "INFO") + elif self.complete_successful_activation: + self.msg = """The requested image with id '{0}' activated successfully on all devices in that particular site + in the Cisco Catalyst Center.""".format(image_id) + self.log(self.msg, "INFO") + elif self.partial_successful_activation: + self.msg = "The requested image with id '{0}' activated partially on some devices in the Cisco Catalyst Center.".format(image_id) + self.log(self.msg, "INFO") + else: + self.msg = "The requested image with id '{0}' failed to activate on devices in the Cisco Catalyst Center.".format(image_id) + self.log(self.msg, "INFO") + + return self + def main(): """ main entry point for module execution @@ -1377,6 +1513,7 @@ def main(): 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config_verify': {'type': 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, 'state': {'default': 'merged', 'choices': ['merged']} } @@ -1393,12 +1530,16 @@ def main(): dnac_swims.check_return_status() dnac_swims.validate_input().check_return_status() + config_verify = dnac_swims.params.get("config_verify") + for config in dnac_swims.validated_config: dnac_swims.reset_values() dnac_swims.get_want(config).check_return_status() dnac_swims.get_diff_import().check_return_status() dnac_swims.get_have().check_return_status() dnac_swims.get_diff_state_apply[state](config).check_return_status() + if config_verify: + dnac_swims.verify_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_swims.result) From e6032c02bb9802c619fd0cad3f10c5a17d6286a1 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 2 Feb 2024 15:27:44 +0530 Subject: [PATCH 52/76] add seperate API for each swim verify operation validation and make log messages more readable --- plugins/modules/swim_intent.py | 306 +++++++++++++++++++++++---------- 1 file changed, 213 insertions(+), 93 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index b8263a92a6..93aadd8308 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -516,6 +516,40 @@ def get_image_id(self, name): return image_id + def get_image_name_from_id(self, image_id): + """ + Retrieve the unique image name based on the provided image id. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + id (str): The unique image ID (UUID) of the software image to search for. + Returns: + str: The image name corresponding to the given unique image ID (UUID) + Raises: + AnsibleFailJson: If the image is not found in the response. + Description: + This function sends a request to Cisco Catalyst Center to retrieve details about a software image based on its id. + It extracts and returns the image name if a single matching image is found. If no image or multiple + images are found with the same name, it raises an exception. + """ + + image_response = self.dnac._exec( + family="software_image_management_swim", + function='get_software_image_details', + params={"image_uuid": image_id}, + ) + self.log("Received API response from 'get_software_image_details': {0}".format(str(image_response)), "DEBUG") + image_list = image_response.get("response") + + if (len(image_list) == 1): + image_name = image_list[0].get("name") + self.log("SWIM image '{0}' has been fetched successfully from Cisco Catalyst Center".format(image_name), "INFO") + else: + error_message = "SWIM image with Id '{0}' could not be found in Cisco Catalyst Center".format(image_id) + self.log(error_message, "ERROR") + self.module.fail_json(msg=error_message, response=image_response) + + return image_id + def is_image_exist(self, name): """ Retrieve the unique image ID based on the provided image name. @@ -1379,6 +1413,175 @@ def get_diff_merged(self, config): return self + def verify_diff_imported(self, import_type): + """ + Verify the successful import of a software image into Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + import_type (str): The type of import, either 'url' or 'local'. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method verifies the successful import of a software image into Cisco Catalyst Center. + It checks whether the image exists in Catalyst Center based on the provided import type. + If the image exists, the status is set to 'success', and a success message is logged. + If the image does not exist, a warning message is logged indicating a potential import failure. + """ + + if import_type == "url": + image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") + else: + image_name = self.want.get("local_import_details").get("file_path") + + # Code to check if the image already exists in Catalyst Center + name = image_name.split('/')[-1] + image_exist = self.is_image_exist(name) + if image_exist: + self.status = "success" + self.msg = "The requested Image '{0}' imported in the Cisco Catalyst Center and Image presence has been verified.".format(name) + self.log(self.msg, "INFO") + else: + self.log("""The playbook input for SWIM Image '{0}' does not align with the Cisco Catalyst Center, indicating that image + may not have imported successfully.""".format(name), "INFO") + + return self + + def verify_diff_tagged(self): + """ + Verify the Golden tagging status of a software image in Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method verifies the tagging status of a software image in Cisco Catalyst Center. + It retrieves tagging details from the input, including the desired tagging status and image ID. + Using the provided image ID, it obtains image parameters required for checking the image status. + The method then queries Catalyst Center to get the golden tag status of the image. + If the image status matches the desired tagging status, a success message is logged. + If there is a mismatch between the playbook input and the Catalyst Center, a warning message is logged. + """ + + tagging_details = self.want.get("tagging_details") + tag_image_golden = tagging_details.get("tagging") + image_id = self.have.get("tagging_image_id") + image_name = self.get_image_name_from_id(image_id) + + image_params = dict( + image_id=self.have.get("tagging_image_id"), + site_id=self.have.get("site_id"), + device_family_identifier=self.have.get("device_family_identifier"), + device_role=tagging_details.get("device_role", "ALL").upper() + ) + self.log("Parameters for checking the status of image: {0}".format(str(image_params)), "INFO") + + response = self.dnac._exec( + family="software_image_management_swim", + function='get_golden_tag_status_of_an_image', + op_modifies=True, + params=image_params + ) + self.log("Received API response from 'get_golden_tag_status_of_an_image': {0}".format(str(response)), "DEBUG") + + response = response.get('response') + if response: + image_status = response['taggedGolden'] + if image_status == tag_image_golden: + if tag_image_golden: + self.msg = """The requested image '{0}' has been tagged as golden in the Cisco Catalyst Center and + its status has been successfully verified.""".format(image_name) + self.log(self.msg, "INFO") + else: + self.msg = """The requested image '{0}' has been un-tagged as golden in the Cisco Catalyst Center and + image status has been verified.""".format(image_name) + self.log(self.msg, "INFO") + else: + self.log("""Mismatch between the playbook input for tagging/un-tagging image as golden and the Cisco Catalyst Center indicates that + the tagging/un-tagging task was not executed successfully.""", "INFO") + + return self + + def verify_diff_distributed(self): + """ + Verify the distribution status of a software image in Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + import_type (str): The type of import, either 'url' or 'local'. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method verifies the distribution status of a software image in Cisco Catalyst Center. + It retrieves the image ID and name from the input and if distribution device ID is provided, it checks the distribution status for that + list of specific device and logs the info message based on distribution status. + """ + + image_id = self.have.get("distribution_image_id") + image_name = self.get_image_name_from_id(image_id) + + if self.have.get("distribution_device_id"): + if self.single_device_distribution: + self.msg = """The requested image '{0}', associated with the device ID '{1}', has been successfully distributed in the Cisco Catalyst Center + and its status has been verified.""".format(image_name, self.have.get("distribution_device_id")) + self.log(self.msg, "INFO") + else: + self.log("""Mismatch between the playbook input for distributing the image to the device with ID '{0}' and the actual state in the + Cisco Catalyst Center suggests that the distribution task might not have been executed + successfully.""".format(self.have.get("distribution_device_id")), "INFO") + elif self.complete_successful_distribution: + self.msg = """The requested image '{0}', with ID '{1}', has been successfully distributed to all devices within the specified + site in the Cisco Catalyst Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + elif self.partial_successful_distribution: + self.msg = """T"The requested image '{0}', with ID '{1}', has been partially distributed across some devices in the Cisco Catalyst + Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + else: + self.msg = """The requested image '{0}', with ID '{1}', failed to be distributed across devices in the Cisco Catalyst + Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + + return self + + def verify_diff_activated(self): + """ + Verify the activation status of a software image in Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method verifies the activation status of a software image in Cisco Catalyst Center and retrieves the image ID and name from + the input. If activation device ID is provided, it checks the activation status for that specific device. Based on activation status + a corresponding message is logged. + """ + + image_id = self.have.get("activation_image_id") + image_name = self.get_image_name_from_id(image_id) + + if self.have.get("activation_device_id"): + if self.single_device_activation: + self.msg = """The requested image '{0}', associated with the device ID '{1}', has been successfully activated in the Cisco Catalyst + Center and its status has been verified.""".format(image_name, self.have.get("activation_device_id")) + self.log(self.msg, "INFO") + else: + self.log("""Mismatch between the playbook's input for activating the image '{0}' on the device with ID '{1}' and the actual state in + the Cisco Catalyst Center suggests that the activation task might not have been executed + successfully.""".format(image_name, self.have.get("activation_device_id")), "INFO") + elif self.complete_successful_activation: + self.msg = """The requested image '{0}', with ID '{1}', has been successfully activated on all devices within the specified site in the + Cisco Catalyst Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + elif self.partial_successful_activation: + self.msg = """"The requested image '{0}', with ID '{1}', has been partially activated on some devices in the Cisco + Catalyst Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + else: + self.msg = """The activation of the requested image '{0}', with ID '{1}', failed on devices in the Cisco + Catalyst Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + + return self + def verify_diff_merged(self, config): """ Verify the merged status(Importing/Tagging/Distributing/Actiavting) the SWIM Image in devices in Cisco Catalyst Center. @@ -1398,103 +1601,20 @@ def verify_diff_merged(self, config): self.log("Desired State (want): {0}".format(str(self.want)), "INFO") import_type = self.want.get("import_type") - if import_type: - if import_type == "url": - image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") - else: - image_name = self.want.get("local_import_details").get("file_path") + self.verify_diff_imported(import_type).check_return_status() - # Code to check if the image already exists in Catalyst Center - name = image_name.split('/')[-1] - image_exist = self.is_image_exist(name) - if image_exist: - self.status = "success" - self.msg = "The requested Image '{0}' imported in the Cisco Catalyst Center and Image presence has been verified.".format(name) - self.log(self.msg, "INFO") - else: - self.log("""The playbook input for SWIM Image '{0}' does not align with the Cisco Catalyst Center, indicating that image - may not have imported successfully.""".format(name), "INFO") - - if self.want.get("tagging_details"): - tagging_details = self.want.get("tagging_details") - tag_image_golden = tagging_details.get("tagging") + tagged = self.want.get("tagging_details") + if tagged: + self.verify_diff_tagged().check_return_status() - image_params = dict( - image_id=self.have.get("tagging_image_id"), - site_id=self.have.get("site_id"), - device_family_identifier=self.have.get("device_family_identifier"), - device_role=tagging_details.get("device_role", "ALL").upper() - ) - self.log("Parameters for checking the status of image: {0}".format(str(image_params)), "INFO") - - response = self.dnac._exec( - family="software_image_management_swim", - function='get_golden_tag_status_of_an_image', - op_modifies=True, - params=image_params - ) - self.log("Received API response from 'get_golden_tag_status_of_an_image': {0}".format(str(response)), "DEBUG") - - response = response.get('response') - if response: - image_status = response['taggedGolden'] - if image_status == tag_image_golden: - if tag_image_golden: - self.msg = """The requested image '{0}' has been tagged as golden in the Cisco Catalyst Center and image status - has been verified.""".format(name) - self.log(self.msg, "INFO") - else: - self.msg = """The requested image '{0}' has been un-tagged as golden in the Cisco Catalyst Center and - image status has been verified.""".format(name) - self.log(self.msg, "INFO") - else: - self.log("""Mismatch between the playbook input for tagging/un-tagging image as golden and the Cisco Catalyst Center indicates that - the tagging/un-tagging task was not executed successfully.""", "INFO") - - if self.want.get("distribution_details"): - image_id = self.have.get("distribution_image_id") - - if self.have.get("distribution_device_id"): - if self.single_device_distribution: - self.msg = """The requested image with device id '{0}' has been distributed successfully in the Cisco Catalyst Center and - image status has been verified.""".format(self.have.get("distribution_device_id")) - self.log(self.msg, "INFO") - else: - self.log("""Mismatch between the playbook input for distributing image to device with id '{0}' and the Cisco Catalyst Center indicates that - the distribution task was not executed successfully.""".format(self.have.get("distribution_device_id")), "INFO") - elif self.complete_successful_distribution: - self.msg = """The requested image with id '{0}' distributed successfully on all devices in that particular site - in the Cisco Catalyst Center.""".format(image_id) - self.log(self.msg, "INFO") - elif self.partial_successful_distribution: - self.msg = "The requested image with id '{0}' distributed partially on some devices in the Cisco Catalyst Center.".format(image_id) - self.log(self.msg, "INFO") - else: - self.msg = "The requested image with id '{0}' failed to distributed on devices in the Cisco Catalyst Center.".format(image_id) - self.log(self.msg, "INFO") - - if self.want.get("activation_details"): - image_id = self.have.get("activation_image_id") + distribution_details = self.want.get("distribution_details") + if distribution_details: + self.verify_diff_distributed().check_return_status() - if self.have.get("activation_device_id"): - if self.single_device_activation: - self.msg = """The requested image with device id '{0}' has been activated successfully in the Cisco Catalyst Center and - image status has been verified.""".format(self.have.get("activation_device_id")) - self.log(self.msg, "INFO") - else: - self.log("""Mismatch between the playbook input for activating image to device with id '{0}' and the Cisco Catalyst Center indicates that - the activation task was not executed successfully.""".format(self.have.get("activation_device_id")), "INFO") - elif self.complete_successful_activation: - self.msg = """The requested image with id '{0}' activated successfully on all devices in that particular site - in the Cisco Catalyst Center.""".format(image_id) - self.log(self.msg, "INFO") - elif self.partial_successful_activation: - self.msg = "The requested image with id '{0}' activated partially on some devices in the Cisco Catalyst Center.".format(image_id) - self.log(self.msg, "INFO") - else: - self.msg = "The requested image with id '{0}' failed to activate on devices in the Cisco Catalyst Center.".format(image_id) - self.log(self.msg, "INFO") + activation_details = self.want.get("activation_details") + if activation_details: + self.verify_diff_activated().check_return_status() return self From d71b2730a36f7c02c973ebca7b3a67f7b2635722 Mon Sep 17 00:00:00 2001 From: Abinash Date: Fri, 2 Feb 2024 12:51:26 +0000 Subject: [PATCH 53/76] Changes for typos, fixed CIDR issue and added delete all feature --- plugins/modules/discovery_intent.py | 124 +++++++++++++++++----------- 1 file changed, 77 insertions(+), 47 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 4d951f29b8..161097db22 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -58,6 +58,7 @@ start_index: description: Start index for the header in fetching SNMP v2 credentials type: int + default: 1 enable_password_list: description: List of enable passwords for the CLI crfedentials type: list @@ -65,6 +66,7 @@ records_to_return: description: Number of records to return for the header in fetching global v2 credentials type: int + default: 100 http_read_credential: description: HTTP read credentials for hosting a device type: dict @@ -141,6 +143,10 @@ description: Specifies the total number of CLI credentials to be used, ranging from 1 to 5. type: int default: 1 + delete_all: + description: Parameter to delete all the discoveries at one go + type: bool + default: False requirements: - dnacentersdk == 2.6.10 - python >= 3.5 @@ -152,6 +158,8 @@ discovery.Discovery.get_discoveries_by_range, discovery.Discovery.get_discovered_network_devices_by_discovery_id', discovery.Discovery.delete_discovery_by_id + discovery.Discovery.delete_all_discovery + discovery.Discovery.get_count_of_all_discovery_jobs - Paths used are get /dna/intent/api/v2/global-credential @@ -160,6 +168,8 @@ get /dna/intent/api/v1/discovery/{startIndex}/{recordsToReturn} get /dna/intent/api/v1/discovery/{id}/network-device delete /dna/intent/api/v1/discovery/{id} + delete /dna/intent/api/v1/delete + get /dna/intent/api/v1/discovery/count """ @@ -206,7 +216,7 @@ snmp_version: string timeout: integer username_list: list - cli_cred_length: integer + cli_cred_len: integer - name: Delete disovery by name cisco.dnac.discovery_intent: dnac_host: "{{dnac_host}}" @@ -221,10 +231,7 @@ state: deleted config_verify: True config: - - ip_address_list: list - start_index: integer - records_to_return: integer - discovery_name: string + - discovery_name: string """ RETURN = r""" @@ -293,7 +300,7 @@ def __init__(self, module): super().__init__(module) self.creds_ids_list = [] - def validate_input(self): + def validate_input(self, state=None): """ Validate the fields provided in the playbook. Checks the configuration provided in the playbook against a predefined @@ -322,22 +329,18 @@ def validate_input(self): discovery_spec = { 'cdp_level': {'type': 'int', 'required': False, 'default': 16}, - 'discovery_type': {'type': 'str', 'required': True}, 'enable_password_list': {'type': 'list', 'required': False, 'elements': 'str'}, - 'ip_address_list': {'type': 'list', 'required': True, - 'elements': 'str'}, 'start_index': {'type': 'int', 'required': False, - 'default': 25}, - 'records_to_return': {'type': 'int', 'required': False}, + 'default': 1}, + 'records_to_return': {'type': 'int', 'required': False, + 'default': 100}, 'http_read_credential': {'type': 'dict', 'required': False}, 'http_write_credential': {'type': 'dict', 'required': False}, 'ip_filter_list': {'type': 'list', 'required': False, 'elements': 'str'}, 'lldp_level': {'type': 'int', 'required': False, 'default': 16}, - 'prefix_length': {'type': 'int', 'required': False, - 'default': 30}, 'discovery_name': {'type': 'str', 'required': True}, 'netconf_port': {'type': 'str', 'required': False}, 'password_list': {'type': 'list', 'required': False, @@ -364,6 +367,17 @@ def validate_input(self): 'default': 1} } + if state == "merged": + discovery_spec["ip_address_list"] = {'type': 'list', 'required': True, + 'elements': 'str'} + discovery_spec["discovery_type"] = {'type': 'str', 'required': True} + + if (self.config[0].get("delete_all") is True and state == "deleted"): + self.validated_config = [{"delete_all": True}] + self.msg = "Sucessfully collected input for deletion of all the discoveries" + self.log(self.msg, "WARNING") + return self + # Validate discovery params valid_discovery, invalid_params = validate_list_of_dicts( self.config, discovery_spec @@ -475,9 +489,11 @@ def preprocess_device_discovery(self, ip_address_list=None): else: self.preprocess_device_discovery_handle_error() elif discovery_type == "CIDR": - if len(ip_address_list) == 1 and self.validated_config[0].get('prefix_length'): - ip_address_list = ip_address_list[0] - ip_address_list = str(ip_address_list) + "/" + str(self.validated_config[0].get('prefix_length')) + if len(ip_address_list) == 1: + if len(ip_address_list[0].split("/")) == 2: + ip_address_list = ip_address_list[0] + else: + ip_address_list = str(ip_address_list[0]) + "/" + str("30") else: self.preprocess_device_discovery_handle_error() elif discovery_type == "RANGE": @@ -493,7 +509,9 @@ def preprocess_device_discovery(self, ip_address_list=None): for ip in ip_address_list: if len(str(ip).split("-")) != 2: ip_collected = "{0}-{0}".format(ip) - new_ip_collected.append(ip_collected) + new_ip_collected.append(ip_collected) + else: + new_ip_collected.append(ip) ip_address_list = ','.join(new_ip_collected) self.log("Collected IP address/addresses are {0}".format(str(ip_address_list)), "INFO") return str(ip_address_list) @@ -567,7 +585,7 @@ def create_params(self, credential_ids=None, ip_address_list=None): 'snmp_username') new_object_params['snmpVersion'] = self.validated_config[0].get('snmp_version') new_object_params['timeout'] = self.validated_config[0].get('timeout') - new_object_params['userNameList'] = self.validated_config[0].get('user_name_list') + new_object_params['userNameList'] = self.validated_config[0].get('username_list') self.log("The payload/object created for calling the start discovery API is {0}".format(str(new_object_params)), "INFO") return new_object_params @@ -855,20 +873,43 @@ def get_diff_deleted(self): - self: The instance of the class with updated attributes. """ - exist_discovery = self.get_exist_discovery() - if not exist_discovery: - self.result['msg'] = "Discovery {0} Not Found".format( - self.validated_config[0].get("discovery_name")) - self.log(self.result['msg'], "ERROR") - return self + if self.validated_config[0].get("delete_all"): + count_discoveries = self.dnac_apply['exec']( + family="discovery", + function="get_count_of_all_discovery_jobs", + ) + if count_discoveries.get("response") == 0: + msg = "There are no discoveries present in the Discovery Dashboard for deletion" + self.result['msg'] = msg + self.log(msg, "WARNING") + self.result['response'] = self.validated_config[0] + return self + + delete_all_response = self.dnac_apply['exec']( + family="discovery", + function="delete_all_discovery", + ) + discovery_task_id = delete_all_response.get('response').get('taskId') + self.result["changed"] = True + self.result['msg'] = "All of the Discoveries Deleted Successfully" + self.result['diff'] = self.validated_config + + else: + exist_discovery = self.get_exist_discovery() + if not exist_discovery: + self.result['msg'] = "Discovery {0} Not Found".format( + self.validated_config[0].get("discovery_name")) + self.log(self.result['msg'], "ERROR") + return self + + params = dict(id=exist_discovery.get('id')) + discovery_task_id = self.delete_exist_discovery(params=params) + complete_discovery = self.get_task_status(task_id=discovery_task_id) + self.result["changed"] = True + self.result['msg'] = "Discovery Deleted Successfully" + self.result['diff'] = self.validated_config + self.result['response'] = discovery_task_id - params = dict(id=exist_discovery.get('id')) - discovery_task_id = self.delete_exist_discovery(params=params) - complete_discovery = self.get_task_status(task_id=discovery_task_id) - self.result["changed"] = True - self.result['msg'] = "Discovery Deleted Successfully" - self.result['diff'] = self.validated_config - self.result['response'] = discovery_task_id self.log(self.result['msg'], "INFO") return self @@ -900,9 +941,8 @@ def verify_diff_merged(self, config): function='get_discovery_by_id', params=params ) - + discovery_name = config.get('discovery_name') if response: - discovery_name = response.get('response').get('name') self.log("Requested Discovery with name {0} is completed".format(discovery_name), "INFO") else: @@ -928,19 +968,9 @@ def verify_diff_deleted(self, config): self.log("Current State (have): {0}".format(str(self.have)), "INFO") self.log("Desired State (want): {0}".format(str(config)), "INFO") # Code to validate dnac config for deleted state - discovery_task_info = self.get_discoveries_by_range_until_success() - discovery_id = discovery_task_info.get('id') - params = dict( - id=discovery_id - ) - response = self.dnac_apply['exec']( - family="discovery", - function='get_discovery_by_id', - params=params - ) - - if response: - discovery_name = response.get('response').get('name') + discovery_task_info = self.lookup_discovery_by_range_via_name() + discovery_name = config.get('discovery_name') + if discovery_task_info: self.log("Requested Discovery with name {0} is present".format(discovery_name), "WARNING") else: @@ -981,7 +1011,7 @@ def main(): dnac_discovery.msg = "State {0} is invalid".format(state) dnac_discovery.check_return_status() - dnac_discovery.validate_input().check_return_status() + dnac_discovery.validate_input(state=state).check_return_status() for config in dnac_discovery.validated_config: dnac_discovery.reset_values() dnac_discovery.get_diff_state_apply[state]().check_return_status() From 8c6192bb092ee61899a835b3f5594fbe4451ba0b Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 2 Feb 2024 21:34:32 +0530 Subject: [PATCH 54/76] Added the compatibility to update specific credential/info of list of devices keeping the other previous device details as same. --- plugins/modules/inventory_intent.py | 200 +++++++++++++++------------- 1 file changed, 106 insertions(+), 94 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index c3ed62cdf6..448f9b5ca4 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -40,7 +40,8 @@ required: True suboptions: cli_transport: - description: Device's cli transport protocol(ssh/telnet). Required for Adding Network Devices. + description: The essential prerequisite for adding Network devices is the specification of the transport + protocol (either SSH or Telnet) used by the device. type: str compute_device: description: Compute Device flag. @@ -2512,62 +2513,104 @@ def get_diff_merged(self, config): self.check_return_status() csv_reader = self.decrypt_and_read_csv(export_response, password) self.check_return_status() - device_data = next(csv_reader, None) - playbook_params = self.want.get("device_params") - playbook_params['ipAddress'] = device_to_update - - if not playbook_params['cliTransport']: - playbook_params['cliTransport'] = device_data['protocol'] - if not playbook_params['snmpPrivProtocol']: - playbook_params['snmpPrivProtocol'] = device_data['snmpv3_privacy_type'] - - csv_data_dict = { - 'username': device_data['cli_username'], - 'password': device_data['cli_password'], - 'enable_password': device_data['cli_enable_password'], - } + device_details = {} + + for row in csv_reader: + ip_address = row['ip_address'] + device_details[ip_address] = row + + for device_ip in device_to_update: + playbook_params = self.want.get("device_params").copy() + playbook_params['ipAddress'] = [device_ip] + device_data = device_details[device_ip] + + if not playbook_params['cliTransport']: + playbook_params['cliTransport'] = device_data['protocol'] + if not playbook_params['snmpPrivProtocol']: + playbook_params['snmpPrivProtocol'] = device_data['snmpv3_privacy_type'] + + csv_data_dict = { + 'username': device_data['cli_username'], + 'password': device_data['cli_password'], + 'enable_password': device_data['cli_enable_password'], + } + + if device_data['snmp_version'] == '3': + csv_data_dict['snmp_username'] = device_data['snmpv3_user_name'] + if device_data['snmpv3_privacy_password']: + csv_data_dict['snmp_auth_passphrase'] = device_data['snmpv3_auth_password'] + csv_data_dict['snmp_priv_passphrase'] = device_data['snmpv3_privacy_password'] + + device_key_mapping = { + 'username': 'userName', + 'password': 'password', + 'enable_password': 'enablePassword', + 'snmp_username': 'snmpUserName' + } + device_update_key_list = ["username", "password", "enable_password", "snmp_username"] + + for key in device_update_key_list: + mapped_key = device_key_mapping[key] + + if playbook_params[mapped_key] is None: + if playbook_params['snmpMode'] == "AUTHPRIV": + playbook_params['snmpAuthPassphrase'] = csv_data_dict['snmp_auth_passphrase'] + playbook_params['snmpPrivPassphrase'] = csv_data_dict['snmp_priv_passphrase'] + playbook_params[mapped_key] = csv_data_dict[key] + + if playbook_params['snmpMode'] == "NOAUTHNOPRIV": + playbook_params.pop('snmpAuthPassphrase', None) + playbook_params.pop('snmpPrivPassphrase', None) + playbook_params.pop('snmpPrivProtocol', None) + playbook_params.pop('snmpAuthProtocol', None) + elif playbook_params['snmpMode'] == "AUTHNOPRIV": + playbook_params.pop('snmpPrivPassphrase', None) + playbook_params.pop('snmpPrivProtocol', None) + + try: + if playbook_params['updateMgmtIPaddressList']: + new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] + if new_mgmt_ipaddress in self.have['device_in_dnac']: + self.status = "failed" + self.msg = "Device with IP address '{0}' already exists in inventory".format(new_mgmt_ipaddress) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + else: + self.log("Playbook parameter for updating device new management ip address: {0}".format(str(playbook_params)), "DEBUG") + response = self.dnac._exec( + family="devices", + function='sync_devices', + op_modifies=True, + params=playbook_params, + ) + self.log("Received API response from 'sync_devices': {0}".format(str(response)), "DEBUG") + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + if execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = """Device new management IP updation for device '{0}' + get failed due to {1}""".format(device_ip, failure_reason) + else: + self.msg = "Device new management IP updation for device '{0}' get failed".format(device_ip) + self.log(self.msg, "ERROR") + break + elif execution_details.get("endTime"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = """Device '{0}' present in Cisco Catalyst Center and new management ip '{1}' have been + updated successfully""".format(device_ip, new_mgmt_ipaddress) + self.log(self.msg, "INFO") + break - if device_data['snmp_version'] == '3': - csv_data_dict['snmp_username'] = device_data['snmpv3_user_name'] - if device_data['snmpv3_privacy_password']: - csv_data_dict['snmp_auth_passphrase'] = device_data['snmpv3_auth_password'] - csv_data_dict['snmp_priv_passphrase'] = device_data['snmpv3_privacy_password'] - - device_key_mapping = { - 'username': 'userName', - 'password': 'password', - 'enable_password': 'enablePassword', - 'snmp_username': 'snmpUserName' - } - device_update_key_list = ["username", "password", "enable_password", "snmp_username"] - - for key in device_update_key_list: - mapped_key = device_key_mapping[key] - - if playbook_params[mapped_key] is None: - if playbook_params['snmpMode'] == "AUTHPRIV": - playbook_params['snmpAuthPassphrase'] = csv_data_dict['snmp_auth_passphrase'] - playbook_params['snmpPrivPassphrase'] = csv_data_dict['snmp_priv_passphrase'] - playbook_params[mapped_key] = csv_data_dict[key] - - if playbook_params['snmpMode'] == "NOAUTHNOPRIV": - playbook_params.pop('snmpAuthPassphrase', None) - playbook_params.pop('snmpPrivPassphrase', None) - playbook_params.pop('snmpPrivProtocol', None) - playbook_params.pop('snmpAuthProtocol', None) - elif playbook_params['snmpMode'] == "AUTHNOPRIV": - playbook_params.pop('snmpPrivPassphrase', None) - playbook_params.pop('snmpPrivProtocol', None) - - try: - if playbook_params['updateMgmtIPaddressList']: - new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] - if new_mgmt_ipaddress in self.have['device_in_dnac']: - self.status = "failed" - self.msg = "Device with IP address '{0}' already exists in inventory".format(new_mgmt_ipaddress) - self.log(self.msg, "ERROR") - self.result['response'] = self.msg else: + self.log("Playbook parameter for updating devices: {0}".format(str(playbook_params)), "DEBUG") response = self.dnac._exec( family="devices", function='sync_devices', @@ -2581,59 +2624,28 @@ def get_diff_merged(self, config): while True: execution_details = self.get_task_details(task_id) + if execution_details.get("isError"): self.status = "failed" failure_reason = execution_details.get("failureReason") if failure_reason: - self.msg = "Device Updation get failed because of {0}".format(failure_reason) + self.msg = "Device Updation for device '{0}' get failed due to {1}".format(device_ip, failure_reason) else: - self.msg = "Device Updation get failed" + self.msg = "Device Updation for device '{0}' get failed".format(device_ip) self.log(self.msg, "ERROR") break elif execution_details.get("endTime"): self.status = "success" self.result['changed'] = True self.result['response'] = execution_details - self.msg = "Devices {0} present in Cisco Catalyst Center and updated successfully".format(str(device_to_update)) + self.msg = "Device '{0}' present in Cisco Catalyst Center and have been updated successfully".format(device_ip) self.log(self.msg, "INFO") break - else: - response = self.dnac._exec( - family="devices", - function='sync_devices', - op_modifies=True, - params=playbook_params, - ) - self.log("Received API response from 'sync_devices': {0}".format(str(response)), "DEBUG") - - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') - - while True: - execution_details = self.get_task_details(task_id) - - if execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Device Updation get failed because of {0}".format(failure_reason) - else: - self.msg = "Device Updation get failed" - self.log(self.msg, "ERROR") - break - elif execution_details.get("endTime"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Devices {0} present in Cisco Catalyst Center and updated successfully".format(str(device_to_update)) - self.log(self.msg, "INFO") - break - - except Exception as e: - error_message = "Error while updating device in Cisco Catalyst Center: {0}".format(str(e)) - self.log(error_message, "ERROR") - raise Exception(error_message) + except Exception as e: + error_message = "Error while updating device in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) if self.config[0].get('update_interface_details'): # Call the Get interface details by device IP API and fetch the interface Id From d60966e1bb8f30c6b89c2c0b1d030968ec6b575d Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 2 Feb 2024 23:19:36 +0530 Subject: [PATCH 55/76] Restructured the log messages --- playbooks/network_settings_intent.yml | 2 ++ plugins/module_utils/dnac.py | 41 ++++++++++++---------- plugins/modules/network_settings_intent.py | 14 ++++++-- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/playbooks/network_settings_intent.yml b/playbooks/network_settings_intent.yml index 5cb7cd4bfb..29f13ee5f2 100644 --- a/playbooks/network_settings_intent.yml +++ b/playbooks/network_settings_intent.yml @@ -17,6 +17,7 @@ dnac_verify: "{{ dnac_verify }}" dnac_debug: "{{ dnac_debug }}" dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" state: merged config_verify: True config: @@ -95,6 +96,7 @@ dnac_verify: "{{ dnac_verify }}" dnac_debug: "{{ dnac_debug }}" dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" state: deleted config_verify: True config: diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index ca073f5bc6..1d72dc3ca4 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -73,15 +73,15 @@ def __init__(self, module): self.dnac_log_file_path = dnac_params.get("dnac_log_file_path") or 'dnac.log' self.validate_dnac_log_file_path() self.dnac_log_mode = 'w' if not dnac_params.get("dnac_log_append") else 'a' - self.setup_logger('dnac_file_logger') - self.dnac_file_logger = logging.getLogger('dnac_file_logger') + self.setup_logger('logger') + self.logger = logging.getLogger('logger') DnacBase.__is_log_init = True - self.dnac_file_logger.debug('Logging configured and initiated') + self.logger.debug('Logging configured and initiated') elif not self.dnac_log: # If dnac_log is False, return an empty logger - self.dnac_file_logger = logging.getLogger('empty_logger') + self.logger = logging.getLogger('empty_logger') - self.dnac_file_logger.debug('Dnac parameters: %s', str(dnac_params)) + self.logger.debug('Dnac parameters: %s', str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -174,7 +174,8 @@ def setup_logger(self, logger_name): level = level_mapping.get(self.dnac_log_level, logging.WARNING) logger = logging.getLogger(logger_name) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(module)s: %(funcName)s: %(lineno)d --- %(message)s', datefmt='%m-%d-%Y %H:%M:%S') + # formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(module)s: %(funcName)s: %(lineno)d --- %(message)s', datefmt='%m-%d-%Y %H:%M:%S') + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', datefmt='%m-%d-%Y %H:%M:%S') file_handler = logging.FileHandler(self.dnac_log_file_path, mode=self.dnac_log_mode) file_handler.setFormatter(formatter) @@ -210,19 +211,21 @@ def log(self, message, level="WARNING", frameIncrement=0): """ if self.dnac_log: - message = "Module: " + self.__class__.__name__ + ", " + message - callerframerecord = inspect.stack()[frameIncrement] + # of.write("---- %s ---- %s@%s ---- %s \n" % (d, info.lineno, info.function, msg)) + # message = "Module: " + self.__class__.__name__ + ", " + message + class_name = self.__class__.__name__ + callerframerecord = inspect.stack()[1 + frameIncrement] frame = callerframerecord[0] info = inspect.getframeinfo(frame) - # log_message = "{0}@{1} --- {2}".format(info.lineno, info.function, message) - log_method = getattr(self.dnac_file_logger, level.lower()) - log_method(message) + log_message = " %s: %s: %s: %s \n" % (class_name, info.function, info.lineno, message) + log_method = getattr(self.logger, level.lower()) + log_method(log_message) def check_return_status(self): """API to check the return status value and exit/fail the module""" # self.log("status: {0}, msg:{1}".format(self.status, self.msg), frameIncrement=1) - self.dnac_file_logger.debug("status: %s, msg: %s", self.status, self.msg) + self.logger.debug("status: %s, msg: %s", self.status, self.msg) if "failed" in self.status: self.module.fail_json(msg=self.msg, response=[]) elif "exited" in self.status: @@ -288,7 +291,7 @@ def get_task_details(self, task_id): ) self.log('Task Details: {0}'.format(str(response)), 'DEBUG') - self.dnac_file_logger.debug("Retrieving task details by the API 'get_task_by_id' using task ID: %s, Response: %s", str(task_id), str(response)) + self.logger.debug("Retrieving task details by the API 'get_task_by_id' using task ID: %s, Response: %s", str(task_id), str(response)) if response and isinstance(response, dict): result = response.get('response') @@ -327,7 +330,7 @@ def check_task_response_status(self, response, validation_string, data=False): task_id = response.get("taskId") while True: task_details = self.get_task_details(task_id) - self.dnac_file_logger.debug('Getting task details from task ID %s: %s', task_id, str(task_details)) + self.logger.debug('Getting task details from task ID %s: %s', task_id, str(task_details)) if task_details.get("isError") is True: if task_details.get("failureReason"): @@ -344,7 +347,7 @@ def check_task_response_status(self, response, validation_string, data=False): self.status = "success" break - self.dnac_file_logger.debug("progress set to %s for taskid: %s", task_details.get('progress'), task_id) + self.logger.debug("progress set to %s for taskid: %s", task_details.get('progress'), task_id) return self @@ -365,13 +368,13 @@ def get_execution_details(self, execid): response (dict) - Status for API execution """ - self.dnac_file_logger.debug("Execution Id %s", str(execid)) + self.logger.debug("Execution Id %s", str(execid)) response = self.dnac._exec( family="task", function='get_business_api_execution_details', params={"execution_id": execid} ) - self.dnac_file_logger.debug("Response for the current execution %s", str(response)) + self.logger.debug("Response for the current execution %s", str(response)) return response def check_execution_response_status(self, response): @@ -385,7 +388,7 @@ def check_execution_response_status(self, response): self """ - self.dnac_file_logger.debug(str(response)) + self.logger.debug(str(response)) if not response: self.msg = "response is empty" self.status = "failed" @@ -447,7 +450,7 @@ def camel_to_snake_case(self, config): for key, value in config.items(): new_key = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', key).lower() if new_key != key: - self.dnac_file_logger.debug("%s will be deprecated soon. Please use %s.", key, new_key) + self.logger.debug("%s will be deprecated soon. Please use %s.", key, new_key) new_value = self.camel_to_snake_case(value) new_config[new_key] = new_value elif isinstance(config, list): diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index a32dc71aeb..4f5862821f 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -25,6 +25,14 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: + dnac_log_file_path: + description: Specifies the file path for the log messages + type: str + default: dnac.log + dnac_log_append: + description: Determines the mode of the file. True for 'append' mode. False for 'write' mode. + type: bool + default: True config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -415,7 +423,7 @@ ) -class DnacNetwork(DnacBase): +class NetworkSettings(DnacBase): """Class containing member attributes for network intent module""" def __init__(self, module): @@ -2125,6 +2133,8 @@ def main(): "dnac_debug": {"type": 'bool', "default": False}, "dnac_log": {"type": 'bool', "default": False}, "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, "config_verify": {"type": 'bool', "default": False}, "config": {"type": 'list', "required": True, "elements": 'dict'}, "state": {"default": 'merged', "choices": ['merged', 'deleted']}, @@ -2133,7 +2143,7 @@ def main(): # Create an AnsibleModule object with argument specifications module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) - dnac_network = DnacNetwork(module) + dnac_network = NetworkSettings(module) state = dnac_network.params.get("state") config_verify = dnac_network.params.get("config_verify") if state not in dnac_network.supported_states: From 80ce79a6b13e6499ca5c2feb9b2b45011cfa3ca2 Mon Sep 17 00:00:00 2001 From: Abinash Date: Sat, 3 Feb 2024 06:03:15 +0000 Subject: [PATCH 56/76] Changes for typos, fixed CIDR issue and added delete all feature --- plugins/modules/discovery_intent.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 161097db22..125a77d61c 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -372,11 +372,12 @@ def validate_input(self, state=None): 'elements': 'str'} discovery_spec["discovery_type"] = {'type': 'str', 'required': True} - if (self.config[0].get("delete_all") is True and state == "deleted"): - self.validated_config = [{"delete_all": True}] - self.msg = "Sucessfully collected input for deletion of all the discoveries" - self.log(self.msg, "WARNING") - return self + if state == "deleted": + if self.config[0].get("delete_all") is True: + self.validated_config = [{"delete_all": True}] + self.msg = "Sucessfully collected input for deletion of all the discoveries" + self.log(self.msg, "WARNING") + return self # Validate discovery params valid_discovery, invalid_params = validate_list_of_dicts( @@ -494,6 +495,7 @@ def preprocess_device_discovery(self, ip_address_list=None): ip_address_list = ip_address_list[0] else: ip_address_list = str(ip_address_list[0]) + "/" + str("30") + self.log("Prefix length is not given, hence taking 30 as default", "WARNING") else: self.preprocess_device_discovery_handle_error() elif discovery_type == "RANGE": @@ -906,7 +908,7 @@ def get_diff_deleted(self): discovery_task_id = self.delete_exist_discovery(params=params) complete_discovery = self.get_task_status(task_id=discovery_task_id) self.result["changed"] = True - self.result['msg'] = "Discovery Deleted Successfully" + self.result['msg'] = "Successfully deleted discovery" self.result['diff'] = self.validated_config self.result['response'] = discovery_task_id From e015c82437a9123ad9f8aa88129a02bbb4992c18 Mon Sep 17 00:00:00 2001 From: Abinash Date: Sat, 3 Feb 2024 06:19:14 +0000 Subject: [PATCH 57/76] Changes for typos, fixed CIDR issue and added delete all feature --- plugins/modules/discovery_intent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 125a77d61c..6c4b43bb1a 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -372,7 +372,7 @@ def validate_input(self, state=None): 'elements': 'str'} discovery_spec["discovery_type"] = {'type': 'str', 'required': True} - if state == "deleted": + elif state == "deleted": if self.config[0].get("delete_all") is True: self.validated_config = [{"delete_all": True}] self.msg = "Sucessfully collected input for deletion of all the discoveries" From 01cad9a89a86d41585484e2cbf96d92c8a08ec00 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sat, 3 Feb 2024 12:55:08 +0530 Subject: [PATCH 58/76] Added the new parameter to the template file and addressed the review comments --- playbooks/credentials.template | 5 ++++- playbooks/network_settings_intent.yml | 2 ++ plugins/module_utils/dnac.py | 21 ++++++++++----------- plugins/modules/network_settings_intent.py | 14 ++++++++++++-- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/playbooks/credentials.template b/playbooks/credentials.template index e621bdaa5e..f34ad85b8e 100644 --- a/playbooks/credentials.template +++ b/playbooks/credentials.template @@ -5,4 +5,7 @@ dnac_username: dnac_password: dnac_version: 2.3.5.3 dnac_verify: False -dnac_debug: False \ No newline at end of file +dnac_debug: False +dnac_log_level: [DEBUG, INFO, WARNING, ERROR, CRITICAL] +dnac_log_file_path: +dnac_log_append: True diff --git a/playbooks/network_settings_intent.yml b/playbooks/network_settings_intent.yml index 29f13ee5f2..da4f2dac2d 100644 --- a/playbooks/network_settings_intent.yml +++ b/playbooks/network_settings_intent.yml @@ -18,6 +18,8 @@ dnac_debug: "{{ dnac_debug }}" dnac_log: True dnac_log_level: "{{ dnac_log_level }}" + dnac_log_append: True + dnac_log_file_path: "{{ dnac_log_file_path }}" state: merged config_verify: True config: diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 1d72dc3ca4..4b00a72dcd 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -68,20 +68,20 @@ def __init__(self, module): if self.dnac_log and not DnacBase.__is_log_init: self.dnac_log_level = dnac_params.get("dnac_log_level") or 'WARNING' - self.validate_dnac_log_level() self.dnac_log_level = self.dnac_log_level.upper() + self.validate_dnac_log_level() self.dnac_log_file_path = dnac_params.get("dnac_log_file_path") or 'dnac.log' self.validate_dnac_log_file_path() self.dnac_log_mode = 'w' if not dnac_params.get("dnac_log_append") else 'a' self.setup_logger('logger') self.logger = logging.getLogger('logger') DnacBase.__is_log_init = True - self.logger.debug('Logging configured and initiated') + self.log('Logging configured and initiated', "DEBUG") elif not self.dnac_log: # If dnac_log is False, return an empty logger self.logger = logging.getLogger('empty_logger') - self.logger.debug('Dnac parameters: %s', str(dnac_params)) + self.log('Dnac parameters: {0}'.format(dnac_params), "DEBUG") self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -225,7 +225,7 @@ def check_return_status(self): """API to check the return status value and exit/fail the module""" # self.log("status: {0}, msg:{1}".format(self.status, self.msg), frameIncrement=1) - self.logger.debug("status: %s, msg: %s", self.status, self.msg) + self.log("status: {0}, msg: {1}".format(self.status, self.msg), "DEBUG") if "failed" in self.status: self.module.fail_json(msg=self.msg, response=[]) elif "exited" in self.status: @@ -291,7 +291,7 @@ def get_task_details(self, task_id): ) self.log('Task Details: {0}'.format(str(response)), 'DEBUG') - self.logger.debug("Retrieving task details by the API 'get_task_by_id' using task ID: %s, Response: %s", str(task_id), str(response)) + self.log("Retrieving task details by the API 'get_task_by_id' using task ID: {0}, Response: {1}".format(task_id, response), "DEBUG") if response and isinstance(response, dict): result = response.get('response') @@ -330,7 +330,7 @@ def check_task_response_status(self, response, validation_string, data=False): task_id = response.get("taskId") while True: task_details = self.get_task_details(task_id) - self.logger.debug('Getting task details from task ID %s: %s', task_id, str(task_details)) + self.log('Getting task details from task ID {0}: {1}'.format(task_id, task_details), "DEBUG") if task_details.get("isError") is True: if task_details.get("failureReason"): @@ -347,7 +347,7 @@ def check_task_response_status(self, response, validation_string, data=False): self.status = "success" break - self.logger.debug("progress set to %s for taskid: %s", task_details.get('progress'), task_id) + self.log("progress set to {0} for taskid: {1}".format(task_details.get('progress'), task_id), "DEBUG") return self @@ -368,13 +368,13 @@ def get_execution_details(self, execid): response (dict) - Status for API execution """ - self.logger.debug("Execution Id %s", str(execid)) + self.log("Execution Id: {0}".format(execid), "DEBUG") response = self.dnac._exec( family="task", function='get_business_api_execution_details', params={"execution_id": execid} ) - self.logger.debug("Response for the current execution %s", str(response)) + self.log("Response for the current execution: {0}".format(response)) return response def check_execution_response_status(self, response): @@ -388,7 +388,6 @@ def check_execution_response_status(self, response): self """ - self.logger.debug(str(response)) if not response: self.msg = "response is empty" self.status = "failed" @@ -450,7 +449,7 @@ def camel_to_snake_case(self, config): for key, value in config.items(): new_key = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', key).lower() if new_key != key: - self.logger.debug("%s will be deprecated soon. Please use %s.", key, new_key) + self.log("{0} will be deprecated soon. Please use {1}.".format(key, new_key), "DEBUG") new_value = self.camel_to_snake_case(value) new_config[new_key] = new_value elif isinstance(config, list): diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 4f5862821f..e8a4ca1c59 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -26,11 +26,21 @@ Madhan Sankaranarayanan (@madhansansel) options: dnac_log_file_path: - description: Specifies the file path for the log messages + description: + - Controls the logging behavior. + - Logs will be recorded only if dnac_log is set to True. + - If the path is unspecified, + - When dnac_log_append is True, a dnac.log file will be generated in the current Ansible running directory, and logs will be appended. + - When dnac_log_append is False, a dnac.log file will be generated, and logs will be overwritten. + - If a path is specified, + - When dnac_log_append is True, the file will open in append mode. + - When dnac_log_append is False, the file will open in write (w) mode. + - In scenarios with a single file shared among multiple modules, without append mode, the file content will be overwritten after each module execution. + - For a shared log file, setting append mode to False for the first module (to overwrite the file), and for all subsequent modules, set append mode to true. type: str default: dnac.log dnac_log_append: - description: Determines the mode of the file. True for 'append' mode. False for 'write' mode. + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. type: bool default: True config_verify: From 4992b4054fc0d42fe82d5078da05e963f6682540 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sat, 3 Feb 2024 13:25:27 +0530 Subject: [PATCH 59/76] Changed the documentation for the dnac_log_file_path --- plugins/modules/network_settings_intent.py | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index e8a4ca1c59..f6f3a12e6d 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -27,16 +27,19 @@ options: dnac_log_file_path: description: - - Controls the logging behavior. - - Logs will be recorded only if dnac_log is set to True. - - If the path is unspecified, - - When dnac_log_append is True, a dnac.log file will be generated in the current Ansible running directory, and logs will be appended. - - When dnac_log_append is False, a dnac.log file will be generated, and logs will be overwritten. - - If a path is specified, - - When dnac_log_append is True, the file will open in append mode. - - When dnac_log_append is False, the file will open in write (w) mode. - - In scenarios with a single file shared among multiple modules, without append mode, the file content will be overwritten after each module execution. - - For a shared log file, setting append mode to False for the first module (to overwrite the file), and for all subsequent modules, set append mode to true. + - Governs logging. Logs are recorded if dnac_log is True. + - If unspecified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If a path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. type: str default: dnac.log dnac_log_append: From 0dbcbce11d4ea6edd8f396eb152e454664eee554 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sat, 3 Feb 2024 13:31:56 +0530 Subject: [PATCH 60/76] Changed the documentation --- plugins/modules/network_settings_intent.py | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index f6f3a12e6d..75ca0076c2 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -27,19 +27,19 @@ options: dnac_log_file_path: description: - - Governs logging. Logs are recorded if dnac_log is True. - - If unspecified, - - When 'dnac_log_append' is True, 'dnac.log' is generated in the - current Ansible directory; logs are appended. - - When 'dnac_log_append' is False, 'dnac.log' is generated; logs - are overwritten. - - If a path is specified, - - When 'dnac_log_append' is True, the file opens in append mode. - - When 'dnac_log_append' is False, the file opens in write (w) mode. - - In shared file scenarios, without append mode, content is - overwritten after each module execution. - - For a shared log file, set append to False for the 1st module - (to overwrite); for subsequent modules, set append to True. + - Governs logging. Logs are recorded if dnac_log is True. + - If unspecified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If a path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. type: str default: dnac.log dnac_log_append: From db18f4f40ba67b7c157db6e6b9507f4cd565a002 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sat, 3 Feb 2024 13:49:16 +0530 Subject: [PATCH 61/76] Changes the dnac_log_level list --- playbooks/credentials.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/credentials.template b/playbooks/credentials.template index f34ad85b8e..5270c08164 100644 --- a/playbooks/credentials.template +++ b/playbooks/credentials.template @@ -6,6 +6,6 @@ dnac_password: dnac_version: 2.3.5.3 dnac_verify: False dnac_debug: False -dnac_log_level: [DEBUG, INFO, WARNING, ERROR, CRITICAL] +dnac_log_level: [CRITICAL, ERROR, WARNING, INFO, DEBUG] dnac_log_file_path: dnac_log_append: True From c6a4737c4884f707362c63db47432e923b804ff5 Mon Sep 17 00:00:00 2001 From: Abinash Date: Sat, 3 Feb 2024 09:13:20 +0000 Subject: [PATCH 62/76] Changes for typos, fixed CIDR issue and added delete all feature --- plugins/modules/discovery_intent.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 6c4b43bb1a..b83c26ac11 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -491,11 +491,13 @@ def preprocess_device_discovery(self, ip_address_list=None): self.preprocess_device_discovery_handle_error() elif discovery_type == "CIDR": if len(ip_address_list) == 1: - if len(ip_address_list[0].split("/")) == 2: - ip_address_list = ip_address_list[0] + cidr_notation = ip_address_list[0] + if len(cidr_notation.split("/")) == 2: + ip_address_list = cidr_notation else: - ip_address_list = str(ip_address_list[0]) + "/" + str("30") - self.log("Prefix length is not given, hence taking 30 as default", "WARNING") + ip_address_list = "{0}/30".format(cidr_notation) + self.log("CIDR notation is being used for discovery and it requires a prefix length to be specified, such as 1.1.1.1/24.\ + As no prefix length was provided, it will default to 30.", "WARNING") else: self.preprocess_device_discovery_handle_error() elif discovery_type == "RANGE": From 881830e6841ce7b9ccde2599724a1f38d2e47276 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sun, 4 Feb 2024 11:47:05 +0530 Subject: [PATCH 63/76] Added the documentation part for dnac_log_file_path, dnac_log_append --- plugins/modules/device_credential_intent.py | 23 +++++++++++++++++++ plugins/modules/discovery_intent.py | 23 +++++++++++++++++++ plugins/modules/inventory_intent.py | 23 +++++++++++++++++++ plugins/modules/pnp_intent.py | 23 +++++++++++++++++++ plugins/modules/provision_intent.py | 23 +++++++++++++++++++ plugins/modules/site_intent.py | 23 +++++++++++++++++++ plugins/modules/swim_intent.py | 23 +++++++++++++++++++ plugins/modules/template_intent.py | 25 ++++++++++++++++++++- 8 files changed, 185 insertions(+), 1 deletion(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index f7d92f5701..3a808f24af 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -25,6 +25,27 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: + dnac_log_file_path: + description: + - Governs logging. Logs are recorded if dnac_log is True. + - If unspecified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If a path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. + type: str + default: dnac.log + dnac_log_append: + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. + type: bool + default: True config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -2568,6 +2589,8 @@ def main(): "dnac_debug": {"type": 'bool', "default": False}, "dnac_log": {"type": 'bool', "default": False}, "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, "config_verify": {"type": 'bool', "default": False}, "config": {"type": 'list', "required": True, "elements": 'dict'}, "state": {"default": 'merged', "choices": ['merged', 'deleted']}, diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index b83c26ac11..9064f76485 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -22,6 +22,27 @@ author: Abinash Mishra (@abimishr) Phan Nguyen (phannguy) options: + dnac_log_file_path: + description: + - Governs logging. Logs are recorded if dnac_log is True. + - If unspecified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If a path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. + type: str + default: dnac.log + dnac_log_append: + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. + type: bool + default: True config_verify: description: Set to True to verify the Cisco DNA Center config after applying the playbook config. type: bool @@ -997,6 +1018,8 @@ def main(): 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config_verify': {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 448f9b5ca4..089bea34fb 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -24,6 +24,27 @@ author: Abhishek Maheshwari (@abmahesh) Madhan Sankaranarayanan (@madhansansel) options: + dnac_log_file_path: + description: + - Governs logging. Logs are recorded if dnac_log is True. + - If unspecified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If a path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. + type: str + default: dnac.log + dnac_log_append: + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. + type: bool + default: True config_verify: description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool @@ -3217,6 +3238,8 @@ def main(): 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config_verify': {'type': 'bool', "default": False}, diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 489568cff1..7a8ae5c2cd 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -24,6 +24,27 @@ Rishita Chowdhary (@rishitachowdhary) Abinash Mishra (@abimishr) options: + dnac_log_file_path: + description: + - Governs logging. Logs are recorded if dnac_log is True. + - If unspecified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If a path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. + type: str + default: dnac.log + dnac_log_append: + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. + type: bool + default: True config_verify: description: Set to True to verify the Cisco DNA Center config after applying the playbook config. type: bool @@ -1106,6 +1127,8 @@ def main(): 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config_verify': {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, diff --git a/plugins/modules/provision_intent.py b/plugins/modules/provision_intent.py index 9998e8c89d..c65a3faba7 100644 --- a/plugins/modules/provision_intent.py +++ b/plugins/modules/provision_intent.py @@ -21,6 +21,27 @@ - cisco.dnac.intent_params author: Abinash Mishra (@abimishr) options: + dnac_log_file_path: + description: + - Governs logging. Logs are recorded if dnac_log is True. + - If unspecified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If a path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. + type: str + default: dnac.log + dnac_log_append: + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. + type: bool + default: True config_verify: description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool @@ -587,6 +608,8 @@ def main(): 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, "config_verify": {"type": 'bool', "default": False}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index d0c4db7f6a..814d32b53c 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -25,6 +25,27 @@ Rishita Chowdhary (@rishitachowdhary) Abhishek Maheshwari (@abhishekmaheshwari) options: + dnac_log_file_path: + description: + - Governs logging. Logs are recorded if dnac_log is True. + - If unspecified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If a path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. + type: str + default: dnac.log + dnac_log_append: + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. + type: bool + default: True config_verify: description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool @@ -1026,6 +1047,8 @@ def main(): 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config_verify': {'type': 'bool', "default": False}, diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 93aadd8308..429a3bf606 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -30,6 +30,27 @@ Rishita Chowdhary (@rishitachowdhary) Abhishek Maheshwari (@abmahesh) options: + dnac_log_file_path: + description: + - Governs logging. Logs are recorded if dnac_log is True. + - If unspecified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If a path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. + type: str + default: dnac.log + dnac_log_append: + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. + type: bool + default: True config_verify: description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool @@ -1631,6 +1652,8 @@ def main(): 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, 'config_verify': {'type': 'bool', "default": False}, diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 1e9fde74b8..c766626283 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -30,6 +30,27 @@ Akash Bhaskaran (@akabhask) Muthu Rakesh (@MUTHU-RAKESH-27) options: + dnac_log_file_path: + description: + - Governs logging. Logs are recorded if dnac_log is True. + - If unspecified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If a path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. + type: str + default: dnac.log + dnac_log_append: + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. + type: bool + default: True config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -579,7 +600,7 @@ type: str import: description: Import the project/template details. - type: + type: dict suboptions: project: description: Import the project details. @@ -2762,6 +2783,8 @@ def main(): 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, 'validate_response_schema': {'type': 'bool', 'default': True}, "config_verify": {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, From 38d59d08f2655bac4bdfd5cf4ee59179a886ee92 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Sun, 4 Feb 2024 18:32:20 +0530 Subject: [PATCH 64/76] Added the examples for tasks in swim and inventory, added code for Idempotency in SWIM module for tagging/un-tagging the image as Golden image --- plugins/modules/inventory_intent.py | 298 ++++++++++++++++++---------- plugins/modules/swim_intent.py | 62 +++++- 2 files changed, 245 insertions(+), 115 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 448f9b5ca4..18c807c57d 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -455,6 +455,27 @@ new_mgmt_ipaddress: string username: string +- name: Update new management IP address of device in inventory + cisco.dnac.inventory_intent: + 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_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - device_updated: true + ip_address: + - string + credential_update: true + update_mgmt_ipaddresslist: + - exist_mgmt_ipaddress: string + new_mgmt_ipaddress: string + - name: Associate Wired Devices to site and Provisioned it in Inventory cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" @@ -2402,6 +2423,170 @@ def get_provision_wired_device(self, device_ip): return True + def update_interface_detail_of_device(self, device_to_update): + """ + Update interface details for a device in Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_to_update (list): A list of IP addresses of devices to be updated. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method updates interface details for devices in Cisco Catalyst Center. + It iterates over the list of devices to be updated, retrieves interface parameters from the configuration, + calls the update interface details API with the required parameters, and checks the execution response. + If the update is successful, it sets the status to 'success' and logs an informational message. + """ + + # Call the Get interface details by device IP API and fetch the interface Id + for device_ip in device_to_update: + interface_params = self.config[0].get('update_interface_details') + interface_name = interface_params.get('interface_name') + device_id = self.get_device_ids([device_ip]) + interface_id = self.get_interface_from_id_and_name(device_id[0], interface_name) + self.check_return_status() + + # Now we call update interface details api with required parameter + try: + interface_params = self.config[0].get('update_interface_details') + temp_params = { + 'description': interface_params.get('description', ''), + 'adminStatus': interface_params.get('admin_status'), + 'voiceVlanId': interface_params.get('voice_vlan_id'), + 'vlanId': interface_params.get('vlan_id') + } + payload_params = {} + for key, value in temp_params.items(): + if value is not None: + payload_params[key] = value + + update_interface_params = { + 'payload': payload_params, + 'interface_uuid': interface_id, + 'deployment_mode': interface_params.get('deployment_mode', 'Deploy') + } + response = self.dnac._exec( + family="devices", + function='update_interface_details', + op_modifies=True, + params=update_interface_params, + ) + self.log("Received API response from 'update_interface_details': {0}".format(str(response)), "DEBUG") + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if 'SUCCESS' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Updated Interface Details for device '{0}' successfully".format(device_ip) + self.log(self.msg, "INFO") + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Interface Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Interface Updation get failed" + self.log(self.msg, "ERROR") + break + + except Exception as e: + error_message = "Error while updating interface details in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "INFO") + self.status = "success" + self.result['changed'] = False + self.msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" + self.log(self.msg, "INFO") + + return self + + def check_managementip_execution_response(self, response, device_ip, new_mgmt_ipaddress): + """ + Check the execution response of a management IP update task. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + response (dict): The response received after initiating the management IP update task. + device_ip (str): The IP address of the device for which the management IP was updated. + new_mgmt_ipaddress (str): The new management IP address of the device. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the execution response of a management IP update task in Cisco Catalyst Center. + It continuously queries the task details until the task is completed or an error occurs. + If the task is successful, it sets the status to 'success' and logs an informational message. + If the task fails, it sets the status to 'failed' and logs an error message with the failure reason, if available. + """ + + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + if execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device new management IP updation for device '{0}' get failed due to {1}".format(device_ip, failure_reason) + else: + self.msg = "Device new management IP updation for device '{0}' get failed".format(device_ip) + self.log(self.msg, "ERROR") + break + elif execution_details.get("endTime"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = """Device '{0}' present in Cisco Catalyst Center and new management ip '{1}' have been + updated successfully""".format(device_ip, new_mgmt_ipaddress) + self.log(self.msg, "INFO") + break + + return self + + def check_device_update_execution_response(self, response, device_ip): + """ + Check the execution response of a device update task. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + response (dict): The response received after initiating the device update task. + device_ip (str): The IP address of the device for which the update is performed. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the execution response of a device update task in Cisco Catalyst Center. + It continuously queries the task details until the task is completed or an error occurs. + If the task is successful, it sets the status to 'success' and logs an informational message. + If the task fails, it sets the status to 'failed' and logs an error message with the failure reason, if available. + """ + + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Updation for device '{0}' get failed due to {1}".format(device_ip, failure_reason) + else: + self.msg = "Device Updation for device '{0}' get failed".format(device_ip) + self.log(self.msg, "ERROR") + break + elif execution_details.get("endTime"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Device '{0}' present in Cisco Catalyst Center and have been updated successfully".format(device_ip) + self.log(self.msg, "INFO") + break + + return self + def get_want(self, config): """ Get all the device related information from playbook that is needed to be @@ -2586,28 +2771,8 @@ def get_diff_merged(self, config): self.log("Received API response from 'sync_devices': {0}".format(str(response)), "DEBUG") if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') - - while True: - execution_details = self.get_task_details(task_id) - if execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = """Device new management IP updation for device '{0}' - get failed due to {1}""".format(device_ip, failure_reason) - else: - self.msg = "Device new management IP updation for device '{0}' get failed".format(device_ip) - self.log(self.msg, "ERROR") - break - elif execution_details.get("endTime"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = """Device '{0}' present in Cisco Catalyst Center and new management ip '{1}' have been - updated successfully""".format(device_ip, new_mgmt_ipaddress) - self.log(self.msg, "INFO") - break + self.check_managementip_execution_response(response, device_ip, new_mgmt_ipaddress) + self.check_return_status() else: self.log("Playbook parameter for updating devices: {0}".format(str(playbook_params)), "DEBUG") @@ -2620,27 +2785,8 @@ def get_diff_merged(self, config): self.log("Received API response from 'sync_devices': {0}".format(str(response)), "DEBUG") if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') - - while True: - execution_details = self.get_task_details(task_id) - - if execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Device Updation for device '{0}' get failed due to {1}".format(device_ip, failure_reason) - else: - self.msg = "Device Updation for device '{0}' get failed".format(device_ip) - self.log(self.msg, "ERROR") - break - elif execution_details.get("endTime"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Device '{0}' present in Cisco Catalyst Center and have been updated successfully".format(device_ip) - self.log(self.msg, "INFO") - break + self.check_device_update_execution_response(response, device_ip) + self.check_return_status() except Exception as e: error_message = "Error while updating device in Cisco Catalyst Center: {0}".format(str(e)) @@ -2648,71 +2794,7 @@ def get_diff_merged(self, config): raise Exception(error_message) if self.config[0].get('update_interface_details'): - # Call the Get interface details by device IP API and fetch the interface Id - for device_ip in device_to_update: - interface_params = self.config[0].get('update_interface_details') - interface_name = interface_params.get('interface_name') - device_id = self.get_device_ids([device_ip]) - interface_id = self.get_interface_from_id_and_name(device_id[0], interface_name) - self.check_return_status() - - # Now we call update interface details api with required parameter - try: - interface_params = self.config[0].get('update_interface_details') - temp_params = { - 'description': interface_params.get('description', ''), - 'adminStatus': interface_params.get('admin_status'), - 'voiceVlanId': interface_params.get('voice_vlan_id'), - 'vlanId': interface_params.get('vlan_id') - } - payload_params = {} - for key, value in temp_params.items(): - if value is not None: - payload_params[key] = value - - update_interface_params = { - 'payload': payload_params, - 'interface_uuid': interface_id, - 'deployment_mode': interface_params.get('deployment_mode', 'Deploy') - } - response = self.dnac._exec( - family="devices", - function='update_interface_details', - op_modifies=True, - params=update_interface_params, - ) - self.log("Received API response from 'update_interface_details': {0}".format(str(response)), "DEBUG") - - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') - - while True: - execution_details = self.get_task_details(task_id) - - if 'SUCCESS' in execution_details.get("progress"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Updated Interface Details for device '{0}' successfully".format(device_ip) - self.log(self.msg, "INFO") - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Interface Updation get failed because of {0}".format(failure_reason) - else: - self.msg = "Interface Updation get failed" - self.log(self.msg, "ERROR") - break - - except Exception as e: - error_message = "Error while updating interface details in Cisco Catalyst Center: {0}".format(str(e)) - self.log(error_message, "INFO") - self.status = "success" - self.result['changed'] = False - self.msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" - self.log(self.msg, "INFO") + self.update_interface_detail_of_device(device_to_update).check_return_status() if self.config[0].get('update_device_role'): for device_ip in device_to_update: diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 93aadd8308..c12b65632b 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -310,7 +310,26 @@ device_role: string device_type: string site_name: string - tagging: bool + tagging: true + +- name: Un-tagged the given image as golden and load it on device + cisco.dnac.swim_intent: + 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_level: "{{dnac_log_level}}" + dnac_log: True + config: + - tagging_details: + image_name: string + device_role: string + device_type: string + site_name: string + tagging: false - name: Distribute the given image on devices associated to that site with specified role. cisco.dnac.swim_intent: @@ -1006,6 +1025,41 @@ def get_diff_tagging(self): tagging_details = self.want.get("tagging_details") tag_image_golden = tagging_details.get("tagging") + image_name = self.get_image_name_from_id(self.have.get("tagging_image_id")) + + image_params = dict( + image_id=self.have.get("tagging_image_id"), + site_id=self.have.get("site_id"), + device_family_identifier=self.have.get("device_family_identifier"), + device_role=tagging_details.get("device_role", "ALL").upper() + ) + + response = self.dnac._exec( + family="software_image_management_swim", + function='get_golden_tag_status_of_an_image', + op_modifies=True, + params=image_params + ) + self.log("Received API response from 'get_golden_tag_status_of_an_image': {0}".format(str(response)), "DEBUG") + + response = response.get('response') + if response: + image_status = response['taggedGolden'] + if image_status and image_status == tag_image_golden: + self.status = "success" + self.result['changed'] = False + self.msg = "SWIM Image '{0}' already tagged as Golden image in Cisco Catalyst Center".format(image_name) + self.result['msg'] = self.msg + self.log(self.msg, "INFO") + return self + + if not image_status and image_status == tag_image_golden: + self.status = "success" + self.result['changed'] = False + self.msg = "SWIM Image '{0}' already un-tagged from Golden image in Cisco Catalyst Center".format(image_name) + self.result['msg'] = self.msg + self.log(self.msg, "INFO") + return self if tag_image_golden: image_params = dict( @@ -1025,12 +1079,6 @@ def get_diff_tagging(self): self.log("Received API response from 'tag_as_golden_image': {0}".format(str(response)), "DEBUG") else: - image_params = dict( - image_id=self.have.get("tagging_image_id"), - site_id=self.have.get("site_id"), - device_family_identifier=self.have.get("device_family_identifier"), - device_role=tagging_details.get("device_role", "ALL").upper() - ) self.log("Parameters for un-tagging the image as golden: {0}".format(str(image_params)), "INFO") response = self.dnac._exec( From 068736ca95d6e72a291f02d8a61276773357d130 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sun, 4 Feb 2024 18:51:18 +0530 Subject: [PATCH 65/76] Added the dnac_log_file_path, dnac_log_append common intent documentation file --- plugins/doc_fragments/intent_params.py | 41 +++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/plugins/doc_fragments/intent_params.py b/plugins/doc_fragments/intent_params.py index afaae9f79f..087544872d 100644 --- a/plugins/doc_fragments/intent_params.py +++ b/plugins/doc_fragments/intent_params.py @@ -50,16 +50,43 @@ class ModuleDocFragment(object): default: false dnac_log: description: - - Flag for logging playbook execution details. - If set to true the log file will be created at the location of the execution with the name dnac.log + - Flag for logging playbook execution details. + If set to true the log file will be created at the location of the execution with the name dnac.log type: bool default: false dnac_log_level: - description: - - Specifies the desired log level for Cisco Catalyst Center logging. - Options - [CRITICAL, ERROR, WARNING, INFO, DEBUG] - type: str - default: WARNING + description: + - Sets the threshold for log level. Messages with a level equal to or higher than + this will be logged. Levels are listed in order of severity. + - CRITICAL: Indicates serious errors halting the program. Displays only CRITICAL messages. + - ERROR: Indicates problems preventing a function. Displays ERROR and CRITICAL messages. + - WARNING: Indicates potential future issues. Displays WARNING, ERROR, CRITICAL messages. + - INFO: Tracks normal operation. Displays INFO, WARNING, ERROR, CRITICAL messages. + - DEBUG: Provides detailed diagnostic info. Displays all log messages. + type: str + choices: [CRITICAL, ERROR, WARNING, INFO, DEBUG] + default: WARNING + dnac_log_file_path: + description: + - Governs logging. Logs are recorded if dnac_log is True. + - If path is not specified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. + type: str + default: dnac.log + dnac_log_append: + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. + type: bool + default: True validate_response_schema: description: - Flag for Cisco DNA Center SDK to enable the validation of request bodies against a JSON schema. From 57d8da28573fbd4f24bec069bcd2d115343713c6 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sun, 4 Feb 2024 19:02:02 +0530 Subject: [PATCH 66/76] Changes in documentation --- plugins/doc_fragments/intent_params.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/doc_fragments/intent_params.py b/plugins/doc_fragments/intent_params.py index 087544872d..99d0937d4a 100644 --- a/plugins/doc_fragments/intent_params.py +++ b/plugins/doc_fragments/intent_params.py @@ -57,14 +57,13 @@ class ModuleDocFragment(object): dnac_log_level: description: - Sets the threshold for log level. Messages with a level equal to or higher than - this will be logged. Levels are listed in order of severity. + this will be logged. Levels are listed in order of severity [CRITICAL, ERROR, WARNING, INFO, DEBUG]. - CRITICAL: Indicates serious errors halting the program. Displays only CRITICAL messages. - ERROR: Indicates problems preventing a function. Displays ERROR and CRITICAL messages. - WARNING: Indicates potential future issues. Displays WARNING, ERROR, CRITICAL messages. - INFO: Tracks normal operation. Displays INFO, WARNING, ERROR, CRITICAL messages. - DEBUG: Provides detailed diagnostic info. Displays all log messages. type: str - choices: [CRITICAL, ERROR, WARNING, INFO, DEBUG] default: WARNING dnac_log_file_path: description: From 7b4860ae357bb6c8cb70dab62a9b6dd0d0e596ff Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sun, 4 Feb 2024 19:05:39 +0530 Subject: [PATCH 67/76] Changes in documentation --- plugins/doc_fragments/intent_params.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/doc_fragments/intent_params.py b/plugins/doc_fragments/intent_params.py index 99d0937d4a..3efee055a3 100644 --- a/plugins/doc_fragments/intent_params.py +++ b/plugins/doc_fragments/intent_params.py @@ -58,11 +58,11 @@ class ModuleDocFragment(object): description: - Sets the threshold for log level. Messages with a level equal to or higher than this will be logged. Levels are listed in order of severity [CRITICAL, ERROR, WARNING, INFO, DEBUG]. - - CRITICAL: Indicates serious errors halting the program. Displays only CRITICAL messages. - - ERROR: Indicates problems preventing a function. Displays ERROR and CRITICAL messages. - - WARNING: Indicates potential future issues. Displays WARNING, ERROR, CRITICAL messages. - - INFO: Tracks normal operation. Displays INFO, WARNING, ERROR, CRITICAL messages. - - DEBUG: Provides detailed diagnostic info. Displays all log messages. + - CRITICAL Indicates serious errors halting the program. Displays only CRITICAL messages. + - ERROR Indicates problems preventing a function. Displays ERROR and CRITICAL messages. + - WARNING Indicates potential future issues. Displays WARNING, ERROR, CRITICAL messages. + - INFO Tracks normal operation. Displays INFO, WARNING, ERROR, CRITICAL messages. + - DEBUG Provides detailed diagnostic info. Displays all log messages. type: str default: WARNING dnac_log_file_path: From 1af297af4f12c14f572865fa111eebfca995267b Mon Sep 17 00:00:00 2001 From: Madhan Date: Mon, 5 Feb 2024 06:29:25 +0530 Subject: [PATCH 68/76] Changes in document --- plugins/doc_fragments/intent_params.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/doc_fragments/intent_params.py b/plugins/doc_fragments/intent_params.py index 3efee055a3..06f10ab41b 100644 --- a/plugins/doc_fragments/intent_params.py +++ b/plugins/doc_fragments/intent_params.py @@ -58,11 +58,11 @@ class ModuleDocFragment(object): description: - Sets the threshold for log level. Messages with a level equal to or higher than this will be logged. Levels are listed in order of severity [CRITICAL, ERROR, WARNING, INFO, DEBUG]. - - CRITICAL Indicates serious errors halting the program. Displays only CRITICAL messages. - - ERROR Indicates problems preventing a function. Displays ERROR and CRITICAL messages. - - WARNING Indicates potential future issues. Displays WARNING, ERROR, CRITICAL messages. - - INFO Tracks normal operation. Displays INFO, WARNING, ERROR, CRITICAL messages. - - DEBUG Provides detailed diagnostic info. Displays all log messages. + - CRITICAL indicates serious errors halting the program. Displays only CRITICAL messages. + - ERROR indicates problems preventing a function. Displays ERROR and CRITICAL messages. + - WARNING indicates potential future issues. Displays WARNING, ERROR, CRITICAL messages. + - INFO tracks normal operation. Displays INFO, WARNING, ERROR, CRITICAL messages. + - DEBUG provides detailed diagnostic info. Displays all log messages. type: str default: WARNING dnac_log_file_path: From 5cc73c70a3e66e62223587e1d45f5ca7c6de56dd Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 5 Feb 2024 10:48:25 +0530 Subject: [PATCH 69/76] Add the device with snmp_mode (AUTHNOPRIV, NOAUTHNOPRIV) in Inventory in Cisco Catalyst Center --- plugins/modules/inventory_intent.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 18c807c57d..c7b99d1c57 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2876,6 +2876,15 @@ def get_diff_merged(self, config): if not device_params['snmpPrivProtocol']: device_params['snmpPrivProtocol'] = "AES128" + if device_params['snmpMode'] == "NOAUTHNOPRIV": + device_params.pop('snmpAuthPassphrase', None) + device_params.pop('snmpPrivPassphrase', None) + device_params.pop('snmpPrivProtocol', None) + device_params.pop('snmpAuthProtocol', None) + elif device_params['snmpMode'] == "AUTHNOPRIV": + device_params.pop('snmpPrivPassphrase', None) + device_params.pop('snmpPrivProtocol', None) + self.mandatory_parameter().check_return_status() try: response = self.dnac._exec( From e28b298b05f71717a7b6403c8af7acf65a0b24a5 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 5 Feb 2024 10:53:44 +0530 Subject: [PATCH 70/76] Changes in the dnac_log documentation --- plugins/doc_fragments/intent_params.py | 65 ++++++++++++++------------ 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/plugins/doc_fragments/intent_params.py b/plugins/doc_fragments/intent_params.py index 06f10ab41b..c12143ab07 100644 --- a/plugins/doc_fragments/intent_params.py +++ b/plugins/doc_fragments/intent_params.py @@ -51,41 +51,46 @@ class ModuleDocFragment(object): dnac_log: description: - Flag for logging playbook execution details. - If set to true the log file will be created at the location of the execution with the name dnac.log + - If set to true and dnac_log_file_path is provided, + - The log file will be created at the location of the execution with the provided name. + - If set to true and dnac_log_file_path is not provided, + - The log file will be created at the location of the execution with the name 'dnac.log'. + - If set to false, + - Logging will not take place. type: bool default: false dnac_log_level: - description: - - Sets the threshold for log level. Messages with a level equal to or higher than - this will be logged. Levels are listed in order of severity [CRITICAL, ERROR, WARNING, INFO, DEBUG]. - - CRITICAL indicates serious errors halting the program. Displays only CRITICAL messages. - - ERROR indicates problems preventing a function. Displays ERROR and CRITICAL messages. - - WARNING indicates potential future issues. Displays WARNING, ERROR, CRITICAL messages. - - INFO tracks normal operation. Displays INFO, WARNING, ERROR, CRITICAL messages. - - DEBUG provides detailed diagnostic info. Displays all log messages. - type: str - default: WARNING + description: + - Sets the threshold for log level. Messages with a level equal to or higher than + this will be logged. Levels are listed in order of severity [CRITICAL, ERROR, WARNING, INFO, DEBUG]. + - CRITICAL indicates serious errors halting the program. Displays only CRITICAL messages. + - ERROR indicates problems preventing a function. Displays ERROR and CRITICAL messages. + - WARNING indicates potential future issues. Displays WARNING, ERROR, CRITICAL messages. + - INFO tracks normal operation. Displays INFO, WARNING, ERROR, CRITICAL messages. + - DEBUG provides detailed diagnostic info. Displays all log messages. + type: str + default: WARNING dnac_log_file_path: - description: - - Governs logging. Logs are recorded if dnac_log is True. - - If path is not specified, - - When 'dnac_log_append' is True, 'dnac.log' is generated in the - current Ansible directory; logs are appended. - - When 'dnac_log_append' is False, 'dnac.log' is generated; logs - are overwritten. - - If path is specified, - - When 'dnac_log_append' is True, the file opens in append mode. - - When 'dnac_log_append' is False, the file opens in write (w) mode. - - In shared file scenarios, without append mode, content is - overwritten after each module execution. - - For a shared log file, set append to False for the 1st module - (to overwrite); for subsequent modules, set append to True. - type: str - default: dnac.log + description: + - Governs logging. Logs are recorded if dnac_log is True. + - If path is not specified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. + type: str + default: dnac.log dnac_log_append: - description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. - type: bool - default: True + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. + type: bool + default: True validate_response_schema: description: - Flag for Cisco DNA Center SDK to enable the validation of request bodies against a JSON schema. From 11830ee5a639ae72f3856dc634b98f3476affd04 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 5 Feb 2024 11:23:36 +0530 Subject: [PATCH 71/76] Add the check while updating device if cli_transport is ssh2 then make it ssh --- plugins/modules/inventory_intent.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index c581a2cd63..abf2ba2b24 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2731,7 +2731,10 @@ def get_diff_merged(self, config): device_data = device_details[device_ip] if not playbook_params['cliTransport']: - playbook_params['cliTransport'] = device_data['protocol'] + if device_data['protocol'] == "ssh2": + playbook_params['cliTransport'] = "ssh" + else: + playbook_params['cliTransport'] = device_data['protocol'] if not playbook_params['snmpPrivProtocol']: playbook_params['snmpPrivProtocol'] = device_data['snmpv3_privacy_type'] From 1b11f0502325134507bdaca0651efa8a9d14083e Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 5 Feb 2024 11:53:20 +0530 Subject: [PATCH 72/76] Changes in the dnac_log documentation --- plugins/doc_fragments/intent_params.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/plugins/doc_fragments/intent_params.py b/plugins/doc_fragments/intent_params.py index c12143ab07..9c76aa25ac 100644 --- a/plugins/doc_fragments/intent_params.py +++ b/plugins/doc_fragments/intent_params.py @@ -50,13 +50,17 @@ class ModuleDocFragment(object): default: false dnac_log: description: - - Flag for logging playbook execution details. - - If set to true and dnac_log_file_path is provided, - - The log file will be created at the location of the execution with the provided name. - - If set to true and dnac_log_file_path is not provided, - - The log file will be created at the location of the execution with the name 'dnac.log'. - - If set to false, - - Logging will not take place. + - Flag to enable/disable playbook execution logging. + - When true and dnac_log_file_path is provided, + - Create the log file at the execution location with the specified name. + - When true and dnac_log_file_path is not provided, + - Create the log file at the execution location with the name 'dnac.log'. + - When false, + - Logging is disabled. + - If the log file doesn't exist, + - It is created in append or write mode based on the "dnac_log_append" flag. + - If the log file exists, + - It is overwritten or appended based on the "dnac_log_append" flag. type: bool default: false dnac_log_level: From 2cefbd08bd6730b6f228b61c5b34d8ad133a03cf Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 5 Feb 2024 13:31:44 +0530 Subject: [PATCH 73/76] Removed the documentation part of dnac_log_file_path and dnac_log_append --- plugins/modules/device_credential_intent.py | 343 ++++++++++---------- plugins/modules/discovery_intent.py | 21 -- plugins/modules/inventory_intent.py | 21 -- plugins/modules/network_settings_intent.py | 21 -- plugins/modules/pnp_intent.py | 21 -- plugins/modules/provision_intent.py | 21 -- plugins/modules/site_intent.py | 21 -- plugins/modules/swim_intent.py | 21 -- plugins/modules/template_intent.py | 21 -- 9 files changed, 167 insertions(+), 344 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index 3a808f24af..661192831c 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -25,27 +25,6 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: - dnac_log_file_path: - description: - - Governs logging. Logs are recorded if dnac_log is True. - - If unspecified, - - When 'dnac_log_append' is True, 'dnac.log' is generated in the - current Ansible directory; logs are appended. - - When 'dnac_log_append' is False, 'dnac.log' is generated; logs - are overwritten. - - If a path is specified, - - When 'dnac_log_append' is True, the file opens in append mode. - - When 'dnac_log_append' is False, the file opens in write (w) mode. - - In shared file scenarios, without append mode, content is - overwritten after each module execution. - - For a shared log file, set append to False for the 1st module - (to overwrite); for subsequent modules, set append to True. - type: str - default: dnac.log - dnac_log_append: - description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. - type: bool - default: True config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool @@ -1987,173 +1966,185 @@ def get_want_assign_credentials(self, AssignCredentials): site_id.append(siteId) want.update({"site_id": site_id}) global_credentials = self.get_global_credentials_params() - cliId = AssignCredentials.get("cli_credential").get("id") - cliDescription = AssignCredentials.get("cli_credential").get("description") - cliUsername = AssignCredentials.get("cli_credential").get("username") - if cliId or cliDescription and cliUsername: - - # All CLI details from the Cisco DNA Center - cli_details = global_credentials.get("cliCredential") - if not cli_details: - self.msg = "No Global CLI Credential is available" - self.status = "failed" - return self - cliDetail = None - if cliId: - cliDetail = get_dict_result(cli_details, "id", cliId) - if not cliDetail: - self.msg = "CLI credential ID is invalid" - self.status = "failed" - return self - elif cliDescription and cliUsername: - for item in cli_details: - if item.get("description") == cliDescription and \ - item.get("username") == cliUsername: - cliDetail = item - if not cliDetail: - self.msg = "CLI credential username and description is invalid" - self.status = "failed" - return self - want.get("assign_credentials").update({"cliId": cliDetail.get("id")}) - - snmpV2cReadId = AssignCredentials.get("snmp_v2c_read").get("id") - snmpV2cReadDescription = AssignCredentials.get("snmp_v2c_read").get("description") - if snmpV2cReadId or snmpV2cReadDescription: - - # All snmpV2cRead details from the Cisco DNA Center - snmpV2cRead_details = global_credentials.get("snmpV2cRead") - if not snmpV2cRead_details: - self.msg = "No Global snmpV2cRead Credential is available" - self.status = "failed" - return self - snmpV2cReadDetail = None - if snmpV2cReadId: - snmpV2cReadDetail = get_dict_result(snmpV2cRead_details, "id", snmpV2cReadId) - if not snmpV2cReadDetail: - self.msg = "snmpV2cRead credential ID is invalid" - self.status = "failed" - return self - elif snmpV2cReadDescription: - for item in snmpV2cRead_details: - if item.get("description") == snmpV2cReadDescription: - snmpV2cReadDetail = item - if not snmpV2cReadDetail: - self.msg = "snmpV2cRead credential username and description is invalid" + cli_credential = AssignCredentials.get("cli_credential") + if cli_credential: + cliId = cli_credential.get("id") + cliDescription = cli_credential.get("description") + cliUsername = cli_credential.get("username") + if cliId or cliDescription and cliUsername: + + # All CLI details from the Cisco DNA Center + cli_details = global_credentials.get("cliCredential") + if not cli_details: + self.msg = "No Global CLI Credential is available" self.status = "failed" return self - want.get("assign_credentials").update({"snmpV2ReadId": snmpV2cReadDetail.get("id")}) - - snmpV2cWriteId = AssignCredentials.get("snmp_v2c_write").get("id") - snmpV2cWriteDescription = AssignCredentials.get("snmp_v2c_write").get("description") - if snmpV2cWriteId or snmpV2cWriteDescription: - - # All snmpV2cWrite details from the Cisco DNA Center - snmpV2cWrite_details = global_credentials.get("snmpV2cWrite") - if not snmpV2cWrite_details: - self.msg = "No Global snmpV2cWrite Credential is available" - self.status = "failed" - return self - snmpV2cWriteDetail = None - if snmpV2cWriteId: - snmpV2cWriteDetail = get_dict_result(snmpV2cWrite_details, "id", snmpV2cWriteId) - if not snmpV2cWriteDetail: - self.msg = "snmpV2cWrite credential ID is invalid" - self.status = "failed" - return self - elif snmpV2cWriteDescription: - for item in snmpV2cWrite_details: - if item.get("description") == snmpV2cWriteDescription: - snmpV2cWriteDetail = item - if not snmpV2cWriteDetail: - self.msg = "snmpV2cWrite credential username and description is invalid" - self.status = "failed" - return self - want.get("assign_credentials").update({"snmpV2WriteId": snmpV2cWriteDetail.get("id")}) - - httpReadId = AssignCredentials.get("https_read").get("id") - httpReadDescription = AssignCredentials.get("https_read").get("description") - httpReadUsername = AssignCredentials.get("https_read").get("username") - if httpReadId or httpReadDescription and httpReadUsername: - - # All httpRead details from the Cisco DNA Center - httpRead_details = global_credentials.get("httpsRead") - if not httpRead_details: - self.msg = "No Global httpRead Credential is available" - self.status = "failed" - return self - httpReadDetail = None - if httpReadId: - httpReadDetail = get_dict_result(httpRead_details, "id", httpReadId) - if not httpReadDetail: - self.msg = "httpRead credential ID is invalid" - self.status = "failed" - return self - elif httpReadDescription and httpReadUsername: - for item in httpRead_details: - if item.get("description") == httpReadDescription and \ - item.get("username") == httpReadUsername: - httpReadDetail = item - if not httpReadDetail: - self.msg = "httpRead credential description and username is invalid" + cliDetail = None + if cliId: + cliDetail = get_dict_result(cli_details, "id", cliId) + if not cliDetail: + self.msg = "CLI credential ID is invalid" + self.status = "failed" + return self + elif cliDescription and cliUsername: + for item in cli_details: + if item.get("description") == cliDescription and \ + item.get("username") == cliUsername: + cliDetail = item + if not cliDetail: + self.msg = "CLI credential username and description is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"cliId": cliDetail.get("id")}) + + snmp_v2c_read = AssignCredentials.get("snmp_v2c_read") + if snmp_v2c_read: + snmpV2cReadId = snmp_v2c_read.get("id") + snmpV2cReadDescription = snmp_v2c_read.get("description") + if snmpV2cReadId or snmpV2cReadDescription: + + # All snmpV2cRead details from the Cisco DNA Center + snmpV2cRead_details = global_credentials.get("snmpV2cRead") + if not snmpV2cRead_details: + self.msg = "No Global snmpV2cRead Credential is available" self.status = "failed" return self - want.get("assign_credentials").update({"httpRead": httpReadDetail.get("id")}) - - httpWriteId = AssignCredentials.get("https_write").get("id") - httpWriteDescription = AssignCredentials.get("https_write").get("description") - httpWriteUsername = AssignCredentials.get("https_write").get("username") - if httpWriteId or httpWriteDescription and httpWriteUsername: - - # All httpWrite details from the Cisco DNA Center - httpWrite_details = global_credentials.get("httpsWrite") - if not httpWrite_details: - self.msg = "No Global httpWrite Credential is available" - self.status = "failed" - return self - httpWriteDetail = None - if httpWriteId: - httpWriteDetail = get_dict_result(httpWrite_details, "id", httpWriteId) - if not httpWriteDetail: - self.msg = "httpWrite credential ID is invalid" + snmpV2cReadDetail = None + if snmpV2cReadId: + snmpV2cReadDetail = get_dict_result(snmpV2cRead_details, "id", snmpV2cReadId) + if not snmpV2cReadDetail: + self.msg = "snmpV2cRead credential ID is invalid" + self.status = "failed" + return self + elif snmpV2cReadDescription: + for item in snmpV2cRead_details: + if item.get("description") == snmpV2cReadDescription: + snmpV2cReadDetail = item + if not snmpV2cReadDetail: + self.msg = "snmpV2cRead credential username and description is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"snmpV2ReadId": snmpV2cReadDetail.get("id")}) + + snmp_v2c_write = AssignCredentials.get("snmp_v2c_write") + if snmp_v2c_write: + snmpV2cWriteId = snmp_v2c_write.get("id") + snmpV2cWriteDescription = snmp_v2c_write.get("description") + if snmpV2cWriteId or snmpV2cWriteDescription: + + # All snmpV2cWrite details from the Cisco DNA Center + snmpV2cWrite_details = global_credentials.get("snmpV2cWrite") + if not snmpV2cWrite_details: + self.msg = "No Global snmpV2cWrite Credential is available" self.status = "failed" return self - elif httpWriteDescription and httpWriteUsername: - for item in httpWrite_details: - if item.get("description") == httpWriteDescription and \ - item.get("username") == httpWriteUsername: - httpWriteDetail = item - if not httpWriteDetail: - self.msg = "httpWrite credential description and username is invalid" + snmpV2cWriteDetail = None + if snmpV2cWriteId: + snmpV2cWriteDetail = get_dict_result(snmpV2cWrite_details, "id", snmpV2cWriteId) + if not snmpV2cWriteDetail: + self.msg = "snmpV2cWrite credential ID is invalid" + self.status = "failed" + return self + elif snmpV2cWriteDescription: + for item in snmpV2cWrite_details: + if item.get("description") == snmpV2cWriteDescription: + snmpV2cWriteDetail = item + if not snmpV2cWriteDetail: + self.msg = "snmpV2cWrite credential username and description is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"snmpV2WriteId": snmpV2cWriteDetail.get("id")}) + + https_read = AssignCredentials.get("https_read") + if https_read: + httpReadId = https_read.get("id") + httpReadDescription = https_read.get("description") + httpReadUsername = https_read.get("username") + if httpReadId or httpReadDescription and httpReadUsername: + + # All httpRead details from the Cisco DNA Center + httpRead_details = global_credentials.get("httpsRead") + if not httpRead_details: + self.msg = "No Global httpRead Credential is available" self.status = "failed" return self - want.get("assign_credentials").update({"httpWrite": httpWriteDetail.get("id")}) - - snmpV3Id = AssignCredentials.get("snmp_v3").get("id") - snmpV3Description = AssignCredentials.get("snmp_v3").get("description") - if snmpV3Id or snmpV3Description: - - # All snmpV3 details from the Cisco DNA Center - snmpV3_details = global_credentials.get("snmpV3") - if not snmpV3_details: - self.msg = "No Global snmpV3 Credential is available" - self.status = "failed" - return self - snmpV3Detail = None - if snmpV3Id: - snmpV3Detail = get_dict_result(snmpV3_details, "id", snmpV3Id) - if not snmpV3Detail: - self.msg = "snmpV3 credential ID is invalid" + httpReadDetail = None + if httpReadId: + httpReadDetail = get_dict_result(httpRead_details, "id", httpReadId) + if not httpReadDetail: + self.msg = "httpRead credential ID is invalid" + self.status = "failed" + return self + elif httpReadDescription and httpReadUsername: + for item in httpRead_details: + if item.get("description") == httpReadDescription and \ + item.get("username") == httpReadUsername: + httpReadDetail = item + if not httpReadDetail: + self.msg = "httpRead credential description and username is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"httpRead": httpReadDetail.get("id")}) + + https_write = AssignCredentials.get("https_write") + if https_write: + httpWriteId = https_write.get("id") + httpWriteDescription = https_write.get("description") + httpWriteUsername = https_write.get("username") + if httpWriteId or httpWriteDescription and httpWriteUsername: + + # All httpWrite details from the Cisco DNA Center + httpWrite_details = global_credentials.get("httpsWrite") + if not httpWrite_details: + self.msg = "No Global httpWrite Credential is available" self.status = "failed" return self - elif snmpV3Description: - for item in snmpV3_details: - if item.get("description") == snmpV3Description: - snmpV3Detail = item - if not snmpV3Detail: - self.msg = "snmpV2cWrite credential username and description is invalid" + httpWriteDetail = None + if httpWriteId: + httpWriteDetail = get_dict_result(httpWrite_details, "id", httpWriteId) + if not httpWriteDetail: + self.msg = "httpWrite credential ID is invalid" + self.status = "failed" + return self + elif httpWriteDescription and httpWriteUsername: + for item in httpWrite_details: + if item.get("description") == httpWriteDescription and \ + item.get("username") == httpWriteUsername: + httpWriteDetail = item + if not httpWriteDetail: + self.msg = "httpWrite credential description and username is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"httpWrite": httpWriteDetail.get("id")}) + + snmp_v3 = AssignCredentials.get("snmp_v3") + if snmp_v3: + snmpV3Id = snmp_v3.get("id") + snmpV3Description = snmp_v3.get("description") + if snmpV3Id or snmpV3Description: + + # All snmpV3 details from the Cisco DNA Center + snmpV3_details = global_credentials.get("snmpV3") + if not snmpV3_details: + self.msg = "No Global snmpV3 Credential is available" self.status = "failed" return self - want.get("assign_credentials").update({"snmpV3Id": snmpV3Detail.get("id")}) + snmpV3Detail = None + if snmpV3Id: + snmpV3Detail = get_dict_result(snmpV3_details, "id", snmpV3Id) + if not snmpV3Detail: + self.msg = "snmpV3 credential ID is invalid" + self.status = "failed" + return self + elif snmpV3Description: + for item in snmpV3_details: + if item.get("description") == snmpV3Description: + snmpV3Detail = item + if not snmpV3Detail: + self.msg = "snmpV2cWrite credential username and description is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"snmpV3Id": snmpV3Detail.get("id")}) self.log("Desired State (want): {0}".format(want), "INFO") self.want.update(want) self.msg = "Collected the Credentials needed to be assigned from the Cisco DNA Center" diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 9064f76485..3026ca165a 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -22,27 +22,6 @@ author: Abinash Mishra (@abimishr) Phan Nguyen (phannguy) options: - dnac_log_file_path: - description: - - Governs logging. Logs are recorded if dnac_log is True. - - If unspecified, - - When 'dnac_log_append' is True, 'dnac.log' is generated in the - current Ansible directory; logs are appended. - - When 'dnac_log_append' is False, 'dnac.log' is generated; logs - are overwritten. - - If a path is specified, - - When 'dnac_log_append' is True, the file opens in append mode. - - When 'dnac_log_append' is False, the file opens in write (w) mode. - - In shared file scenarios, without append mode, content is - overwritten after each module execution. - - For a shared log file, set append to False for the 1st module - (to overwrite); for subsequent modules, set append to True. - type: str - default: dnac.log - dnac_log_append: - description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. - type: bool - default: True config_verify: description: Set to True to verify the Cisco DNA Center config after applying the playbook config. type: bool diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index abf2ba2b24..03f24bbdc5 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -24,27 +24,6 @@ author: Abhishek Maheshwari (@abmahesh) Madhan Sankaranarayanan (@madhansansel) options: - dnac_log_file_path: - description: - - Governs logging. Logs are recorded if dnac_log is True. - - If unspecified, - - When 'dnac_log_append' is True, 'dnac.log' is generated in the - current Ansible directory; logs are appended. - - When 'dnac_log_append' is False, 'dnac.log' is generated; logs - are overwritten. - - If a path is specified, - - When 'dnac_log_append' is True, the file opens in append mode. - - When 'dnac_log_append' is False, the file opens in write (w) mode. - - In shared file scenarios, without append mode, content is - overwritten after each module execution. - - For a shared log file, set append to False for the 1st module - (to overwrite); for subsequent modules, set append to True. - type: str - default: dnac.log - dnac_log_append: - description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. - type: bool - default: True config_verify: description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 75ca0076c2..bb3ab1d6de 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -25,27 +25,6 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: - dnac_log_file_path: - description: - - Governs logging. Logs are recorded if dnac_log is True. - - If unspecified, - - When 'dnac_log_append' is True, 'dnac.log' is generated in the - current Ansible directory; logs are appended. - - When 'dnac_log_append' is False, 'dnac.log' is generated; logs - are overwritten. - - If a path is specified, - - When 'dnac_log_append' is True, the file opens in append mode. - - When 'dnac_log_append' is False, the file opens in write (w) mode. - - In shared file scenarios, without append mode, content is - overwritten after each module execution. - - For a shared log file, set append to False for the 1st module - (to overwrite); for subsequent modules, set append to True. - type: str - default: dnac.log - dnac_log_append: - description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. - type: bool - default: True config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 7a8ae5c2cd..85436a1cf9 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -24,27 +24,6 @@ Rishita Chowdhary (@rishitachowdhary) Abinash Mishra (@abimishr) options: - dnac_log_file_path: - description: - - Governs logging. Logs are recorded if dnac_log is True. - - If unspecified, - - When 'dnac_log_append' is True, 'dnac.log' is generated in the - current Ansible directory; logs are appended. - - When 'dnac_log_append' is False, 'dnac.log' is generated; logs - are overwritten. - - If a path is specified, - - When 'dnac_log_append' is True, the file opens in append mode. - - When 'dnac_log_append' is False, the file opens in write (w) mode. - - In shared file scenarios, without append mode, content is - overwritten after each module execution. - - For a shared log file, set append to False for the 1st module - (to overwrite); for subsequent modules, set append to True. - type: str - default: dnac.log - dnac_log_append: - description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. - type: bool - default: True config_verify: description: Set to True to verify the Cisco DNA Center config after applying the playbook config. type: bool diff --git a/plugins/modules/provision_intent.py b/plugins/modules/provision_intent.py index c65a3faba7..4e92722264 100644 --- a/plugins/modules/provision_intent.py +++ b/plugins/modules/provision_intent.py @@ -21,27 +21,6 @@ - cisco.dnac.intent_params author: Abinash Mishra (@abimishr) options: - dnac_log_file_path: - description: - - Governs logging. Logs are recorded if dnac_log is True. - - If unspecified, - - When 'dnac_log_append' is True, 'dnac.log' is generated in the - current Ansible directory; logs are appended. - - When 'dnac_log_append' is False, 'dnac.log' is generated; logs - are overwritten. - - If a path is specified, - - When 'dnac_log_append' is True, the file opens in append mode. - - When 'dnac_log_append' is False, the file opens in write (w) mode. - - In shared file scenarios, without append mode, content is - overwritten after each module execution. - - For a shared log file, set append to False for the 1st module - (to overwrite); for subsequent modules, set append to True. - type: str - default: dnac.log - dnac_log_append: - description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. - type: bool - default: True config_verify: description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 814d32b53c..e5ff977f4b 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -25,27 +25,6 @@ Rishita Chowdhary (@rishitachowdhary) Abhishek Maheshwari (@abhishekmaheshwari) options: - dnac_log_file_path: - description: - - Governs logging. Logs are recorded if dnac_log is True. - - If unspecified, - - When 'dnac_log_append' is True, 'dnac.log' is generated in the - current Ansible directory; logs are appended. - - When 'dnac_log_append' is False, 'dnac.log' is generated; logs - are overwritten. - - If a path is specified, - - When 'dnac_log_append' is True, the file opens in append mode. - - When 'dnac_log_append' is False, the file opens in write (w) mode. - - In shared file scenarios, without append mode, content is - overwritten after each module execution. - - For a shared log file, set append to False for the 1st module - (to overwrite); for subsequent modules, set append to True. - type: str - default: dnac.log - dnac_log_append: - description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. - type: bool - default: True config_verify: description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 7026a0267a..627cc66417 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -30,27 +30,6 @@ Rishita Chowdhary (@rishitachowdhary) Abhishek Maheshwari (@abmahesh) options: - dnac_log_file_path: - description: - - Governs logging. Logs are recorded if dnac_log is True. - - If unspecified, - - When 'dnac_log_append' is True, 'dnac.log' is generated in the - current Ansible directory; logs are appended. - - When 'dnac_log_append' is False, 'dnac.log' is generated; logs - are overwritten. - - If a path is specified, - - When 'dnac_log_append' is True, the file opens in append mode. - - When 'dnac_log_append' is False, the file opens in write (w) mode. - - In shared file scenarios, without append mode, content is - overwritten after each module execution. - - For a shared log file, set append to False for the 1st module - (to overwrite); for subsequent modules, set append to True. - type: str - default: dnac.log - dnac_log_append: - description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. - type: bool - default: True config_verify: description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index c766626283..4052424087 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -30,27 +30,6 @@ Akash Bhaskaran (@akabhask) Muthu Rakesh (@MUTHU-RAKESH-27) options: - dnac_log_file_path: - description: - - Governs logging. Logs are recorded if dnac_log is True. - - If unspecified, - - When 'dnac_log_append' is True, 'dnac.log' is generated in the - current Ansible directory; logs are appended. - - When 'dnac_log_append' is False, 'dnac.log' is generated; logs - are overwritten. - - If a path is specified, - - When 'dnac_log_append' is True, the file opens in append mode. - - When 'dnac_log_append' is False, the file opens in write (w) mode. - - In shared file scenarios, without append mode, content is - overwritten after each module execution. - - For a shared log file, set append to False for the 1st module - (to overwrite); for subsequent modules, set append to True. - type: str - default: dnac.log - dnac_log_append: - description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. - type: bool - default: True config_verify: description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool From bab6575e2ac0f23060646d0a72135a1978495664 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 5 Feb 2024 14:43:48 +0530 Subject: [PATCH 74/76] Make the netconf port to retain previous value while updating device detail --- plugins/modules/inventory_intent.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index abf2ba2b24..95305b418c 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2742,6 +2742,7 @@ def get_diff_merged(self, config): 'username': device_data['cli_username'], 'password': device_data['cli_password'], 'enable_password': device_data['cli_enable_password'], + 'netconf_port': device_data['netconf_port'], } if device_data['snmp_version'] == '3': @@ -2754,9 +2755,10 @@ def get_diff_merged(self, config): 'username': 'userName', 'password': 'password', 'enable_password': 'enablePassword', - 'snmp_username': 'snmpUserName' + 'snmp_username': 'snmpUserName', + 'netconf_port': 'netconfPort' } - device_update_key_list = ["username", "password", "enable_password", "snmp_username"] + device_update_key_list = ["username", "password", "enable_password", "snmp_username", "netconf_port"] for key in device_update_key_list: mapped_key = device_key_mapping[key] @@ -2776,6 +2778,9 @@ def get_diff_merged(self, config): playbook_params.pop('snmpPrivPassphrase', None) playbook_params.pop('snmpPrivProtocol', None) + if playbook_params['netconfPort'] == " ": + playbook_params['netconfPort'] = None + try: if playbook_params['updateMgmtIPaddressList']: new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] From a35c4d1c614ed93384d74d4860eb856d9d52ed71 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 5 Feb 2024 16:42:33 +0530 Subject: [PATCH 75/76] Addressed the review comments --- plugins/modules/device_credential_intent.py | 38 ++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index 661192831c..aa12786788 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -1971,19 +1971,19 @@ def get_want_assign_credentials(self, AssignCredentials): cliId = cli_credential.get("id") cliDescription = cli_credential.get("description") cliUsername = cli_credential.get("username") - if cliId or cliDescription and cliUsername: + if cliId or cliDescription and cliUsername: # All CLI details from the Cisco DNA Center cli_details = global_credentials.get("cliCredential") if not cli_details: - self.msg = "No Global CLI Credential is available" + self.msg = "Global CLI credential is not available" self.status = "failed" return self cliDetail = None if cliId: cliDetail = get_dict_result(cli_details, "id", cliId) if not cliDetail: - self.msg = "CLI credential ID is invalid" + self.msg = "The ID for the CLI credential is not valid." self.status = "failed" return self elif cliDescription and cliUsername: @@ -1992,7 +1992,7 @@ def get_want_assign_credentials(self, AssignCredentials): item.get("username") == cliUsername: cliDetail = item if not cliDetail: - self.msg = "CLI credential username and description is invalid" + self.msg = "The username and description of the CLI credential are invalid" self.status = "failed" return self want.get("assign_credentials").update({"cliId": cliDetail.get("id")}) @@ -2006,14 +2006,14 @@ def get_want_assign_credentials(self, AssignCredentials): # All snmpV2cRead details from the Cisco DNA Center snmpV2cRead_details = global_credentials.get("snmpV2cRead") if not snmpV2cRead_details: - self.msg = "No Global snmpV2cRead Credential is available" + self.msg = "Global snmpV2cRead credential is not available" self.status = "failed" return self snmpV2cReadDetail = None if snmpV2cReadId: snmpV2cReadDetail = get_dict_result(snmpV2cRead_details, "id", snmpV2cReadId) if not snmpV2cReadDetail: - self.msg = "snmpV2cRead credential ID is invalid" + self.msg = "The ID of the snmpV2cRead credential is not valid." self.status = "failed" return self elif snmpV2cReadDescription: @@ -2021,7 +2021,7 @@ def get_want_assign_credentials(self, AssignCredentials): if item.get("description") == snmpV2cReadDescription: snmpV2cReadDetail = item if not snmpV2cReadDetail: - self.msg = "snmpV2cRead credential username and description is invalid" + self.msg = "The username and description for the snmpV2cRead credential are invalid." self.status = "failed" return self want.get("assign_credentials").update({"snmpV2ReadId": snmpV2cReadDetail.get("id")}) @@ -2035,14 +2035,14 @@ def get_want_assign_credentials(self, AssignCredentials): # All snmpV2cWrite details from the Cisco DNA Center snmpV2cWrite_details = global_credentials.get("snmpV2cWrite") if not snmpV2cWrite_details: - self.msg = "No Global snmpV2cWrite Credential is available" + self.msg = "Global snmpV2cWrite Credential is not available" self.status = "failed" return self snmpV2cWriteDetail = None if snmpV2cWriteId: snmpV2cWriteDetail = get_dict_result(snmpV2cWrite_details, "id", snmpV2cWriteId) if not snmpV2cWriteDetail: - self.msg = "snmpV2cWrite credential ID is invalid" + self.msg = "The ID of the snmpV2cWrite credential is invalid." self.status = "failed" return self elif snmpV2cWriteDescription: @@ -2050,7 +2050,7 @@ def get_want_assign_credentials(self, AssignCredentials): if item.get("description") == snmpV2cWriteDescription: snmpV2cWriteDetail = item if not snmpV2cWriteDetail: - self.msg = "snmpV2cWrite credential username and description is invalid" + self.msg = "The username and description of the snmpV2cWrite credential are invalid." self.status = "failed" return self want.get("assign_credentials").update({"snmpV2WriteId": snmpV2cWriteDetail.get("id")}) @@ -2065,14 +2065,14 @@ def get_want_assign_credentials(self, AssignCredentials): # All httpRead details from the Cisco DNA Center httpRead_details = global_credentials.get("httpsRead") if not httpRead_details: - self.msg = "No Global httpRead Credential is available" + self.msg = "Global httpRead Credential is not available." self.status = "failed" return self httpReadDetail = None if httpReadId: httpReadDetail = get_dict_result(httpRead_details, "id", httpReadId) if not httpReadDetail: - self.msg = "httpRead credential ID is invalid" + self.msg = "The ID of the httpRead credential is not valid." self.status = "failed" return self elif httpReadDescription and httpReadUsername: @@ -2081,7 +2081,7 @@ def get_want_assign_credentials(self, AssignCredentials): item.get("username") == httpReadUsername: httpReadDetail = item if not httpReadDetail: - self.msg = "httpRead credential description and username is invalid" + self.msg = "The description and username for the httpRead credential are invalid." self.status = "failed" return self want.get("assign_credentials").update({"httpRead": httpReadDetail.get("id")}) @@ -2096,14 +2096,14 @@ def get_want_assign_credentials(self, AssignCredentials): # All httpWrite details from the Cisco DNA Center httpWrite_details = global_credentials.get("httpsWrite") if not httpWrite_details: - self.msg = "No Global httpWrite Credential is available" + self.msg = "Global httpWrite credential is not available." self.status = "failed" return self httpWriteDetail = None if httpWriteId: httpWriteDetail = get_dict_result(httpWrite_details, "id", httpWriteId) if not httpWriteDetail: - self.msg = "httpWrite credential ID is invalid" + self.msg = "The ID of the httpWrite credential is not valid." self.status = "failed" return self elif httpWriteDescription and httpWriteUsername: @@ -2112,7 +2112,7 @@ def get_want_assign_credentials(self, AssignCredentials): item.get("username") == httpWriteUsername: httpWriteDetail = item if not httpWriteDetail: - self.msg = "httpWrite credential description and username is invalid" + self.msg = "The description and username for the httpWrite credential are invalid." self.status = "failed" return self want.get("assign_credentials").update({"httpWrite": httpWriteDetail.get("id")}) @@ -2126,14 +2126,14 @@ def get_want_assign_credentials(self, AssignCredentials): # All snmpV3 details from the Cisco DNA Center snmpV3_details = global_credentials.get("snmpV3") if not snmpV3_details: - self.msg = "No Global snmpV3 Credential is available" + self.msg = "Global snmpV3 Credential is not available." self.status = "failed" return self snmpV3Detail = None if snmpV3Id: snmpV3Detail = get_dict_result(snmpV3_details, "id", snmpV3Id) if not snmpV3Detail: - self.msg = "snmpV3 credential ID is invalid" + self.msg = "The ID of the snmpV3 credential is not valid." self.status = "failed" return self elif snmpV3Description: @@ -2141,7 +2141,7 @@ def get_want_assign_credentials(self, AssignCredentials): if item.get("description") == snmpV3Description: snmpV3Detail = item if not snmpV3Detail: - self.msg = "snmpV2cWrite credential username and description is invalid" + self.msg = "The username and description for the snmpV2cWrite credential are invalid." self.status = "failed" return self want.get("assign_credentials").update({"snmpV3Id": snmpV3Detail.get("id")}) From e2784a468e94ece7a7653428f49642e87a336cfe Mon Sep 17 00:00:00 2001 From: Madhan Date: Mon, 5 Feb 2024 23:36:03 +0530 Subject: [PATCH 76/76] Enhancements in Cisco Catalyst Center logging --- changelogs/changelog.yaml | 10 +++++++++- galaxy.yml | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index ac477470e6..74f9969840 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -783,4 +783,12 @@ releases: changes: release_summary: Fixing swim_import_local action. minor_changes: - - Building additional parameters needed in sdk. \ No newline at end of file + - Building additional parameters needed in sdk. + 6.10.4: + release_date: "2024-02-05" + changes: + release_summary: Enhancements in Cisco Catalyst Center logging + minor_changes: + - Introducing log levels and log file path + - Updated Documentation in template intent module + - Enhancements in device_credential, inventory, discovery and template intent modules. diff --git a/galaxy.yml b/galaxy.yml index a8cb8689e1..7aeb8d456b 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.10.3 +version: 6.10.4 readme: README.md authors: - Rafael Campos