From 85ff014a9a368b686e5dd16840c1ebe17d34cde4 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 25 Jun 2025 14:52:58 -0400 Subject: [PATCH 01/32] synth class + write back each function --- aci-preupgrade-validation-script.py | 70 +++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 7ccd3134..7a3ea82a 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -64,6 +64,69 @@ warnings.simplefilter(action='ignore', category=FutureWarning) +class syntheticMaintPValidate: + def __init__(self, name, description): + self.name = name + self.description = description + self.reason = "" + self.criticality = "critical" + self.passed = True + self.recommended_action = "" + self.sub_reason = "" + self.showValidation = True + self.failureDetails = self.initFailureDetails() + + def initFailureDetails(self): + failure_details = {} + failure_details["fail_type"] = "" + failure_details["header"] = "" + failure_details["footer"] = "" + failure_details["column"] = [] + failure_details["row"] = [] + return failure_details + + def updateFailureDetails(self, result, recommended_action, reason, header, footer, column, row): + self.recommended_action = recommended_action + self.reason = reason + self.passed = False + self.failureDetails["fail_type"] = result + self.failureDetails["header"] = header + self.failureDetails["footer"] = footer + self.failureDetails["column"] = column + self.failureDetails["row"] = row + + def buildResult(self): + result = { + "syntheticMaintPValidate": { + "attributes": { + "name": self.name, + "description": self.description, + "reason": self.reason, + "criticality": self.criticality, + "passed": self.passed, + "recommended_action": self.recommended_action, + "sub_reason": self.sub_reason, + "showValidation": self.showValidation, + "failureDetails": self.failureDetails + } + } + } + return result + + def writeResult(self): + """ + Write the results of the syntheticMaintPValidate object to a file. + :return: None + """ + filename = self.name + '.json' + path = "cx-preupgrade-validation-results" + if not os.path.isdir(path): + os.mkdir(path) + with open(os.path.join(f'{path}', filename), "w") as f: + json.dump(self.buildResult(), f, indent=4) + #f.write(self.buildResult()) + + class OldVerClassNotFound(Exception): """ Later versions of ACI can have class properties not found in older versions """ pass @@ -1032,6 +1095,13 @@ def print_result(title, result, msg='', recommended_action='', doc_url='', adjust_title=False): + synth = syntheticMaintPValidate(title, "") + if result in [FAIL_O, FAIL_UF, ERROR, MANUAL, POST]: + # TODO: deal with unformatted data and headers + synth.updateFailureDetails( + result=result, recommended_action=recommended_action, reason=msg, header="", footer=doc_url, column=headers, row=data + ) + synth.writeResult() padding = 120 - len(title) - len(msg) if adjust_title: padding += len(title) + 18 output = '{}{:>{}}'.format(msg, result, padding) From 3a2bb9f4bb0609bf9ffdf72ada8c7038d36d7077 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 25 Jun 2025 15:08:47 -0400 Subject: [PATCH 02/32] add inspect to pull func name --- aci-preupgrade-validation-script.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 7a3ea82a..1955de37 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -22,6 +22,7 @@ from getpass import getpass from collections import defaultdict from datetime import datetime +import inspect import warnings import time import pexpect @@ -65,7 +66,8 @@ class syntheticMaintPValidate: - def __init__(self, name, description): + def __init__(self, func, name, description): + self.function_name = func self.name = name self.description = description self.reason = "" @@ -118,7 +120,7 @@ def writeResult(self): Write the results of the syntheticMaintPValidate object to a file. :return: None """ - filename = self.name + '.json' + filename = self.function_name + '.json' path = "cx-preupgrade-validation-results" if not os.path.isdir(path): os.mkdir(path) @@ -1094,8 +1096,9 @@ def print_result(title, result, msg='', unformatted_headers=None, unformatted_data=None, recommended_action='', doc_url='', - adjust_title=False): - synth = syntheticMaintPValidate(title, "") + adjust_title=False, + func="test"): + synth = syntheticMaintPValidate(func, title, "") if result in [FAIL_O, FAIL_UF, ERROR, MANUAL, POST]: # TODO: deal with unformatted data and headers synth.updateFailureDetails( @@ -4687,7 +4690,7 @@ def fc_ex_model_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func=inspect.currentframe().f_code.co_name) return result From 8671373be3392ab2c82aab9418fb5aa8b3563260 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 25 Jun 2025 16:12:58 -0400 Subject: [PATCH 03/32] fix pytest from non str objects --- aci-preupgrade-validation-script.py | 50 +++++++++++++++-------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 1955de37..8f3b6eae 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -116,17 +116,13 @@ def buildResult(self): return result def writeResult(self): - """ - Write the results of the syntheticMaintPValidate object to a file. - :return: None - """ - filename = self.function_name + '.json' + cleaned_name = re.sub(r'[^a-zA-Z0-9_]+|\s+', '_', self.name) + filename = cleaned_name + '.json' path = "cx-preupgrade-validation-results" if not os.path.isdir(path): os.mkdir(path) with open(os.path.join(f'{path}', filename), "w") as f: json.dump(self.buildResult(), f, indent=4) - #f.write(self.buildResult()) class OldVerClassNotFound(Exception): @@ -1143,7 +1139,11 @@ def _icurl(apitype, query, page=0, page_size=100000): pre = '&' if '?' in query else '?' query += '{}page={}&page-size={}'.format(pre, page, page_size) uri = 'http://127.0.0.1:7777/api/{}/{}'.format(apitype, query) - cmd = ['icurl', '-gs', uri] + if TOKEN: + cookie = "APIC-cookie={}".format(TOKEN) + cmd = ['curl', '-b', cookie, '-gs', uri] + else: + cmd = ['icurl', '-gs', uri] logging.info('cmd = ' + ' '.join(cmd)) response = subprocess.check_output(cmd) logging.debug('response: ' + str(response)) @@ -2021,7 +2021,7 @@ def switch_ssd_check(index, total_checks, **kwargs): print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) return result - +# Connection based check def apic_ssd_check(index, total_checks, cversion, **kwargs): title = 'APIC SSD Health' result = FAIL_UF @@ -2669,7 +2669,7 @@ def lldp_with_infra_vlan_mismatch_check(index, total_checks, **kwargs): print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) return result - +# Connection based check def apic_version_md5_check(index, total_checks, tversion, username, password, **kwargs): title = 'APIC Target version image and MD5 hash' result = FAIL_UF @@ -2689,7 +2689,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** desc = fm_mo["firmwareFirmware"]['attributes']["description"] md5 = fm_mo["firmwareFirmware"]['attributes']["checksum"] if "Image signing verification failed" in desc: - data.append(["All", tversion, md5, + data.append(["All", str(tversion), md5, 'Target image is corrupted', 'Delete and Upload Again']) image_validaton = False @@ -2716,7 +2716,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** c.log = LOG_FILE c.connect() except Exception as e: - data.append([apic_name, '-', '-', e, '-']) + data.append([apic_name, '-', '-', json.dumps(e.__dict__), '-']) print_result(node_title, ERROR) has_error = True continue @@ -2726,7 +2726,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** tversion.dot_version) except Exception as e: data.append([apic_name, '-', '-', - 'ls command via ssh failed due to:{}'.format(e), '-']) + 'ls command via ssh failed due to:{}'.format(json.dumps(e.__dict__)), '-']) print_result(node_title, ERROR) has_error = True continue @@ -2740,7 +2740,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** tversion.dot_version) except Exception as e: data.append([apic_name, str(tversion), '-', - 'failed to check md5sum via ssh due to:{}'.format(e), '-']) + 'failed to check md5sum via ssh due to:{}'.format(json.dumps(e.__dict__)), '-']) print_result(node_title, ERROR) has_error = True continue @@ -2774,7 +2774,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** print_result(title, result, msg, headers, data, adjust_title=True) return result - +# Connection Based Check def standby_apic_disk_space_check(index, total_checks, **kwargs): title = 'Standby APIC Disk Space Usage' result = FAIL_UF @@ -4689,8 +4689,8 @@ def fc_ex_model_check(index, total_checks, tversion, **kwargs): data.append([node_dn, model]) if data: result = FAIL_O - - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func=inspect.currentframe().f_code.co_name) + # func=inspect.currentframe().f_code.co_name + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) return result @@ -5026,9 +5026,9 @@ def aes_encryption_check(index, total_checks, tversion, **kwargs): cryptkeys = icurl("mo", "uni/exportcryptkey.json") if not cryptkeys: - data = [[tversion, "Object Not Found", impact]] + data = [[str(tversion), "Object Not Found", impact]] elif cryptkeys[0]["pkiExportEncryptionKey"]["attributes"]["strongEncryptionEnabled"] != "yes": - data = [[tversion, "Disabled", impact]] + data = [[str(tversion), "Disabled", impact]] else: result = PASS @@ -5080,12 +5080,12 @@ def service_bd_forceful_routing_check(index, total_checks, cversion, tversion, * print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action, doc_url) return result - +# Connection Base Check def observer_db_size_check(index, total_checks, username, password, **kwargs): title = 'Observer Database Size' result = PASS msg = '' - headers = ["Node" , "File Location", "Size (GB)"] + headers = ["Node", "File Location", "Size (GB)"] data = [] recommended_action = 'Contact TAC to analyze and truncate large DB files' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#observer-database-size' @@ -5102,7 +5102,7 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): prints('') for apic in controllers: attr = apic['topSystem']['attributes'] - node_title = 'Checking %s...' % attr['name'] + node_title = 'Checking %s...' % attr['name'] print_title(node_title) try: c = Connection(attr['address']) @@ -5111,7 +5111,7 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): c.log = LOG_FILE c.connect() except Exception as e: - data.append([attr['id'], attr['name'], e]) + data.append([attr['id'], attr['name'], json.dumps(e.__dict__)]) print_result(node_title, ERROR) has_error = True continue @@ -5131,9 +5131,9 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): file_size = size_match.group("size") file_name = "/data2/dbstats/" + size_match.group("file") data.append([attr['id'], file_name, file_size]) - print_result(node_title, DONE) + print_result(node_title, DONE) except Exception as e: - data.append([attr['id'], attr['name'], e]) + data.append([attr['id'], attr['name'], json.dumps(e.__dict__)]) print_result(node_title, ERROR) has_error = True continue @@ -5178,6 +5178,8 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') prints('To use a non-default Login Domain, enter apic#DOMAIN\\\\USERNAME') + global TOKEN + TOKEN = os.getenv('webtoken') username, password = get_credentials() try: cversion = get_current_version() From 40c7485b255e605b49042f18a3d3c0ae3c94118b Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 25 Jun 2025 16:15:02 -0400 Subject: [PATCH 04/32] py2 --- aci-preupgrade-validation-script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 8f3b6eae..3b768269 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -121,7 +121,7 @@ def writeResult(self): path = "cx-preupgrade-validation-results" if not os.path.isdir(path): os.mkdir(path) - with open(os.path.join(f'{path}', filename), "w") as f: + with open(os.path.join(path, filename), "w") as f: json.dump(self.buildResult(), f, indent=4) From d3ad34854a6b35bcd7112153012f5e0733a1529f Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 26 Jun 2025 11:15:26 -0400 Subject: [PATCH 05/32] args for input + global cleanup --- aci-preupgrade-validation-script.py | 51 ++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 3b768269..a42e7703 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -22,7 +22,7 @@ from getpass import getpass from collections import defaultdict from datetime import datetime -import inspect +import argparse import warnings import time import pexpect @@ -5175,15 +5175,26 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): if __name__ == "__main__": - prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) - prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') - prints('To use a non-default Login Domain, enter apic#DOMAIN\\\\USERNAME') - global TOKEN - TOKEN = os.getenv('webtoken') - username, password = get_credentials() + parser = argparse.ArgumentParser() + parser.add_argument("-v", "--version", help="CCO Version string, e.g. 6.0(2a)", type=str, default=None) + parser.add_argument("-s", "--scriptcontainer", help="Running in APIC script container, icurl checks only", action="store_true") + args = parser.parse_args() + tversion = None + if args.scriptcontainer: + username = password = None + else: + prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) + prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') + prints('To use a non-default Login Domain, enter apic#DOMAIN\\\\USERNAME') + username, password = get_credentials() + + if args.version: + tversion = AciVersion(args.version) + try: cversion = get_current_version() - tversion = get_target_version() + if not tversion: + tversion = get_target_version() vpc_nodes = get_vpc_nodes() sw_cversion = get_switch_version() except Exception as e: @@ -5200,9 +5211,9 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): json_log = {"name": "PreupgradeCheck", "method": "standalone script", "datetime": ts + tz, "script_version": str(SCRIPT_VERSION), "check_details": [], 'cversion': str(cversion), 'tversion': str(tversion), 'sw_cversion': str(sw_cversion)} - checks = [ + api_checks = [ # General Checks - apic_version_md5_check, + # apic_version_md5_check, # Connection target_version_compatibility_check, gen1_switch_compatibility_check, r_leaf_compatibility_check, @@ -5221,8 +5232,8 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): # Faults apic_disk_space_faults_check, switch_bootflash_usage_check, - standby_apic_disk_space_check, - apic_ssd_check, + # standby_apic_disk_space_check, # Connection + # apic_ssd_check, # Connection switch_ssd_check, port_configured_for_apic_check, port_configured_as_l2_check, @@ -5290,9 +5301,25 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): n9408_model_check, pbr_high_scale_check, standby_sup_sync_check, + # observer_db_size_check, # Connection + + ] + conn_checks = [ + # General + apic_version_md5_check, + + # Faults + standby_apic_disk_space_check, + apic_ssd_check, + + # Bugs observer_db_size_check, ] + checks = conn_checks + api_checks + if args.scriptcontainer: + # No connections allowed within scriptcontainer + checks = api_checks summary = {PASS: 0, FAIL_O: 0, FAIL_UF: 0, ERROR: 0, MANUAL: 0, POST: 0, NA: 0, 'TOTAL': len(checks)} for idx, check in enumerate(checks): try: From 003e6067e10a4e0c47e26ea9e35e6ebf85845ac8 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 26 Jun 2025 11:24:12 -0400 Subject: [PATCH 06/32] cleanup global --- aci-preupgrade-validation-script.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index a42e7703..c64ad8c2 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -1139,11 +1139,7 @@ def _icurl(apitype, query, page=0, page_size=100000): pre = '&' if '?' in query else '?' query += '{}page={}&page-size={}'.format(pre, page, page_size) uri = 'http://127.0.0.1:7777/api/{}/{}'.format(apitype, query) - if TOKEN: - cookie = "APIC-cookie={}".format(TOKEN) - cmd = ['curl', '-b', cookie, '-gs', uri] - else: - cmd = ['icurl', '-gs', uri] + cmd = ['icurl', '-gs', uri] logging.info('cmd = ' + ' '.join(cmd)) response = subprocess.check_output(cmd) logging.debug('response: ' + str(response)) From e65ffbecd75736b5615aefd59d10ae947ee429c3 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 26 Jun 2025 11:32:08 -0400 Subject: [PATCH 07/32] inspect cleanup --- aci-preupgrade-validation-script.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index c64ad8c2..6d93e49b 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -4685,7 +4685,6 @@ def fc_ex_model_check(index, total_checks, tversion, **kwargs): data.append([node_dn, model]) if data: result = FAIL_O - # func=inspect.currentframe().f_code.co_name print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) return result From 46e1996d53c605df7db69e3ed95b3cd195e68d22 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 26 Jun 2025 11:53:20 -0400 Subject: [PATCH 08/32] NA PASS logic in synth class --- aci-preupgrade-validation-script.py | 30 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 6d93e49b..b5921c18 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -87,15 +87,18 @@ def initFailureDetails(self): failure_details["row"] = [] return failure_details - def updateFailureDetails(self, result, recommended_action, reason, header, footer, column, row): - self.recommended_action = recommended_action + def updateWithResults(self, result, recommended_action, reason, header, footer, column, row): self.reason = reason - self.passed = False - self.failureDetails["fail_type"] = result - self.failureDetails["header"] = header - self.failureDetails["footer"] = footer - self.failureDetails["column"] = column - self.failureDetails["row"] = row + if result in [NA, PASS]: + self.showValidation = False + else: + self.passed = False + self.recommended_action = recommended_action + self.failureDetails["fail_type"] = result + self.failureDetails["header"] = header + self.failureDetails["footer"] = footer + self.failureDetails["column"] = column + self.failureDetails["row"] = row def buildResult(self): result = { @@ -1095,12 +1098,11 @@ def print_result(title, result, msg='', adjust_title=False, func="test"): synth = syntheticMaintPValidate(func, title, "") - if result in [FAIL_O, FAIL_UF, ERROR, MANUAL, POST]: - # TODO: deal with unformatted data and headers - synth.updateFailureDetails( - result=result, recommended_action=recommended_action, reason=msg, header="", footer=doc_url, column=headers, row=data - ) - synth.writeResult() + # TODO: deal with unformatted data and headers + synth.updateWithResults( + result=result, recommended_action=recommended_action, reason=msg, header="", footer=doc_url, column=headers, row=data + ) + synth.writeResult() padding = 120 - len(title) - len(msg) if adjust_title: padding += len(title) + 18 output = '{}{:>{}}'.format(msg, result, padding) From 8be4138e1127382d4476b62beda39261c676ec8d Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 26 Jun 2025 14:01:49 -0400 Subject: [PATCH 09/32] update args --- aci-preupgrade-validation-script.py | 34 +++++++++++++---------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index b5921c18..f01ac3df 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -22,7 +22,7 @@ from getpass import getpass from collections import defaultdict from datetime import datetime -import argparse +from argparse import ArgumentParser import warnings import time import pexpect @@ -5171,27 +5171,24 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): return result +def args(): + parser = ArgumentParser(description="ACI Pre-Upgrade Validation Script - %s" % SCRIPT_VERSION) + parser.add_argument("-t", "--tversion", action="store", type=str, help="Upgrade Target Version. Ex. 6.2(1a)") + parser.add_argument("-a", "--api-only", action="store_true", help="Run checks that are using only API. Checks using SSH are skipped.") + return parser.parse_args() + + if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("-v", "--version", help="CCO Version string, e.g. 6.0(2a)", type=str, default=None) - parser.add_argument("-s", "--scriptcontainer", help="Running in APIC script container, icurl checks only", action="store_true") - args = parser.parse_args() - tversion = None - if args.scriptcontainer: - username = password = None - else: - prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) - prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') + args = args() + api_only = args.a + prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) + prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') + if not api_only: prints('To use a non-default Login Domain, enter apic#DOMAIN\\\\USERNAME') username, password = get_credentials() - - if args.version: - tversion = AciVersion(args.version) - try: cversion = get_current_version() - if not tversion: - tversion = get_target_version() + tversion = AciVersion(args.t) if args.t else get_target_version() vpc_nodes = get_vpc_nodes() sw_cversion = get_switch_version() except Exception as e: @@ -5314,8 +5311,7 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): ] checks = conn_checks + api_checks - if args.scriptcontainer: - # No connections allowed within scriptcontainer + if api_only: checks = api_checks summary = {PASS: 0, FAIL_O: 0, FAIL_UF: 0, ERROR: 0, MANUAL: 0, POST: 0, NA: 0, 'TOTAL': len(checks)} for idx, check in enumerate(checks): From 44e871368be6088c7c5b8b21c2e29122c3647135 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 26 Jun 2025 14:34:52 -0400 Subject: [PATCH 10/32] tk review cleanup --- aci-preupgrade-validation-script.py | 53 +++++++++++++++++------------ 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index f01ac3df..921896c5 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -66,32 +66,34 @@ class syntheticMaintPValidate: - def __init__(self, func, name, description): - self.function_name = func + def __init__(self, name, description): self.name = name self.description = description self.reason = "" - self.criticality = "critical" + self.criticality = "informational" self.passed = True self.recommended_action = "" self.sub_reason = "" self.showValidation = True - self.failureDetails = self.initFailureDetails() - - def initFailureDetails(self): - failure_details = {} - failure_details["fail_type"] = "" - failure_details["header"] = "" - failure_details["footer"] = "" - failure_details["column"] = [] - failure_details["row"] = [] - return failure_details - - def updateWithResults(self, result, recommended_action, reason, header, footer, column, row): + self.failureDetails = {} + + def updateWithResults(self, result, recommended_action, reason, header, footer, column, row, unformatted_column, unformatted_rows): self.reason = reason - if result in [NA, PASS]: + + # Show validation + if result in [NA, POST]: self.showValidation = False - else: + + # Criticality + if result in [FAIL_O, FAIL_UF]: + self.criticality = "critical" + elif result in [ERROR]: + self.criticality = "major" + elif result in [MANUAL]: + self.criticality = "warning" + + # FailureDetails + if result != PASS: self.passed = False self.recommended_action = recommended_action self.failureDetails["fail_type"] = result @@ -99,6 +101,8 @@ def updateWithResults(self, result, recommended_action, reason, header, footer, self.failureDetails["footer"] = footer self.failureDetails["column"] = column self.failureDetails["row"] = row + self.failureDetails["unformatted_column"] = unformatted_column + self.failureDetails["unformatted_rows"] = unformatted_rows def buildResult(self): result = { @@ -1095,12 +1099,19 @@ def print_result(title, result, msg='', unformatted_headers=None, unformatted_data=None, recommended_action='', doc_url='', - adjust_title=False, - func="test"): - synth = syntheticMaintPValidate(func, title, "") + adjust_title=False): + synth = syntheticMaintPValidate(title, "") # TODO: deal with unformatted data and headers synth.updateWithResults( - result=result, recommended_action=recommended_action, reason=msg, header="", footer=doc_url, column=headers, row=data + result=result, + recommended_action=recommended_action, + reason=msg, + header="", + footer=doc_url, + column=headers, + row=data, + unformatted_column=unformatted_headers, + unformatted_rows=unformatted_data, ) synth.writeResult() padding = 120 - len(title) - len(msg) From bca729883639da9eebfd2af547ff5e4169df09f0 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 26 Jun 2025 14:50:29 -0400 Subject: [PATCH 11/32] cleanup --- aci-preupgrade-validation-script.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 921896c5..01222a3e 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -22,7 +22,7 @@ from getpass import getpass from collections import defaultdict from datetime import datetime -from argparse import ArgumentParser +from argparse import ArgumentParser import warnings import time import pexpect @@ -1101,7 +1101,6 @@ def print_result(title, result, msg='', doc_url='', adjust_title=False): synth = syntheticMaintPValidate(title, "") - # TODO: deal with unformatted data and headers synth.updateWithResults( result=result, recommended_action=recommended_action, @@ -5218,7 +5217,6 @@ def args(): 'cversion': str(cversion), 'tversion': str(tversion), 'sw_cversion': str(sw_cversion)} api_checks = [ # General Checks - # apic_version_md5_check, # Connection target_version_compatibility_check, gen1_switch_compatibility_check, r_leaf_compatibility_check, @@ -5237,8 +5235,6 @@ def args(): # Faults apic_disk_space_faults_check, switch_bootflash_usage_check, - # standby_apic_disk_space_check, # Connection - # apic_ssd_check, # Connection switch_ssd_check, port_configured_for_apic_check, port_configured_as_l2_check, @@ -5306,7 +5302,6 @@ def args(): n9408_model_check, pbr_high_scale_check, standby_sup_sync_check, - # observer_db_size_check, # Connection ] conn_checks = [ From 41f9d72762a5f51d15fc220fab9d176debb83ea6 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 27 Jun 2025 09:56:13 -0400 Subject: [PATCH 12/32] synth pytest --- aci-preupgrade-validation-script.py | 2 +- tests/test_synthenticMaintPValidate.py | 152 +++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 tests/test_synthenticMaintPValidate.py diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 01222a3e..d9fe9987 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -93,7 +93,7 @@ def updateWithResults(self, result, recommended_action, reason, header, footer, self.criticality = "warning" # FailureDetails - if result != PASS: + if result not in [NA, PASS]: self.passed = False self.recommended_action = recommended_action self.failureDetails["fail_type"] = result diff --git a/tests/test_synthenticMaintPValidate.py b/tests/test_synthenticMaintPValidate.py new file mode 100644 index 00000000..4d535d4e --- /dev/null +++ b/tests/test_synthenticMaintPValidate.py @@ -0,0 +1,152 @@ +import pytest +import importlib + +script = importlib.import_module("aci-preupgrade-validation-script") + + +@pytest.mark.parametrize( + "name, description, result, recommended_action, reason, header, footer, column, row, unformatted_column, unformatted_rows, expected_show, expected_criticality, expected_passed", + [ + # Check 1: NA + ( + "NA", + "", + script.NA, + "", + "", + "", + "", + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + False, + "informational", + True + ), + # Check 2: PASS + ( + "PASS", + "", + script.PASS, + "", + "", + "", + "", + [], + [], + [], + [], + True, + "informational", + True + ), + # Check 3: POST + ( + "POST", + "", + script.POST, + "reboot", + "test reason", + "test header", + "test footer", + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + False, + "informational", + False + ), + # Check 4: MANUAL + ( + "MANUAL", + "", + script.MANUAL, + "reboot", + "test reason", + "test header", + "test footer", + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + True, + "warning", + False + ), + # Check 5: ERROR + ( + "ERROR", + "", + script.ERROR, + "reboot", + "test reason", + "test header", + "test footer", + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + True, + "major", + False + ), + # Check 6: FAIL_UF + ( + "FAIL_UF", + "", + script.FAIL_UF, + "reboot", + "test reason", + "test header", + "test footer", + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + True, + "critical", + False + ), + # Check 7: FAIL_O + ( + "FAIL_O", + "", + script.FAIL_O, + "reboot", + "test reason", + "test header", + "test footer", + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + True, + "critical", + False + ), + ], +) +def test_syntheticMaintPValidate( + name, + description, + result, + recommended_action, + reason, + header, + footer, + column, + row, + unformatted_column, + unformatted_rows, + expected_show, + expected_criticality, + expected_passed, +): + synth = script.syntheticMaintPValidate(name, description) + synth.updateWithResults(result, recommended_action, reason, header, footer, column, row, unformatted_column, unformatted_rows) + result = synth.buildResult() + assert result["syntheticMaintPValidate"]["attributes"]["showValidation"] == expected_show + assert result["syntheticMaintPValidate"]["attributes"]["criticality"] == expected_criticality + assert result["syntheticMaintPValidate"]["attributes"]["passed"] == expected_passed From 581825bd7a5aacadbfee2ce6d2d17ce1b34ee5f8 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 27 Jun 2025 14:50:48 -0400 Subject: [PATCH 13/32] synth pytest --- aci-preupgrade-validation-script.py | 13 +++++++------ tests/test_synthenticMaintPValidate.py | 11 +++++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index d9fe9987..4523b504 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -76,6 +76,9 @@ def __init__(self, name, description): self.sub_reason = "" self.showValidation = True self.failureDetails = {} + cleaned_name = re.sub(r'[^a-zA-Z0-9_]+|\s+', '_', self.name) + self.filename = cleaned_name + '.json' + self.path = "cx-preupgrade-validation-results" def updateWithResults(self, result, recommended_action, reason, header, footer, column, row, unformatted_column, unformatted_rows): self.reason = reason @@ -123,13 +126,11 @@ def buildResult(self): return result def writeResult(self): - cleaned_name = re.sub(r'[^a-zA-Z0-9_]+|\s+', '_', self.name) - filename = cleaned_name + '.json' - path = "cx-preupgrade-validation-results" - if not os.path.isdir(path): - os.mkdir(path) - with open(os.path.join(path, filename), "w") as f: + if not os.path.isdir(self.path): + os.mkdir(self.path) + with open(os.path.join(self.path, self.filename), "w") as f: json.dump(self.buildResult(), f, indent=4) + return "{}/{}".format(self.path, self.filename) class OldVerClassNotFound(Exception): diff --git a/tests/test_synthenticMaintPValidate.py b/tests/test_synthenticMaintPValidate.py index 4d535d4e..ebec9bc6 100644 --- a/tests/test_synthenticMaintPValidate.py +++ b/tests/test_synthenticMaintPValidate.py @@ -1,5 +1,6 @@ import pytest import importlib +import json script = importlib.import_module("aci-preupgrade-validation-script") @@ -146,7 +147,9 @@ def test_syntheticMaintPValidate( ): synth = script.syntheticMaintPValidate(name, description) synth.updateWithResults(result, recommended_action, reason, header, footer, column, row, unformatted_column, unformatted_rows) - result = synth.buildResult() - assert result["syntheticMaintPValidate"]["attributes"]["showValidation"] == expected_show - assert result["syntheticMaintPValidate"]["attributes"]["criticality"] == expected_criticality - assert result["syntheticMaintPValidate"]["attributes"]["passed"] == expected_passed + file = synth.writeResult() + with open(file, "r") as f: + data = json.load(f) + assert data["syntheticMaintPValidate"]["attributes"]["showValidation"] == expected_show + assert data["syntheticMaintPValidate"]["attributes"]["criticality"] == expected_criticality + assert data["syntheticMaintPValidate"]["attributes"]["passed"] == expected_passed From d5317433391c10ab9daab596bd4a687a68c185c8 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 27 Jun 2025 15:48:05 -0400 Subject: [PATCH 14/32] fix args --- aci-preupgrade-validation-script.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 4523b504..e37c80d7 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -5191,7 +5191,7 @@ def args(): if __name__ == "__main__": args = args() - api_only = args.a + api_only = args.api_only prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') if not api_only: @@ -5199,7 +5199,7 @@ def args(): username, password = get_credentials() try: cversion = get_current_version() - tversion = AciVersion(args.t) if args.t else get_target_version() + tversion = AciVersion(args.tversion) if args.tversion else get_target_version() vpc_nodes = get_vpc_nodes() sw_cversion = get_switch_version() except Exception as e: From dac2901ac13d3380e59664c6988f6b542c0b94e2 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 27 Jun 2025 15:54:46 -0400 Subject: [PATCH 15/32] instantiate uname pw for api-only --- aci-preupgrade-validation-script.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index e37c80d7..281383bb 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -5194,7 +5194,9 @@ def args(): api_only = args.api_only prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') - if not api_only: + if api_only: + username = password = None + else: prints('To use a non-default Login Domain, enter apic#DOMAIN\\\\USERNAME') username, password = get_credentials() try: From 764c6b294cb74d89f51b1b015452c50fdd5d7a56 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 27 Jun 2025 16:17:33 -0400 Subject: [PATCH 16/32] --puv flag and live result cleanup logic --- aci-preupgrade-validation-script.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 281383bb..bdb54812 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -23,6 +23,7 @@ from collections import defaultdict from datetime import datetime from argparse import ArgumentParser +import shutil import warnings import time import pexpect @@ -54,6 +55,7 @@ tz = time.strftime('%z') ts = datetime.now().strftime('%Y-%m-%dT%H-%M-%S') +LIVE_RESULTS_DIR = "cx-preupgrade-validation-results" DIR = 'preupgrade_validator_logs/' BUNDLE_NAME = 'preupgrade_validator_%s%s.tgz' % (ts, tz) RESULT_FILE = DIR + 'preupgrade_validator_%s%s.txt' % (ts, tz) @@ -66,7 +68,7 @@ class syntheticMaintPValidate: - def __init__(self, name, description): + def __init__(self, name, description, path=LIVE_RESULTS_DIR): self.name = name self.description = description self.reason = "" @@ -78,7 +80,7 @@ def __init__(self, name, description): self.failureDetails = {} cleaned_name = re.sub(r'[^a-zA-Z0-9_]+|\s+', '_', self.name) self.filename = cleaned_name + '.json' - self.path = "cx-preupgrade-validation-results" + self.path = path def updateWithResults(self, result, recommended_action, reason, header, footer, column, row, unformatted_column, unformatted_rows): self.reason = reason @@ -5185,17 +5187,19 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): def args(): parser = ArgumentParser(description="ACI Pre-Upgrade Validation Script - %s" % SCRIPT_VERSION) parser.add_argument("-t", "--tversion", action="store", type=str, help="Upgrade Target Version. Ex. 6.2(1a)") - parser.add_argument("-a", "--api-only", action="store_true", help="Run checks that are using only API. Checks using SSH are skipped.") + parser.add_argument("--puv", action="store_true", help="For built-in PUV. API Checks only. Checks using SSH are skipped.") return parser.parse_args() if __name__ == "__main__": args = args() - api_only = args.api_only + is_puv = args.puv prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') - if api_only: + if is_puv: username = password = None + if os.path.exists(LIVE_RESULTS_DIR) and os.path.isdir(LIVE_RESULTS_DIR): + shutil.rmtree(LIVE_RESULTS_DIR) else: prints('To use a non-default Login Domain, enter apic#DOMAIN\\\\USERNAME') username, password = get_credentials() @@ -5320,7 +5324,7 @@ def args(): ] checks = conn_checks + api_checks - if api_only: + if is_puv: checks = api_checks summary = {PASS: 0, FAIL_O: 0, FAIL_UF: 0, ERROR: 0, MANUAL: 0, POST: 0, NA: 0, 'TOTAL': len(checks)} for idx, check in enumerate(checks): @@ -5364,4 +5368,6 @@ def args(): """.format(bundle=bundle_loc)) prints('==== Script Version %s FIN ====' % (SCRIPT_VERSION)) + if not is_puv and os.path.exists(LIVE_RESULTS_DIR) and os.path.isdir(LIVE_RESULTS_DIR): + shutil.rmtree(LIVE_RESULTS_DIR) subprocess.check_output(['rm', '-rf', DIR]) From 8ecbb36e307d28ca078c96f660dd504feb8f1a1a Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 27 Jun 2025 16:24:07 -0400 Subject: [PATCH 17/32] fix exeception writing --- aci-preupgrade-validation-script.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index bdb54812..f8e88b1e 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2073,7 +2073,7 @@ def apic_ssd_check(index, total_checks, cversion, **kwargs): c.log = LOG_FILE c.connect() except Exception as e: - data.append([attr['id'], attr['name'], '-', '-', '-', e]) + data.append([attr['id'], attr['name'], '-', '-', '-', str(e)]) print_result(node_title, ERROR) has_error = True continue @@ -2081,7 +2081,7 @@ def apic_ssd_check(index, total_checks, cversion, **kwargs): c.cmd( 'grep -oE "SSD Wearout Indicator is [0-9]+" /var/log/dme/log/svc_ifc_ae.bin.log | tail -1') except Exception as e: - data.append([attr['id'], attr['name'], '-', '-', '-', e]) + data.append([attr['id'], attr['name'], '-', '-', '-', str(e)]) print_result(node_title, ERROR) has_error = True continue @@ -2727,7 +2727,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** c.log = LOG_FILE c.connect() except Exception as e: - data.append([apic_name, '-', '-', json.dumps(e.__dict__), '-']) + data.append([apic_name, '-', '-', str(e), '-']) print_result(node_title, ERROR) has_error = True continue @@ -2737,7 +2737,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** tversion.dot_version) except Exception as e: data.append([apic_name, '-', '-', - 'ls command via ssh failed due to:{}'.format(json.dumps(e.__dict__)), '-']) + 'ls command via ssh failed due to:{}'.format(str(e)), '-']) print_result(node_title, ERROR) has_error = True continue @@ -2751,7 +2751,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** tversion.dot_version) except Exception as e: data.append([apic_name, str(tversion), '-', - 'failed to check md5sum via ssh due to:{}'.format(json.dumps(e.__dict__)), '-']) + 'failed to check md5sum via ssh due to:{}'.format(str(e)), '-']) print_result(node_title, ERROR) has_error = True continue @@ -2810,14 +2810,14 @@ def standby_apic_disk_space_check(index, total_checks, **kwargs): c.log = LOG_FILE c.connect() except Exception as e: - data.append([stb['mbSn'], stb['oobIpAddr'], '-', '-', e]) + data.append([stb['mbSn'], stb['oobIpAddr'], '-', '-', str(e)]) has_error = True continue try: c.cmd("df -h") except Exception as e: - data.append([stb['mbSn'], stb['oobIpAddr'], '-', '-', e]) + data.append([stb['mbSn'], stb['oobIpAddr'], '-', '-', str(e)]) has_error = True continue @@ -5121,7 +5121,7 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): c.log = LOG_FILE c.connect() except Exception as e: - data.append([attr['id'], attr['name'], json.dumps(e.__dict__)]) + data.append([attr['id'], attr['name'], str(e)]) print_result(node_title, ERROR) has_error = True continue @@ -5143,7 +5143,7 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): data.append([attr['id'], file_name, file_size]) print_result(node_title, DONE) except Exception as e: - data.append([attr['id'], attr['name'], json.dumps(e.__dict__)]) + data.append([attr['id'], attr['name'], str(e)]) print_result(node_title, ERROR) has_error = True continue From 77e6e27a36840d7f9579fbe70662a9d55be6a3e3 Mon Sep 17 00:00:00 2001 From: takishida <38262981+takishida@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:41:54 -0700 Subject: [PATCH 18/32] Puv pytest (#258) * Clean up get_vpc_nodes() and its pytest * Split the main script execution code into each func and add pytest * Retire json_log and adopt json per rule for both PUV and regular use + linting --- aci-preupgrade-validation-script.py | 323 ++++++++++++++++------------ tests/test_get_vpc_node.py | 33 ++- tests/test_parse_args.py | 54 +++++ tests/test_prepare.py | 182 ++++++++++++++++ tests/test_run_checks.py | 75 +++++++ 5 files changed, 528 insertions(+), 139 deletions(-) create mode 100644 tests/test_parse_args.py create mode 100644 tests/test_prepare.py create mode 100644 tests/test_run_checks.py diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index f8e88b1e..1d2df205 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -55,20 +55,24 @@ tz = time.strftime('%z') ts = datetime.now().strftime('%Y-%m-%dT%H-%M-%S') -LIVE_RESULTS_DIR = "cx-preupgrade-validation-results" -DIR = 'preupgrade_validator_logs/' BUNDLE_NAME = 'preupgrade_validator_%s%s.tgz' % (ts, tz) +DIR = 'preupgrade_validator_logs/' +JSON_DIR = DIR + 'json_results/' +META_FILE = DIR + 'meta.json' RESULT_FILE = DIR + 'preupgrade_validator_%s%s.txt' % (ts, tz) -JSON_FILE = DIR + 'preupgrade_validator_%s%s.json' % (ts, tz) +SUMMARY_FILE = DIR + 'summary.json' LOG_FILE = DIR + 'preupgrade_validator_debug.log' fmt = '[%(asctime)s.%(msecs)03d{} %(levelname)-8s %(funcName)20s:%(lineno)-4d] %(message)s'.format(tz) -subprocess.check_output(['mkdir', '-p', DIR]) +if os.path.isdir(DIR): + shutil.rmtree(DIR) +os.mkdir(DIR) +os.mkdir(JSON_DIR) logging.basicConfig(level=logging.DEBUG, filename=LOG_FILE, format=fmt, datefmt='%Y-%m-%d %H:%M:%S') warnings.simplefilter(action='ignore', category=FutureWarning) class syntheticMaintPValidate: - def __init__(self, name, description, path=LIVE_RESULTS_DIR): + def __init__(self, name, description, path=JSON_DIR): self.name = name self.description = description self.reason = "" @@ -131,7 +135,7 @@ def writeResult(self): if not os.path.isdir(self.path): os.mkdir(self.path) with open(os.path.join(self.path, self.filename), "w") as f: - json.dump(self.buildResult(), f, indent=4) + json.dump(self.buildResult(), f, indent=2) return "{}/{}".format(self.path, self.filename) @@ -515,7 +519,7 @@ def __init__(self, version): self.patch = v.group('patch') if v else None self.regex = v if not v: - raise RuntimeError("Parsing failure of ACI version `%s`", version) + raise RuntimeError("Parsing failure of ACI version `%s`" % version) def __str__(self): return self.version @@ -1102,20 +1106,22 @@ def print_result(title, result, msg='', unformatted_headers=None, unformatted_data=None, recommended_action='', doc_url='', - adjust_title=False): - synth = syntheticMaintPValidate(title, "") - synth.updateWithResults( - result=result, - recommended_action=recommended_action, - reason=msg, - header="", - footer=doc_url, - column=headers, - row=data, - unformatted_column=unformatted_headers, - unformatted_rows=unformatted_data, - ) - synth.writeResult() + adjust_title=False, + json_output=True): + if json_output: + synth = syntheticMaintPValidate(title, "") + synth.updateWithResults( + result=result, + recommended_action=recommended_action, + reason=msg, + header="", + footer=doc_url, + column=headers, + row=data, + unformatted_column=unformatted_headers, + unformatted_rows=unformatted_data, + ) + synth.writeResult() padding = 120 - len(title) - len(msg) if adjust_title: padding += len(title) + 18 output = '{}{:>{}}'.format(msg, result, padding) @@ -1138,11 +1144,11 @@ def print_result(title, result, msg='', def _icurl_error_handler(imdata): if imdata and "error" in imdata[0]: if "not found in class" in imdata[0]['error']['attributes']['text']: - raise OldVerPropNotFound('cversion does not have requested property') + raise OldVerPropNotFound('Your current ACI version does not have requested property') elif "unresolved class for" in imdata[0]['error']['attributes']['text']: - raise OldVerClassNotFound('cversion does not have requested class') + raise OldVerClassNotFound('Your current ACI version does not have requested class') elif "not found" in imdata[0]['error']['attributes']['text']: - raise OldVerClassNotFound('cversion does not have requested class') + raise OldVerClassNotFound('Your current ACI version does not have requested class') else: raise Exception('API call failed! Check debug log') @@ -1178,6 +1184,7 @@ def icurl(apitype, query, page_size=100000): def get_credentials(): + prints('To use a non-default Login Domain, enter apic#DOMAIN\\\\USERNAME') while True: usr = input('Enter username for APIC login : ') if usr: break @@ -1235,23 +1242,25 @@ def get_target_version(): return None -def get_vpc_nodes(**kwargs): +def get_vpc_nodes(): """ Returns list of VPC Node IDs; ['101', '102', etc...] """ - prints("Collecting VPC Node IDs...\n") + prints("Collecting VPC Node IDs...", end='') vpc_nodes = [] - - prot_pols = kwargs.get("fabricNodePEp.json", None) - if not prot_pols: - prot_pols = icurl('class', 'fabricNodePEp.json') - - if prot_pols: - for vpc_node in prot_pols: - vpc_nodes.append(vpc_node['fabricNodePEp']['attributes']['id']) - + prot_pols = icurl('class', 'fabricNodePEp.json') + for vpc_node in prot_pols: + vpc_nodes.append(vpc_node['fabricNodePEp']['attributes']['id']) + vpc_nodes.sort() + # Display up to 4 node IDs + max_display = 4 + if len(vpc_nodes) <= max_display: + prints('%s\n' % ", ".join(vpc_nodes)) + else: + omitted_count = len(vpc_nodes) - max_display + prints('%s, ... (and %d more)\n' % (", ".join(vpc_nodes[:max_display]), omitted_count)) return vpc_nodes -def get_switch_version(**kwargs): +def get_switch_version(): """ Returns lowest switch version as AciVersion instance """ prints("Gathering Lowest Switch Version from Firmware Repository...", end='') firmwares = icurl('class', 'firmwareRunning.json') @@ -2032,8 +2041,9 @@ def switch_ssd_check(index, total_checks, **kwargs): print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) return result + # Connection based check -def apic_ssd_check(index, total_checks, cversion, **kwargs): +def apic_ssd_check(index, total_checks, cversion, username, password, **kwargs): title = 'APIC SSD Health' result = FAIL_UF msg = '' @@ -2074,7 +2084,7 @@ def apic_ssd_check(index, total_checks, cversion, **kwargs): c.connect() except Exception as e: data.append([attr['id'], attr['name'], '-', '-', '-', str(e)]) - print_result(node_title, ERROR) + print_result(node_title, ERROR, json_output=False) has_error = True continue try: @@ -2082,7 +2092,7 @@ def apic_ssd_check(index, total_checks, cversion, **kwargs): 'grep -oE "SSD Wearout Indicator is [0-9]+" /var/log/dme/log/svc_ifc_ae.bin.log | tail -1') except Exception as e: data.append([attr['id'], attr['name'], '-', '-', '-', str(e)]) - print_result(node_title, ERROR) + print_result(node_title, ERROR, json_output=False) has_error = True continue @@ -2094,12 +2104,12 @@ def apic_ssd_check(index, total_checks, cversion, **kwargs): wearout, recommended_action]) report_other = True - print_result(node_title, DONE) + print_result(node_title, DONE, json_output=False) continue if report_other: data.append([pod_id, node_id, "Solid State Disk", wearout, "No Action Required"]) - print_result(node_title, DONE) + print_result(node_title, DONE, json_output=False) else: headers = ["Fault", "Pod", "Node", "Storage Unit", "% lifetime remaining", "Recommended Action"] unformatted_headers = ["Fault", "Fault DN", "% lifetime remaining", "Recommended Action"] @@ -2680,6 +2690,7 @@ def lldp_with_infra_vlan_mismatch_check(index, total_checks, **kwargs): print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) return result + # Connection based check def apic_version_md5_check(index, total_checks, tversion, username, password, **kwargs): title = 'APIC Target version image and MD5 hash' @@ -2728,7 +2739,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** c.connect() except Exception as e: data.append([apic_name, '-', '-', str(e), '-']) - print_result(node_title, ERROR) + print_result(node_title, ERROR, json_output=False) has_error = True continue @@ -2738,12 +2749,12 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** except Exception as e: data.append([apic_name, '-', '-', 'ls command via ssh failed due to:{}'.format(str(e)), '-']) - print_result(node_title, ERROR) + print_result(node_title, ERROR, json_output=False) has_error = True continue if "No such file or directory" in c.output: data.append([apic_name, str(tversion), '-', 'image not found', recommended_action]) - print_result(node_title, FAIL_UF) + print_result(node_title, FAIL_UF, json_output=False) continue try: @@ -2752,12 +2763,12 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** except Exception as e: data.append([apic_name, str(tversion), '-', 'failed to check md5sum via ssh due to:{}'.format(str(e)), '-']) - print_result(node_title, ERROR) + print_result(node_title, ERROR, json_output=False) has_error = True continue if "No such file or directory" in c.output: data.append([apic_name, str(tversion), '-', 'md5sum file not found', recommended_action]) - print_result(node_title, FAIL_UF) + print_result(node_title, FAIL_UF, json_output=False) continue for line in c.output.split("\n"): words = line.split() @@ -2770,11 +2781,11 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** break else: data.append([apic_name, str(tversion), '-', 'unexpected output when checking md5sum file', recommended_action]) - print_result(node_title, ERROR) + print_result(node_title, ERROR, json_output=False) has_error = True continue - print_result(node_title, DONE) + print_result(node_title, DONE, json_output=False) if len(set(md5s)) > 1: for name, md5 in zip(md5_names, md5s): data.append([name, str(tversion), md5, 'md5sum do not match on all APICs', recommended_action]) @@ -2785,6 +2796,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** print_result(title, result, msg, headers, data, adjust_title=True) return result + # Connection Based Check def standby_apic_disk_space_check(index, total_checks, **kwargs): title = 'Standby APIC Disk Space Usage' @@ -3974,12 +3986,12 @@ def fabric_port_down_check(index, total_checks, **kwargs): recommended_action = 'Identify if these ports are needed for redundancy and reason for being down' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#fabric-port-is-down' print_title(title, index, total_checks) - - fault_api = 'faultInst.json' + + fault_api = 'faultInst.json' fault_api += '?&query-target-filter=and(eq(faultInst.code,"F1394")' fault_api += ',eq(faultInst.rule,"ethpm-if-port-down-fabric"))' - faultInsts = icurl('class',fault_api) + faultInsts = icurl('class', fault_api) dn_re = node_regex + r'/.+/phys-\[(?Peth\d/\d+)\]' for faultInst in faultInsts: @@ -4013,16 +4025,18 @@ def fabric_dpp_check(index, total_checks, tversion, **kwargs): if not tversion: print_result(title, MANUAL, "Target version not supplied. Skipping.") return MANUAL - - lbpol_api = 'lbpPol.json' + + lbpol_api = 'lbpPol.json' lbpol_api += '?query-target-filter=eq(lbpPol.pri,"on")' lbpPol = icurl('class', lbpol_api) if lbpPol: - if ((tversion.newer_than("5.1(1h)") and tversion.older_than("5.2(8e)")) or - (tversion.major1 == "6" and tversion.older_than("6.0(3d)"))): - result = FAIL_O - data.append(["CSCwf05073", "Target Version susceptible to Defect"]) + if ( + (tversion.newer_than("5.1(1h)") and tversion.older_than("5.2(8e)")) or + (tversion.major1 == "6" and tversion.older_than("6.0(3d)")) + ): + result = FAIL_O + data.append(["CSCwf05073", "Target Version susceptible to Defect"]) print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) return result @@ -4075,7 +4089,7 @@ def subnet_scope_check(index, total_checks, cversion, **kwargs): print_title(title, index, total_checks) if cversion.older_than("4.2(6d)") or (cversion.major1 == "5" and cversion.older_than("5.1(1h)")): - epg_api = 'fvAEPg.json?' + epg_api = 'fvAEPg.json?' epg_api += 'rsp-subtree=children&rsp-subtree-class=fvSubnet&rsp-subtree-include=required' fvAEPg = icurl('class', epg_api) @@ -4083,7 +4097,7 @@ def subnet_scope_check(index, total_checks, cversion, **kwargs): print_result(title, NA, "0 EPG Subnets found. Skipping.") return NA - bd_api = 'fvBD.json' + bd_api = 'fvBD.json' bd_api += '?rsp-subtree=children&rsp-subtree-class=fvSubnet&rsp-subtree-include=required' fvBD = icurl('class', bd_api) @@ -4156,10 +4170,10 @@ def rtmap_comm_match_defect_check(index, total_checks, tversion, **kwargs): has_dest = True if has_comm and not has_dest: subj_dn_list.append(dn) - + # Now check if affected match statement is in use by any route-map if len(subj_dn_list) > 0: - rtctrlCtxPs = icurl('class','rtctrlCtxP.json?rsp-subtree=full&rsp-subtree-class=rtctrlRsCtxPToSubjP,rtctrlRsScopeToAttrP&rsp-subtree-include=required') + rtctrlCtxPs = icurl('class', 'rtctrlCtxP.json?rsp-subtree=full&rsp-subtree-class=rtctrlRsCtxPToSubjP,rtctrlRsScopeToAttrP&rsp-subtree-include=required') if rtctrlCtxPs: for rtctrlCtxP in rtctrlCtxPs: has_affected_subj = False @@ -4176,7 +4190,7 @@ def rtmap_comm_match_defect_check(index, total_checks, tversion, **kwargs): if has_affected_subj and has_set: dn = rtctrlCtxP['rtctrlCtxP']['attributes']['dn'] parent_dn = '/'.join(dn.rsplit('/', 1)[:-1]) - data.append([parent_dn,subj_dn,"Route-map has community match statement but no prefix list."]) + data.append([parent_dn, subj_dn, "Route-map has community match statement but no prefix list."]) if data: result = FAIL_O @@ -4197,8 +4211,8 @@ def fabricPathEp_target_check(index, total_checks, **kwargs): fabricPathEp_regex = r"topology/pod-\d+/(?:\w+)?paths-\d+(?:-\d+)?(?:/ext(?:\w+)?paths-(?P\d+)(?:-(?P\d+))?)?/pathep-\[(?P.+)\]" eth_regex = r'eth(?P\d+)/(?P\d+)(?:/(?P\d+))?' - hpath_api = 'infraRsHPathAtt.json' - oosPorts_api = 'fabricRsOosPath.json' + hpath_api = 'infraRsHPathAtt.json' + oosPorts_api = 'fabricRsOosPath.json' infraRsHPathAtt = icurl('class', hpath_api) fabricRsOosPath = icurl('class', oosPorts_api) @@ -4275,7 +4289,7 @@ def lldp_custom_int_description_defect_check(index, total_checks, tversion, **kw if tversion.major1 == '6' and tversion.older_than('6.0(3a)'): custom_int_count = icurl('class', 'infraPortBlk.json?query-target-filter=ne(infraPortBlk.descr,"")&rsp-subtree-include=count')[0]['moCount']['attributes']['count'] - lazy_vmm_count = icurl('class','fvRsDomAtt.json?query-target-filter=and(eq(fvRsDomAtt.tCl,"vmmDomP"),eq(fvRsDomAtt.resImedcy,"lazy"))&rsp-subtree-include=count')[0]['moCount']['attributes']['count'] + lazy_vmm_count = icurl('class', 'fvRsDomAtt.json?query-target-filter=and(eq(fvRsDomAtt.tCl,"vmmDomP"),eq(fvRsDomAtt.resImedcy,"lazy"))&rsp-subtree-include=count')[0]['moCount']['attributes']['count'] if int(custom_int_count) > 0 and int(lazy_vmm_count) > 0: result = FAIL_O @@ -4489,8 +4503,8 @@ def validate_32_64_bit_image_check(index, total_checks, cversion, tversion, **kw if cversion.newer_than("6.0(2a)") and tversion.newer_than("6.0(2a)"): result_32 = result_64 = "Not Found" target_sw_ver = 'n9000-1' + tversion.version - firmware_api = 'firmwareFirmware.json' - firmware_api += '?query-target-filter=eq(firmwareFirmware.fullVersion,"%s")' % (target_sw_ver) + firmware_api = 'firmwareFirmware.json' + firmware_api += '?query-target-filter=eq(firmwareFirmware.fullVersion,"%s")' % (target_sw_ver) firmwares = icurl('class', firmware_api) for firmware in firmwares: @@ -4608,7 +4622,7 @@ def cloudsec_encryption_depr_check(index, total_checks, tversion, **kwargs): doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#cloudsec-encryption-deprecated' print_title(title, index, total_checks) - cloudsec_api = 'cloudsecPreSharedKey.json' + cloudsec_api = 'cloudsecPreSharedKey.json' if not tversion: print_result(title, MANUAL, "Target version not supplied. Skipping.") @@ -4639,7 +4653,7 @@ def out_of_service_ports_check(index, total_checks, **kwargs): title = 'Out-of-Service Ports' result = PASS msg = '' - headers = ["Pod ID", "Node ID", "Port ID", "Operational State", "Usage" ] + headers = ["Pod ID", "Node ID", "Port ID", "Operational State", "Usage"] data = [] recommended_action = 'Remove Out-of-service Policy on identified "up" ports or they will remain "down" after switch Upgrade' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#out-of-service-ports' @@ -4659,7 +4673,7 @@ def out_of_service_ports_check(index, total_checks, **kwargs): data.append([node_data.group("pod"), node_data.group("node"), node_data.group("port"), oper_st, usage]) if data: - result = FAIL_O + result = FAIL_O print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) return result @@ -4678,26 +4692,26 @@ def fc_ex_model_check(index, total_checks, tversion, **kwargs): fcEntity_api = "fcEntity.json" fabricNode_api = 'fabricNode.json' fabricNode_api += '?query-target-filter=wcard(fabricNode.model,".*EX")' - + if not tversion: print_result(title, MANUAL, "Target version not supplied. Skipping.") return MANUAL - + if (tversion.newer_than("6.0(7a)") and tversion.older_than("6.0(9c)")) or tversion.same_as("6.1(1f)"): - fcEntitys = icurl('class', fcEntity_api) + fcEntitys = icurl('class', fcEntity_api) fc_nodes = [] if fcEntitys: for fcEntity in fcEntitys: fc_nodes.append(fcEntity['fcEntity']['attributes']['dn'].split('/sys')[0]) - + if fc_nodes: fabricNodes = icurl('class', fabricNode_api) for node in fabricNodes: - node_dn = node['fabricNode']['attributes']['dn'] - if node_dn in fc_nodes: - model = node['fabricNode']['attributes']['model'] - if model in ["N9K-C93180YC-EX", "N9K-C93108TC-EX", "N9K-C93108LC-EX"]: - data.append([node_dn, model]) + node_dn = node['fabricNode']['attributes']['dn'] + if node_dn in fc_nodes: + model = node['fabricNode']['attributes']['model'] + if model in ["N9K-C93180YC-EX", "N9K-C93108TC-EX", "N9K-C93108LC-EX"]: + data.append([node_dn, model]) if data: result = FAIL_O print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) @@ -4729,7 +4743,7 @@ def tep_to_tep_ac_counter_check(index, total_checks, **kwargs): if data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) return result @@ -4788,15 +4802,15 @@ def stale_decomissioned_spine_check(index, total_checks, tversion, **kwargs): doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#stale-decommissioned-spine' print_title(title, index, total_checks) - decomissioned_api ='fabricRsDecommissionNode.json' + decomissioned_api = 'fabricRsDecommissionNode.json' active_spine_api = 'topSystem.json' - active_spine_api += '?query-target-filter=eq(topSystem.role,"spine")' + active_spine_api += '?query-target-filter=eq(topSystem.role,"spine")' if not tversion: print_result(title, MANUAL, "Target version not supplied. Skipping.") return MANUAL - + if tversion.newer_than("5.2(3d)") and tversion.older_than("6.0(3d)"): decomissioned_switches = icurl('class', decomissioned_api) if decomissioned_switches: @@ -4827,11 +4841,11 @@ def n9408_model_check(index, total_checks, tversion, **kwargs): eqptCh_api = 'eqptCh.json' eqptCh_api += '?query-target-filter=eq(eqptCh.model,"N9K-C9400-SW-GX2A")' - + if not tversion: print_result(title, MANUAL, "Target version not supplied. Skipping.") return MANUAL - + if tversion.newer_than("6.1(3a)"): eqptCh = icurl('class', eqptCh_api) for node in eqptCh: @@ -4841,7 +4855,7 @@ def n9408_model_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) return result @@ -4859,7 +4873,7 @@ def pbr_high_scale_check(index, total_checks, tversion, **kwargs): vnsAdjacencyDefCont_api = 'vnsAdjacencyDefCont.json' vnsSvcRedirEcmpBucketCons_api = 'vnsSvcRedirEcmpBucketCons.json' count_filter = '?rsp-subtree-include=count' - + if not tversion: print_result(title, MANUAL, "Target version not supplied. Skipping.") return MANUAL @@ -4867,7 +4881,7 @@ def pbr_high_scale_check(index, total_checks, tversion, **kwargs): if tversion.older_than("5.3(2c)"): vnsAdj = icurl('class', vnsAdjacencyDefCont_api+count_filter) vnsSvc = icurl('class', vnsSvcRedirEcmpBucketCons_api+count_filter) - + vnsAdj_count = int(vnsAdj[0]['moCount']['attributes']['count']) vnsSvc_count = int(vnsSvc[0]['moCount']['attributes']['count']) total = vnsAdj_count + vnsSvc_count @@ -4877,7 +4891,7 @@ def pbr_high_scale_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) return result @@ -4948,8 +4962,10 @@ def standby_sup_sync_check(index, total_checks, cversion, tversion, **kwargs): print_result(title, MANUAL, "Target version not supplied. Skipping.") return MANUAL - if ((cversion.older_than("4.2(7t)") or (cversion.major_version == "5.2" and cversion.older_than("5.2(5d)"))) - and ((tversion.major_version == "5.2" and tversion.older_than("5.2(7f)")) or tversion.newer_than("6.0(2h)"))): + if ( + (cversion.older_than("4.2(7t)") or (cversion.major_version == "5.2" and cversion.older_than("5.2(5d)"))) + and ((tversion.major_version == "5.2" and tversion.older_than("5.2(7f)")) or tversion.newer_than("6.0(2h)")) + ): eqptSupC = icurl('class', eqptSupC_api) for node in eqptSupC: node_dn = node['eqptSupC']['attributes']['dn'] @@ -4963,7 +4979,7 @@ def standby_sup_sync_check(index, total_checks, cversion, tversion, **kwargs): if data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) return result @@ -5000,7 +5016,7 @@ def equipment_disk_limits_exceeded(index, total_checks, **kwargs): data.append([dn_match.group('pod'), dn_match.group('node'), attributes['code'], percent, attributes['descr']]) else: unformatted_data.append([attributes['dn'], percent, attributes['descr']]) - + if data or unformatted_data: result = FAIL_UF @@ -5090,6 +5106,7 @@ def service_bd_forceful_routing_check(index, total_checks, cversion, tversion, * print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action, doc_url) return result + # Connection Base Check def observer_db_size_check(index, total_checks, username, password, **kwargs): title = 'Observer Database Size' @@ -5122,7 +5139,7 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): c.connect() except Exception as e: data.append([attr['id'], attr['name'], str(e)]) - print_result(node_title, ERROR) + print_result(node_title, ERROR, json_output=False) has_error = True continue try: @@ -5130,7 +5147,7 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): c.cmd(cmd) if "No such file or directory" in c.output: data.append([attr['id'], '/data2/dbstats/ not found', "Check user permissions or retry as 'apic#fallback\\\\admin'"]) - print_result(node_title, ERROR) + print_result(node_title, ERROR, json_output=False) has_error = True continue dbstats = c.output.split("\n") @@ -5141,10 +5158,10 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): file_size = size_match.group("size") file_name = "/data2/dbstats/" + size_match.group("file") data.append([attr['id'], file_name, file_size]) - print_result(node_title, DONE) + print_result(node_title, DONE, json_output=False) except Exception as e: data.append([attr['id'], attr['name'], str(e)]) - print_result(node_title, ERROR) + print_result(node_title, ERROR, json_output=False) has_error = True continue if has_error: @@ -5184,44 +5201,62 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): return result -def args(): +# ---- Script Execution ---- + +def parse_args(args): parser = ArgumentParser(description="ACI Pre-Upgrade Validation Script - %s" % SCRIPT_VERSION) parser.add_argument("-t", "--tversion", action="store", type=str, help="Upgrade Target Version. Ex. 6.2(1a)") parser.add_argument("--puv", action="store_true", help="For built-in PUV. API Checks only. Checks using SSH are skipped.") - return parser.parse_args() + parsed_args = parser.parse_args(args) + is_puv = parsed_args.puv + tversion = parsed_args.tversion + # if tversion arg was provided, validate if it is a valid ACI version + if tversion: + try: + tversion = AciVersion(tversion) + except RuntimeError as e: + prints(e) + sys.exit(1) + return is_puv, tversion -if __name__ == "__main__": - args = args() - is_puv = args.puv +def prepare(is_puv, arg_tversion, total_checks): prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') - if is_puv: - username = password = None - if os.path.exists(LIVE_RESULTS_DIR) and os.path.isdir(LIVE_RESULTS_DIR): - shutil.rmtree(LIVE_RESULTS_DIR) - else: - prints('To use a non-default Login Domain, enter apic#DOMAIN\\\\USERNAME') + + username = password = None + if not is_puv: username, password = get_credentials() try: cversion = get_current_version() - tversion = AciVersion(args.tversion) if args.tversion else get_target_version() + tversion = arg_tversion if arg_tversion else get_target_version() vpc_nodes = get_vpc_nodes() sw_cversion = get_switch_version() except Exception as e: - prints('') - err = 'Error: %s' % e - print_title(err) - print_result(err, ERROR) - print_title("Initial query failed. Ensure APICs are healthy. Ending script run.") + prints('\n\nError: %s' % e) + prints("Initial query failed. Ensure APICs are healthy. Ending script run.") logging.exception(e) sys.exit() inputs = {'username': username, 'password': password, 'cversion': cversion, 'tversion': tversion, 'vpc_node_ids': vpc_nodes, 'sw_cversion': sw_cversion} - json_log = {"name": "PreupgradeCheck", "method": "standalone script", "datetime": ts + tz, - "script_version": str(SCRIPT_VERSION), "check_details": [], - 'cversion': str(cversion), 'tversion': str(tversion), 'sw_cversion': str(sw_cversion)} + metadata = { + "name": "PreupgradeCheck", + "method": "standalone script", + "datetime": ts + tz, + "script_version": str(SCRIPT_VERSION), + "cversion": str(cversion), + "tversion": str(tversion), + "sw_cversion": str(sw_cversion), + "is_puv": is_puv, + "total_checks": total_checks, + } + with open(META_FILE, "w") as f: + json.dump(metadata, f, indent=2) + return inputs + + +def get_checks(is_puv): api_checks = [ # General Checks target_version_compatibility_check, @@ -5323,40 +5358,46 @@ def args(): observer_db_size_check, ] - checks = conn_checks + api_checks if is_puv: - checks = api_checks - summary = {PASS: 0, FAIL_O: 0, FAIL_UF: 0, ERROR: 0, MANUAL: 0, POST: 0, NA: 0, 'TOTAL': len(checks)} + return api_checks + return conn_checks + api_checks + + +def run_checks(checks, inputs): + summary_headers = [PASS, FAIL_O, FAIL_UF, MANUAL, POST, NA, ERROR, 'TOTAL'] + summary = {key: 0 if key != 'TOTAL' else len(checks) for key in summary_headers} for idx, check in enumerate(checks): try: r = check(idx + 1, len(checks), **inputs) summary[r] += 1 - json_log["check_details"].append({"check_number": idx + 1, "name": check.__name__, "results": r}) except KeyboardInterrupt: prints('\n\n!!! KeyboardInterrupt !!!\n') break except Exception as e: + # synth.writeResult() uses the first arg in `print_result()` (i.e. title) as + # the filename. When a check has an error and ends up here, we don't know the + # title and we cannot use the error message as the title/filename either. + # Thus, using the func name of the check as the title/filename. prints('') - err = 'Error: %s' % e - print_title(err) - print_result(err, ERROR) + print_title(" " * len(check.__name__)) # not showing the func name in the stdout + msg = 'Unexpected Error: %s' % e + print_result(check.__name__, ERROR, msg) summary[ERROR] += 1 logging.exception(e) - prints('\n=== Summary Result ===\n') - jsonString = json.dumps(json_log) - with open(JSON_FILE, 'w') as f: - f.write(jsonString) - - subprocess.check_output(['tar', '-czf', BUNDLE_NAME, DIR]) - summary_headers = [PASS, FAIL_O, FAIL_UF, MANUAL, POST, NA, ERROR, 'TOTAL'] + prints('\n=== Summary Result ===\n') res = max(summary_headers, key=len) max_header_len = len(res) for key in summary_headers: prints('{:{}} : {:2}'.format(key, max_header_len, summary[key])) - bundle_loc = '/'.join([os.getcwd(), BUNDLE_NAME]) + with open(SUMMARY_FILE, 'w') as f: + json.dump(summary, f, indent=2) + +def wrapup(is_puv): + subprocess.check_output(['tar', '-czf', BUNDLE_NAME, DIR]) + bundle_loc = '/'.join([os.getcwd(), BUNDLE_NAME]) prints(""" Pre-Upgrade Check Complete. Next Steps: Address all checks flagged as FAIL, ERROR or MANUAL CHECK REQUIRED @@ -5368,6 +5409,18 @@ def args(): """.format(bundle=bundle_loc)) prints('==== Script Version %s FIN ====' % (SCRIPT_VERSION)) - if not is_puv and os.path.exists(LIVE_RESULTS_DIR) and os.path.isdir(LIVE_RESULTS_DIR): - shutil.rmtree(LIVE_RESULTS_DIR) - subprocess.check_output(['rm', '-rf', DIR]) + # puv integration needs to keep reading files from `JSON_DIR` under `DIR`. + if not is_puv and os.path.isdir(DIR): + shutil.rmtree(DIR) + + +def main(args=None): + is_puv, arg_tversion = parse_args(args) + checks = get_checks(is_puv) + inputs = prepare(is_puv, arg_tversion, len(checks)) + run_checks(checks, inputs) + wrapup(is_puv) + + +if __name__ == "__main__": + main() diff --git a/tests/test_get_vpc_node.py b/tests/test_get_vpc_node.py index 428cab11..956377cc 100644 --- a/tests/test_get_vpc_node.py +++ b/tests/test_get_vpc_node.py @@ -41,15 +41,40 @@ } ] +data2 = [ + {"fabricNodePEp": {"attributes": {"dn": "uni/fabric/protpol/expgep-101-102/nodepep-101", "id": "101"}}}, + {"fabricNodePEp": {"attributes": {"dn": "uni/fabric/protpol/expgep-101-102/nodepep-102", "id": "102"}}}, + {"fabricNodePEp": {"attributes": {"dn": "uni/fabric/protpol/expgep-103-104/nodepep-103", "id": "103"}}}, + {"fabricNodePEp": {"attributes": {"dn": "uni/fabric/protpol/expgep-103-104/nodepep-104", "id": "104"}}}, + {"fabricNodePEp": {"attributes": {"dn": "uni/fabric/protpol/expgep-105-106/nodepep-105", "id": "105"}}}, + {"fabricNodePEp": {"attributes": {"dn": "uni/fabric/protpol/expgep-105-106/nodepep-106", "id": "106"}}}, +] + @pytest.mark.parametrize( - "icurl_outputs, expected_result", + "icurl_outputs, expected_result, expected_stdout", [ + ( + {fabricNodePEps: []}, + [], + "Collecting VPC Node IDs...\n\n", + ), ( {fabricNodePEps: data}, - ["101", "103", "204", "206"] + ["101", "103", "204", "206"], + "Collecting VPC Node IDs...101, 103, 204, 206\n\n", + ), + ( + {fabricNodePEps: data2}, + ["101", "102", "103", "104", "105", "106"], + "Collecting VPC Node IDs...101, 102, 103, 104, ... (and 2 more)\n\n", ) ] ) -def test_get_vpc_nodes(mock_icurl, expected_result): - assert set(script.get_vpc_nodes()) == set(expected_result) +def test_get_vpc_nodes(capsys, mock_icurl, expected_result, expected_stdout): + vpc_nodes = script.get_vpc_nodes() + assert vpc_nodes == expected_result + + captured = capsys.readouterr() + print(captured.out) + assert captured.out == expected_stdout diff --git a/tests/test_parse_args.py b/tests/test_parse_args.py new file mode 100644 index 00000000..f4c544da --- /dev/null +++ b/tests/test_parse_args.py @@ -0,0 +1,54 @@ +import pytest +import importlib +import sys + +script = importlib.import_module("aci-preupgrade-validation-script") +AciVersion = script.AciVersion + + +def test_no_args(): + # When `None` or nothing is passed to `ArgumentParser.parse_args()`, the `argparse` + # module reads `sys.argv[1:]` which should return an empty list when a script is + # run without any command-line arguments. However, in pytest, `sys.argv[1:]` is + # the arguments to the pytest command which may not be empty. + # To simulate the script being run without any command-line arguments, + # we set `sys.argv[1:]` to an empty list when `args` is `None`. + sys.argv[1:] = [] + is_puv, tversion = script.parse_args(args=None) + assert is_puv is False + assert tversion is None + + +@pytest.mark.parametrize( + "args, expected_result", + [ + ([], False), + (["--puv"], True), + ], +) +def test_puv(args, expected_result): + is_puv, tversion = script.parse_args(args) + assert is_puv == expected_result + + +@pytest.mark.parametrize( + "args, expected_result", + [ + ([], None), + (["-t", "6.2(1a)"], AciVersion("6.2(1a)")), + (["-t", "16.2(1a)"], AciVersion("6.2(1a)")), + (["-t", "n9000-16.2(1a).bin"], AciVersion("6.2(1a)")), + (["-t", "aci-apic-dk9.6.2.1a.bin"], AciVersion("6.2(1a)")), + ], +) +def test_tversion(args, expected_result): + is_puv, tversion = script.parse_args(args) + if tversion is not None: + assert isinstance(tversion, AciVersion) + assert str(tversion) == str(expected_result) + + +def test_tversion_invald(): + with pytest.raises(SystemExit): + with pytest.raises(RuntimeError): + script.parse_args(args=["-t", "invalid_version"]) diff --git a/tests/test_prepare.py b/tests/test_prepare.py new file mode 100644 index 00000000..41252bfd --- /dev/null +++ b/tests/test_prepare.py @@ -0,0 +1,182 @@ +import pytest +import importlib +import logging +import json + +script = importlib.import_module("aci-preupgrade-validation-script") +AciVersion = script.AciVersion + + +@pytest.fixture(autouse=True) +def mock_get_credentials(monkeypatch): + """Mock the get_credentials function to return a fixed username and password.""" + + def _mock_get_credentials(): + return ("admin", "mypassword") + + monkeypatch.setattr(script, "get_credentials", _mock_get_credentials) + + +@pytest.fixture(autouse=True) +def mock_get_target_version(monkeypatch): + """ + Mock `get_target_version()` to return a fixed target version. + Used when the script is run without the `-t` option which is simulated by + `arg_tversion`. + Not using `mock_icurl` because this function involves a user interaction to + select a version. + """ + + def _mock_get_target_version(): + return AciVersion("6.2(1a)") + + monkeypatch.setattr(script, "get_target_version", _mock_get_target_version) + + +outputs = { + "cversion": [ + { + "firmwareCtrlrRunning": { + "attributes": { + "dn": "topology/pod-1/node-1/sys/ctrlrfwstatuscont/ctrlrrunning", + "version": "6.1(1a)", + } + } + } + ], + "switch_version": [ + {"firmwareRunning": {"attributes": {"peVer": "6.1(1a)", "version": "n9000-16.1(1a)"}}}, + {"firmwareRunning": {"attributes": {"peVer": "6.0(9d)", "version": "n9000-16.0(9d)"}}}, + ], + "vpc_nodes": [ + {"fabricNodePEp": {"attributes": {"dn": "uni/fabric/protpol/expgep-101-102/nodepep-101", "id": "101"}}}, + {"fabricNodePEp": {"attributes": {"dn": "uni/fabric/protpol/expgep-101-102/nodepep-102", "id": "102"}}}, + ], +} + + +@pytest.mark.parametrize( + "icurl_outputs, is_puv, arg_tversion, expected_result", + [ + # Default, no argparse arguments + ( + { + "firmwareCtrlrRunning.json": outputs["cversion"], + "firmwareRunning.json": outputs["switch_version"], + "fabricNodePEp.json": outputs["vpc_nodes"], + }, + False, + None, + {"username": "admin", "password": "mypassword", "cversion": AciVersion("6.1(1a)"), "tversion": AciVersion("6.2(1a)"), "sw_cversion": AciVersion("6.0(9d)"), "vpc_node_ids": ["101", "102"]}, + ), + # `is_puv` is True (i.e. --puv) + # No `get_credentials()`, no username nor password + ( + { + "firmwareCtrlrRunning.json": outputs["cversion"], + "firmwareRunning.json": outputs["switch_version"], + "fabricNodePEp.json": outputs["vpc_nodes"], + }, + True, + None, + {"username": None, "password": None, "cversion": AciVersion("6.1(1a)"), "tversion": AciVersion("6.2(1a)"), "sw_cversion": AciVersion("6.0(9d)"), "vpc_node_ids": ["101", "102"]}, + ), + # `arg_tversion` is provided (i.e. -t 6.1(4a)) + # The version `get_target_version()` is ignored. + ( + { + "firmwareCtrlrRunning.json": outputs["cversion"], + "firmwareRunning.json": outputs["switch_version"], + "fabricNodePEp.json": outputs["vpc_nodes"], + }, + False, + AciVersion("6.1(4a)"), + {"username": "admin", "password": "mypassword", "cversion": AciVersion("6.1(1a)"), "tversion": AciVersion("6.1(4a)"), "sw_cversion": AciVersion("6.0(9d)"), "vpc_node_ids": ["101", "102"]}, + ), + ], +) +def test_prepare(mock_icurl, is_puv, arg_tversion, expected_result): + checks = script.get_checks(is_puv) + inputs = script.prepare(is_puv, arg_tversion, len(checks)) + for key, value in expected_result.items(): + if "version" in key: + assert isinstance(inputs[key], AciVersion) + assert str(inputs[key]) == str(value) + else: + assert inputs[key] == value + + with open(script.META_FILE, "r") as f: + meta = json.load(f) + assert meta["name"] == "PreupgradeCheck" + assert meta["method"] == "standalone script" + assert meta.get("datetime") is not None + assert meta["script_version"] == script.SCRIPT_VERSION + assert meta["cversion"] == str(expected_result["cversion"]) + assert meta["tversion"] == str(expected_result["tversion"]) + assert meta["sw_cversion"] == str(expected_result["sw_cversion"]) + assert meta["is_puv"] == is_puv + assert meta["total_checks"] == len(checks) + + +@pytest.mark.parametrize( + "icurl_outputs, is_puv, arg_tversion, expected_result", + [ + # `get_cversion()` failure + ( + { + "firmwareCtrlrRunning.json": [{"error": {"attributes": {"code": "400", "text": "Request failed, unresolved class for firmwareCtrlrRunning_fake"}}}], + "firmwareRunning.json": outputs["switch_version"], + "fabricNodePEp.json": outputs["vpc_nodes"], + }, + False, + None, + """\ +Checking current APIC version... + +Error: Your current ACI version does not have requested class +Initial query failed. Ensure APICs are healthy. Ending script run. +""", + ), + # `get_switch_version()` failure + ( + { + "firmwareCtrlrRunning.json": outputs["cversion"], + "firmwareRunning.json": [{"error": {"attributes": {"code": "400", "text": "Request failed, unresolved class for firmwareRunning_fake"}}}], + "fabricNodePEp.json": outputs["vpc_nodes"], + }, + False, + None, + """\ +Gathering Lowest Switch Version from Firmware Repository... + +Error: Your current ACI version does not have requested class +Initial query failed. Ensure APICs are healthy. Ending script run. +""", + ), + # `get_vpc_nodes()` failure + ( + { + "firmwareCtrlrRunning.json": outputs["cversion"], + "firmwareRunning.json": outputs["switch_version"], + "fabricNodePEp.json": [{"error": {"attributes": {"code": "400", "text": "Request failed, unresolved class for fabricNodePEp_fake"}}}], + }, + False, + None, + """\ +Collecting VPC Node IDs... + +Error: Your current ACI version does not have requested class +Initial query failed. Ensure APICs are healthy. Ending script run. +""", + ), + ], +) +def test_prepare_exception(capsys, caplog, mock_icurl, is_puv, arg_tversion, expected_result): + caplog.set_level(logging.CRITICAL) + with pytest.raises(SystemExit): + with pytest.raises(Exception): + checks = script.get_checks(is_puv) + script.prepare(is_puv, arg_tversion, len(checks)) + captured = capsys.readouterr() + print(captured.out) + assert captured.out.endswith(expected_result) diff --git a/tests/test_run_checks.py b/tests/test_run_checks.py new file mode 100644 index 00000000..ebf76ba2 --- /dev/null +++ b/tests/test_run_checks.py @@ -0,0 +1,75 @@ +import importlib +import logging + +script = importlib.import_module("aci-preupgrade-validation-script") +AciVersion = script.AciVersion + + +def check_builder(func_name, title, result): + def _check(index, total_checks, **kwargs): + _check.__name__ = func_name + script.print_title(title, index, total_checks) + if result == script.ERROR: + raise Exception("This is a test exception to result in `script.ERROR`.") + else: + script.print_result(title, result) + return result + + return _check + + +fake_checks = [ + check_builder(func_name, title, result) + for func_name, title, result in [ + ("check1", "Test Check 1", script.PASS), + ("check2", "Test Check 2", script.FAIL_O), + ("check3", "Test Check 3", script.FAIL_UF), + ("check4", "Test Check 4", script.MANUAL), + ("check5", "Test Check 5", script.POST), + ("check6", "Test Check 6", script.NA), + ("check7", "Test Check 7", script.ERROR), + ("check8", "Test Check 8", script.PASS), + ] +] + + +fake_inputs = { + "username": "admin", + "password": "mypassword", + "cversion": AciVersion("6.1(1a)"), + "tversion": AciVersion("6.2(1a)"), + "sw_cversion": AciVersion("6.1(1a)"), + "vpc_node_ids": ["101", "102"], +} + + +def test_run_checks(capsys, caplog): + caplog.set_level(logging.CRITICAL) # Skip logging.exceptions in pytest output as it is expected. + script.run_checks(fake_checks, fake_inputs) + captured = capsys.readouterr() + print(captured.out) + assert ( + captured.out + == """\ +[Check 1/8] Test Check 1... PASS +[Check 2/8] Test Check 2... FAIL - OUTAGE WARNING!! +[Check 3/8] Test Check 3... FAIL - UPGRADE FAILURE!! +[Check 4/8] Test Check 4... MANUAL CHECK REQUIRED +[Check 5/8] Test Check 5... POST UPGRADE CHECK REQUIRED +[Check 6/8] Test Check 6... N/A +[Check 7/8] Test Check 7... + ... Unexpected Error: This is a test exception to result in `script.ERROR`. ERROR !! +[Check 8/8] Test Check 8... PASS + +=== Summary Result === + +PASS : 2 +FAIL - OUTAGE WARNING!! : 1 +FAIL - UPGRADE FAILURE!! : 1 +MANUAL CHECK REQUIRED : 1 +POST UPGRADE CHECK REQUIRED : 1 +N/A : 1 +ERROR !! : 1 +TOTAL : 8 +""" # noqa: W291 + ) From 78f94102c97f2f740dc3a5e27fa2bbed1a729e0d Mon Sep 17 00:00:00 2001 From: takishida <38262981+takishida@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:31:36 -0700 Subject: [PATCH 19/32] feat: Support QA version and AciVersion instance as input in AciVersion (#259) * feat: Support QA version and AciVersion instance as input in AciVersion * fix: Use supported version format in older_than * fix: Use ValueError in AciVersion --- aci-preupgrade-validation-script.py | 75 +++++++++++------- tests/test_AciVersion.py | 117 ++++++++++++++++++---------- tests/test_parse_args.py | 2 +- 3 files changed, 122 insertions(+), 72 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 1d2df205..09077304 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -500,50 +500,65 @@ def ip_in_subnet(cls, ip, subnet): class AciVersion(): - v_regex = r'(?:dk9\.)?[1]?(?P\d)\.(?P\d)(?:\.|\()(?P\d+)\.?(?P(?:[a-b]|[0-9a-z]+))\)?' + """ + ACI Version parser class. Parses the version string and provides methods to compare versions. + Supported version formats: + - APIC: `5.2(7f)`, `5.2.7f`, `5.2(7.123a)`, `5.2.7.123a`, `5.2(7.123)`, `5.2.7.123`, `aci-apic-dk9.5.2.7f.iso/bin` + - Switch: `15.2(7f)`, `15.2.7f`, `15.2(7.123a)`, `15.2.7.123a`, `15.2(7.123)`, `15.2.7.123`, `aci-n9000-dk9.15.2.7f.bin` + """ + v_regex = r'(?:dk9\.)?[1]?(?P\d)\.(?P\d)(?:\.|\()(?P\d+)(?P\.?)(?P(?:[a-z]|\d+))(?P[a-z]?)\)?' def __init__(self, version): self.original = version v = re.search(self.v_regex, version) - self.version = ('{major1}.{major2}({maint}{patch})' - .format(**v.groupdict()) if v else None) - self.dot_version = ("{major1}.{major2}.{maint}{patch}" - .format(**v.groupdict()) if v else None) - self.simple_version = ("{major1}.{major2}({maint})" - .format(**v.groupdict()) if v else None) - self.major_version = ("{major1}.{major2}" - .format(**v.groupdict()) if v else None) - self.major1 = v.group('major1') if v else None - self.major2 = v.group('major2') if v else None - self.maint = v.group('maint') if v else None - self.patch = v.group('patch') if v else None - self.regex = v if not v: - raise RuntimeError("Parsing failure of ACI version `%s`" % version) + raise ValueError("Parsing failure of ACI version `%s`" % version) + self.version = "{major1}.{major2}({maint}{QAdot}{patch1}{patch2})".format(**v.groupdict()) + self.dot_version = "{major1}.{major2}.{maint}{QAdot}{patch1}{patch2}".format(**v.groupdict()) + self.simple_version = "{major1}.{major2}({maint})".format(**v.groupdict()) + self.major_version = "{major1}.{major2}".format(**v.groupdict()) + self.major1 = v.group("major1") + self.major2 = v.group("major2") + self.maint = v.group("maint") + self.patch1 = v.group("patch1") + self.patch2 = v.group("patch2") + self.regex = v def __str__(self): return self.version def older_than(self, version): - v = re.search(self.v_regex, version) - if not v: return None - for i in range(1, len(v.groups())+1): - if i < 4: - if int(self.regex.group(i)) > int(v.group(i)): return False - elif int(self.regex.group(i)) < int(v.group(i)): return True - if i == 4: - if self.regex.group(i) > v.group(i): return False - elif self.regex.group(i) < v.group(i): return True + v2 = version if isinstance(version, AciVersion) else AciVersion(version) + for key in ["major1", "major2", "maint"]: + if int(self.regex.group(key)) > int(v2.regex.group(key)): return False + elif int(self.regex.group(key)) < int(v2.regex.group(key)): return True + # Patch1 can be alphabet or number + if self.patch1.isalpha() and v2.patch1.isdigit(): + return True # e.g., 5.2(7f) is older than 5.2(7.123) + elif self.patch1.isdigit() and v2.patch1.isalpha(): + return False + elif self.patch1.isalpha() and v2.patch1.isalpha(): + if self.patch1 > v2.patch1: return False + elif self.patch1 < v2.patch1: return True + elif self.patch1.isdigit() and v2.patch1.isdigit(): + if int(self.patch1) > int(v2.patch1): return False + elif int(self.patch1) < int(v2.patch1): return True + # Patch2 (alphabet) is optional. + if not self.patch2 and v2.patch2: + return True # one without Patch2 is older. + elif self.patch2 and not v2.patch2: + return False + elif self.patch2 and v2.patch2: + if self.patch2 > v2.patch2: return False + elif self.patch2 < v2.patch2: return True return False def newer_than(self, version): return not self.older_than(version) and not self.same_as(version) def same_as(self, version): - v = re.search(self.v_regex, version) - ver = ('{major1}.{major2}({maint}{patch})' - .format(**v.groupdict()) if v else None) - return self.version == ver + v2 = version if isinstance(version, AciVersion) else AciVersion(version) + return self.version == v2.version class AciObjectCrawler(object): @@ -1292,7 +1307,7 @@ def apic_cluster_health_check(index, total_checks, cversion, **kwargs): unformatted_data = [] doc_url = 'ACI Troubleshooting Guide 2nd Edition - http://cs.co/9003ybZ1d' print_title(title, index, total_checks) - if cversion.older_than("4.2"): + if cversion.older_than("4.2(1a)"): recommended_action = 'Follow "Initial Fabric Setup" in ACI Troubleshooting Guide 2nd Edition' else: recommended_action = 'Troubleshoot by running "acidiag cluster" on APIC CLI' @@ -5214,7 +5229,7 @@ def parse_args(args): if tversion: try: tversion = AciVersion(tversion) - except RuntimeError as e: + except ValueError as e: prints(e) sys.exit(1) return is_puv, tversion diff --git a/tests/test_AciVersion.py b/tests/test_AciVersion.py index 77f7f109..d5105cc9 100644 --- a/tests/test_AciVersion.py +++ b/tests/test_AciVersion.py @@ -5,66 +5,101 @@ @pytest.mark.parametrize( - "input, major1, major2, maint, patch", + "input, major1, major2, maint, patch1, patch2, version", [ # APIC version format - ("5.2(7f)", "5", "2", "7", "f"), - ("5.2.7f", "5", "2", "7", "f"), - ("5.2(7.123a)", "5", "2", "7", "123a"), - ("5.2.7.123a", "5", "2", "7", "123a"), - ("aci-apic-dk9.5.2.7f.iso", "5", "2", "7", "f"), + ("5.2(7f)", "5", "2", "7", "f", "", "5.2(7f)"), + ("5.2.7f", "5", "2", "7", "f", "", "5.2(7f)"), + ("5.2(7.123a)", "5", "2", "7", "123", "a", "5.2(7.123a)"), + ("5.2.7.123a", "5", "2", "7", "123", "a", "5.2(7.123a)"), + ("5.2(7.123)", "5", "2", "7", "123", "", "5.2(7.123)"), + ("5.2.7.123", "5", "2", "7", "123", "", "5.2(7.123)"), + ("aci-apic-dk9.5.2.7f.iso", "5", "2", "7", "f", "", "5.2(7f)"), # Switch version format - ("15.2(7f)", "5", "2", "7", "f"), - ("15.2.7f", "5", "2", "7", "f"), - ("15.2(7.123a)", "5", "2", "7", "123a"), - ("15.2.7.123a", "5", "2", "7", "123a"), - ("aci-n9000-dk9.15.2.7f.bin", "5", "2", "7", "f"), + ("15.2(7f)", "5", "2", "7", "f", "", "5.2(7f)"), + ("15.2.7f", "5", "2", "7", "f", "", "5.2(7f)"), + ("15.2(7.123a)", "5", "2", "7", "123", "a", "5.2(7.123a)"), + ("15.2.7.123a", "5", "2", "7", "123", "a", "5.2(7.123a)"), + ("15.2(7.123)", "5", "2", "7", "123", "", "5.2(7.123)"), + ("15.2.7.123", "5", "2", "7", "123", "", "5.2(7.123)"), + ("aci-n9000-dk9.15.2.7f.bin", "5", "2", "7", "f", "", "5.2(7f)"), ], ) -def test_basic(input, major1, major2, maint, patch): +def test_basic(input, major1, major2, maint, patch1, patch2, version): v = script.AciVersion(input) assert ( v.major1 == major1 and v.major2 == major2 and v.maint == maint - and v.patch == patch + and v.patch1 == patch1 + and v.patch2 == patch2 ) + assert str(v) == version @pytest.mark.parametrize( - "ver1, ver2, older_than, newer_than, same_as", + "ver1, ver2, expected_result", [ # APIC version format - ("5.2(7f)", "5.2(7f)", False, False, True), - ("5.2(7f)", "5.2(7g)", True, False, False), - ("5.2(7f)", "5.2(7e)", False, True, False), - ("5.2(7f)", "5.2(10f)", True, False, False), - ("5.2(7f)", "5.2(1f)", False, True, False), - ("5.2(7f)", "5.1(2a)", False, True, False), - ("5.2(7f)", "5.3(2a)", True, False, False), - ("5.2(7f)", "4.2(7l)", False, True, False), - ("5.2(7f)", "6.0(2h)", True, False, False), + ("5.2(7f)", "5.2(7f)", "same"), + ("5.2(7f)", "5.2(7g)", "old"), # patch1 + ("5.2(7f)", "5.2(7e)", "new"), # patch1 + ("5.2(7f)", "5.2(10f)", "old"), # maint + ("5.2(7f)", "5.2(1f)", "new"), # maint + ("5.2(7f)", "5.3(2a)", "old"), # major2 + ("5.2(7f)", "5.1(2a)", "new"), # major2 + ("5.2(7f)", "6.0(2h)", "old"), # major1 + ("5.2(7f)", "4.2(7l)", "new"), # major1 + ("5.2(7.123b)", "5.2(7.123b)", "same"), + ("5.2(7.123b)", "5.2(7.123c)", "old"), # QA patch2 + ("5.2(7.123b)", "5.2(7.123a)", "new"), # QA patch2 + ("5.2(7.123b)", "5.2(7.124b)", "old"), # QA patch1 + ("5.2(7.123b)", "5.2(7.90b)", "new"), # QA patch1 + ("5.2(7.123)", "5.2(7.123)", "same"), + ("5.2(7.123)", "5.2(7.124)", "old"), # QA patch1 + ("5.2(7.123)", "5.2(7.90)", "new"), # QA patch1 + ("5.2(7.123)", "5.2(7.123a)", "old"), # None vs QA patch2 + ("5.2(7.123a)", "5.2(7.123)", "new"), # QA patch2 vs None + ("5.2(7f)", "5.2(7.90a)", "old"), # CCO patch1 vs QA patch1 + ("5.2(7.90a)", "5.2(7f)", "new"), # QA patch1 vs CCO patch1 # Switch version format - ("15.2(7f)", "15.2(7f)", False, False, True), - ("15.2(7f)", "15.2(7g)", True, False, False), - ("15.2(7f)", "15.2(7e)", False, True, False), - ("15.2(7f)", "15.2(10f)", True, False, False), - ("15.2(7f)", "15.2(1f)", False, True, False), - ("15.2(7f)", "15.1(2a)", False, True, False), - ("15.2(7f)", "15.3(2a)", True, False, False), - ("15.2(7f)", "14.2(7l)", False, True, False), - ("15.2(7f)", "16.0(2h)", True, False, False), + ("15.2(7f)", "15.2(7f)", "same"), + ("15.2(7f)", "15.2(7g)", "old"), + ("15.2(7f)", "15.2(7e)", "new"), + ("15.2(7f)", "15.2(10f)", "old"), + ("15.2(7f)", "15.2(1f)", "new"), + ("15.2(7f)", "15.1(2a)", "new"), + ("15.2(7f)", "15.3(2a)", "old"), + ("15.2(7f)", "14.2(7l)", "new"), + ("15.2(7f)", "16.0(2h)", "old"), ], ) class TestComparison: - def test_older_than(self, ver1, ver2, older_than, newer_than, same_as): - v = script.AciVersion(ver1) - assert v.older_than(ver2) == older_than + def test_older_than(self, ver1, ver2, expected_result): + result = True if expected_result == "old" else False + v1 = script.AciVersion(ver1) + v2 = script.AciVersion(ver2) + assert v1.older_than(ver2) == result + assert v1.older_than(v2) == result - def test_newer_than(self, ver1, ver2, older_than, newer_than, same_as): - v = script.AciVersion(ver1) - assert v.newer_than(ver2) == newer_than + def test_newer_than(self, ver1, ver2, expected_result): + result = True if expected_result == "new" else False + v1 = script.AciVersion(ver1) + v2 = script.AciVersion(ver2) + assert v1.newer_than(ver2) == result + assert v1.newer_than(v2) == result - def test_same_as(self, ver1, ver2, older_than, newer_than, same_as): - v = script.AciVersion(ver1) - assert v.same_as(ver2) == same_as + def test_same_as(self, ver1, ver2, expected_result): + result = True if expected_result == "same" else False + v1 = script.AciVersion(ver1) + v2 = script.AciVersion(ver2) + assert v1.same_as(ver2) == result + assert v1.same_as(v2) == result + + +def test_invalid_version(): + with pytest.raises(ValueError): + script.AciVersion("invalid_version") + + with pytest.raises(ValueError): + script.AciVersion("5.2(7)") diff --git a/tests/test_parse_args.py b/tests/test_parse_args.py index f4c544da..c3bbac99 100644 --- a/tests/test_parse_args.py +++ b/tests/test_parse_args.py @@ -50,5 +50,5 @@ def test_tversion(args, expected_result): def test_tversion_invald(): with pytest.raises(SystemExit): - with pytest.raises(RuntimeError): + with pytest.raises(ValueError): script.parse_args(args=["-t", "invalid_version"]) From 4da893f91d09186ba4ef3b32ecdd976a3cd3d6df Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 2 Jul 2025 15:51:03 -0400 Subject: [PATCH 20/32] failureDetails.data + pytest --- aci-preupgrade-validation-script.py | 18 ++++++++++++ tests/test_synthenticMaintPValidate.py | 40 ++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 09077304..d0959001 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -86,6 +86,20 @@ def __init__(self, name, description, path=JSON_DIR): self.filename = cleaned_name + '.json' self.path = path + @staticmethod + def craftData(column, row): + data = [] + if isinstance(row, list) and isinstance(column, list): + for i in range(len(row)): + entry = {} + for j in range(len(column)): + if j < len(row[i]): + entry[column[j]] = row[i][j] + else: + entry[column[j]] = None + data.append(entry) + return data + def updateWithResults(self, result, recommended_action, reason, header, footer, column, row, unformatted_column, unformatted_rows): self.reason = reason @@ -112,6 +126,10 @@ def updateWithResults(self, result, recommended_action, reason, header, footer, self.failureDetails["row"] = row self.failureDetails["unformatted_column"] = unformatted_column self.failureDetails["unformatted_rows"] = unformatted_rows + self.failureDetails["data"] = self.craftData(column, row) + if unformatted_column and unformatted_rows: + self.failureDetails["data"].extend(self.craftData(unformatted_column, unformatted_rows)) + self.reason += " Parse failure occurred, please check unformatted data in the output data." def buildResult(self): result = { diff --git a/tests/test_synthenticMaintPValidate.py b/tests/test_synthenticMaintPValidate.py index ebec9bc6..1a8e628f 100644 --- a/tests/test_synthenticMaintPValidate.py +++ b/tests/test_synthenticMaintPValidate.py @@ -119,14 +119,48 @@ "test reason", "test header", "test footer", - ["col1", "col2"], - [["row1", "row2"], ["row3", "row4"]], - ["col1", "col2"], + ["col1", "col2", "col3"], + [["row1", "row2", "row3"], ["row4", "row5", "row6"]], + ["col4", "col5"], [["row1", "row2"], ["row3", "row4"]], True, "critical", False ), + # Check 8: FAIL_O Formatted only + ( + "FAIL_O Formatted only", + "", + script.FAIL_O, + "reboot", + "test reason", + "test header", + "test footer", + ["col1", "col2", "col3"], + [["row1", "row2", "row3"], ["row4", "row5", "row6"]], + [], + [], + True, + "critical", + False + ), + # Check 9: FAIL_O + ( + "FAIL_O Unformatted only", + "", + script.FAIL_O, + "reboot", + "test reason", + "test header", + "test footer", + [], + [], + ["col1", "col2", "col3"], + [["row1", "row2", "row3"], ["row4", "row5", "row6"]], + True, + "critical", + False + ), ], ) def test_syntheticMaintPValidate( From d7098af467ae072a08c652371effc8a48553fd2a Mon Sep 17 00:00:00 2001 From: GM Date: Fri, 11 Jul 2025 10:16:33 -0400 Subject: [PATCH 21/32] -c / -d args into puv (#265) * -c (cversion) and -d (debug_function) for dev testing * pytest fix WIP * fixed pytest --- aci-preupgrade-validation-script.py | 49 ++++++++----- tests/test_parse_args.py | 54 ++++++++++---- tests/test_prepare.py | 108 +++++++++++++++++++++++++--- 3 files changed, 172 insertions(+), 39 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index d0959001..4d2d2510 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -1228,8 +1228,16 @@ def get_credentials(): return usr, pwd -def get_current_version(): +def get_current_version(arg_cversion): """ Returns: AciVersion instance """ + if arg_cversion: + prints("Current APIC version is overridden to %s" % arg_cversion) + try: + current_version = AciVersion(arg_cversion) + except ValueError as e: + prints(e) + sys.exit(1) + return current_version prints("Checking current APIC version...", end='') firmwares = icurl('class', 'firmwareCtrlrRunning.json') for firmware in firmwares: @@ -1241,8 +1249,16 @@ def get_current_version(): return current_version -def get_target_version(): +def get_target_version(arg_tversion): """ Returns: AciVersion instance """ + if arg_tversion: + prints("Target APIC version is overridden to %s" % arg_tversion) + try: + target_version = AciVersion(arg_tversion) + except ValueError as e: + prints(e) + sys.exit(1) + return target_version prints("Gathering APIC Versions from Firmware Repository...\n") repo_list = [] response_json = icurl('class', @@ -5239,21 +5255,18 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): def parse_args(args): parser = ArgumentParser(description="ACI Pre-Upgrade Validation Script - %s" % SCRIPT_VERSION) parser.add_argument("-t", "--tversion", action="store", type=str, help="Upgrade Target Version. Ex. 6.2(1a)") + parser.add_argument("-c", "--cversion", action="store", type=str, help="Override Current Version. Ex. 6.1(1a)") + parser.add_argument("-d", "--debug_function", action="store", type=str, help="Name of a single function to debug. Ex. 'apic_version_md5_check'") parser.add_argument("--puv", action="store_true", help="For built-in PUV. API Checks only. Checks using SSH are skipped.") parsed_args = parser.parse_args(args) is_puv = parsed_args.puv tversion = parsed_args.tversion - # if tversion arg was provided, validate if it is a valid ACI version - if tversion: - try: - tversion = AciVersion(tversion) - except ValueError as e: - prints(e) - sys.exit(1) - return is_puv, tversion + cversion = parsed_args.cversion + debug_function = parsed_args.debug_function + return is_puv, tversion, cversion, debug_function -def prepare(is_puv, arg_tversion, total_checks): +def prepare(is_puv, arg_tversion, arg_cversion, total_checks): prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') @@ -5261,8 +5274,8 @@ def prepare(is_puv, arg_tversion, total_checks): if not is_puv: username, password = get_credentials() try: - cversion = get_current_version() - tversion = arg_tversion if arg_tversion else get_target_version() + cversion = get_current_version(arg_cversion) + tversion = get_target_version(arg_tversion) vpc_nodes = get_vpc_nodes() sw_cversion = get_switch_version() except Exception as e: @@ -5289,7 +5302,7 @@ def prepare(is_puv, arg_tversion, total_checks): return inputs -def get_checks(is_puv): +def get_checks(is_puv, debug_func): api_checks = [ # General Checks target_version_compatibility_check, @@ -5391,6 +5404,8 @@ def get_checks(is_puv): observer_db_size_check, ] + if debug_func: + return [check for check in api_checks + conn_checks if check.__name__ == debug_func] if is_puv: return api_checks return conn_checks + api_checks @@ -5448,9 +5463,9 @@ def wrapup(is_puv): def main(args=None): - is_puv, arg_tversion = parse_args(args) - checks = get_checks(is_puv) - inputs = prepare(is_puv, arg_tversion, len(checks)) + is_puv, arg_tversion, arg_cversion, debug_func = parse_args(args) + checks = get_checks(is_puv, debug_func) + inputs = prepare(is_puv, arg_tversion, arg_cversion, len(checks)) run_checks(checks, inputs) wrapup(is_puv) diff --git a/tests/test_parse_args.py b/tests/test_parse_args.py index c3bbac99..74bd37d1 100644 --- a/tests/test_parse_args.py +++ b/tests/test_parse_args.py @@ -14,9 +14,11 @@ def test_no_args(): # To simulate the script being run without any command-line arguments, # we set `sys.argv[1:]` to an empty list when `args` is `None`. sys.argv[1:] = [] - is_puv, tversion = script.parse_args(args=None) + is_puv, tversion, cversion, debug_function = script.parse_args(args=None) assert is_puv is False assert tversion is None + assert cversion is None + assert debug_function is None @pytest.mark.parametrize( @@ -27,7 +29,7 @@ def test_no_args(): ], ) def test_puv(args, expected_result): - is_puv, tversion = script.parse_args(args) + is_puv, tversion, cversion, debug_function = script.parse_args(args) assert is_puv == expected_result @@ -35,20 +37,48 @@ def test_puv(args, expected_result): "args, expected_result", [ ([], None), - (["-t", "6.2(1a)"], AciVersion("6.2(1a)")), - (["-t", "16.2(1a)"], AciVersion("6.2(1a)")), - (["-t", "n9000-16.2(1a).bin"], AciVersion("6.2(1a)")), - (["-t", "aci-apic-dk9.6.2.1a.bin"], AciVersion("6.2(1a)")), + (["-t", "6.2(1a)"], "6.2(1a)"), + (["-t", "16.2(1a)"], "16.2(1a)"), + (["-t", "n9000-16.2(1a).bin"], "n9000-16.2(1a).bin"), + (["-t", "aci-apic-dk9.6.2.1a.bin"], "aci-apic-dk9.6.2.1a.bin"), + (["-t", "invalid_version"], "invalid_version"), ], ) def test_tversion(args, expected_result): - is_puv, tversion = script.parse_args(args) + is_puv, tversion, cversion, debug_function = script.parse_args(args) if tversion is not None: - assert isinstance(tversion, AciVersion) + assert isinstance(tversion, str) assert str(tversion) == str(expected_result) -def test_tversion_invald(): - with pytest.raises(SystemExit): - with pytest.raises(ValueError): - script.parse_args(args=["-t", "invalid_version"]) +@pytest.mark.parametrize( + "args, expected_result", + [ + ([], None), + (["-c", "6.2(1a)"], "6.2(1a)"), + (["-c", "16.2(1a)"], "16.2(1a)"), + (["-c", "n9000-16.2(1a).bin"], "n9000-16.2(1a).bin"), + (["-c", "aci-apic-dk9.6.2.1a.bin"], "aci-apic-dk9.6.2.1a.bin"), + (["-c", "invalid_version"], "invalid_version"), + ], +) +def test_cversion(args, expected_result): + is_puv, tversion, cversion, debug_function = script.parse_args(args) + if cversion is not None: + assert isinstance(cversion, str) + assert str(cversion) == str(expected_result) + + +@pytest.mark.parametrize( + "args, expected_result", + [ + ([], None), + (["-d", "pbr_high_scale_check"], "pbr_high_scale_check"), + (["-d", "made_up_func"], "made_up_func"), + ], +) +def test_debug_func(args, expected_result): + is_puv, tversion, cversion, debug_function = script.parse_args(args) + if debug_function is not None: + assert isinstance(debug_function, str) + assert str(debug_function) == str(expected_result) diff --git a/tests/test_prepare.py b/tests/test_prepare.py index 41252bfd..00bcfc62 100644 --- a/tests/test_prepare.py +++ b/tests/test_prepare.py @@ -27,7 +27,13 @@ def mock_get_target_version(monkeypatch): select a version. """ - def _mock_get_target_version(): + def _mock_get_target_version(arg_tversion): + if arg_tversion: + try: + return AciVersion(arg_tversion) + except ValueError as e: + script.prints(e) + raise SystemExit(1) return AciVersion("6.2(1a)") monkeypatch.setattr(script, "get_target_version", _mock_get_target_version) @@ -56,7 +62,7 @@ def _mock_get_target_version(): @pytest.mark.parametrize( - "icurl_outputs, is_puv, arg_tversion, expected_result", + "icurl_outputs, is_puv, arg_tversion, arg_cversion, debug_function, expected_result", [ # Default, no argparse arguments ( @@ -67,6 +73,8 @@ def _mock_get_target_version(): }, False, None, + None, + None, {"username": "admin", "password": "mypassword", "cversion": AciVersion("6.1(1a)"), "tversion": AciVersion("6.2(1a)"), "sw_cversion": AciVersion("6.0(9d)"), "vpc_node_ids": ["101", "102"]}, ), # `is_puv` is True (i.e. --puv) @@ -79,6 +87,8 @@ def _mock_get_target_version(): }, True, None, + None, + None, {"username": None, "password": None, "cversion": AciVersion("6.1(1a)"), "tversion": AciVersion("6.2(1a)"), "sw_cversion": AciVersion("6.0(9d)"), "vpc_node_ids": ["101", "102"]}, ), # `arg_tversion` is provided (i.e. -t 6.1(4a)) @@ -90,14 +100,72 @@ def _mock_get_target_version(): "fabricNodePEp.json": outputs["vpc_nodes"], }, False, - AciVersion("6.1(4a)"), + "6.1(4a)", + None, + None, {"username": "admin", "password": "mypassword", "cversion": AciVersion("6.1(1a)"), "tversion": AciVersion("6.1(4a)"), "sw_cversion": AciVersion("6.0(9d)"), "vpc_node_ids": ["101", "102"]}, ), + # `arg_tversion` and `arg_cversion` are both provided (i.e. -t 6.1(4a)) + # The version `get_target_version()` is ignored. + ( + { + "firmwareCtrlrRunning.json": outputs["cversion"], + "firmwareRunning.json": outputs["switch_version"], + "fabricNodePEp.json": outputs["vpc_nodes"], + }, + False, + "6.1(4a)", + "6.0(8d)", + None, + {"username": "admin", "password": "mypassword", "cversion": AciVersion("6.0(8d)"), "tversion": AciVersion("6.1(4a)"), "sw_cversion": AciVersion("6.0(9d)"), "vpc_node_ids": ["101", "102"]}, + ), + # `arg_tversion`, `arg_cversion` and 'debug_function' are all provided + # The version `get_target_version()` is ignored. + ( + { + "firmwareCtrlrRunning.json": outputs["cversion"], + "firmwareRunning.json": outputs["switch_version"], + "fabricNodePEp.json": outputs["vpc_nodes"], + }, + False, + "6.1(4a)", + "6.0(4d)", + "ave_eol_check", + {"username": "admin", "password": "mypassword", "cversion": AciVersion("6.0(4d)"), "tversion": AciVersion("6.1(4a)"), "sw_cversion": AciVersion("6.0(9d)"), "vpc_node_ids": ["101", "102"]}, + ), + # veresions are switch syntax + # The version `get_target_version()` is ignored. + ( + { + "firmwareCtrlrRunning.json": outputs["cversion"], + "firmwareRunning.json": outputs["switch_version"], + "fabricNodePEp.json": outputs["vpc_nodes"], + }, + False, + "16.1(4a)", + "16.0(4d)", + "ave_eol_check", + {"username": "admin", "password": "mypassword", "cversion": AciVersion("6.0(4d)"), "tversion": AciVersion("6.1(4a)"), "sw_cversion": AciVersion("6.0(9d)"), "vpc_node_ids": ["101", "102"]}, + ), + # veresions are switch or APIC syntax + # The version `get_target_version()` is ignored. + ( + { + "firmwareCtrlrRunning.json": outputs["cversion"], + "firmwareRunning.json": outputs["switch_version"], + "fabricNodePEp.json": outputs["vpc_nodes"], + }, + False, + "n9000-16.2(1a).bin", + "aci-apic-dk9.6.0.1a.bin", + "ave_eol_check", + {"username": "admin", "password": "mypassword", "cversion": AciVersion("6.0(1a)"), "tversion": AciVersion("6.2(1a)"), "sw_cversion": AciVersion("6.0(9d)"), "vpc_node_ids": ["101", "102"]}, + ), ], ) -def test_prepare(mock_icurl, is_puv, arg_tversion, expected_result): - checks = script.get_checks(is_puv) - inputs = script.prepare(is_puv, arg_tversion, len(checks)) +def test_prepare(mock_icurl, is_puv, arg_tversion, arg_cversion, debug_function, expected_result): + checks = script.get_checks(is_puv, debug_function) + inputs = script.prepare(is_puv, arg_tversion, arg_cversion, len(checks)) for key, value in expected_result.items(): if "version" in key: assert isinstance(inputs[key], AciVersion) @@ -116,10 +184,24 @@ def test_prepare(mock_icurl, is_puv, arg_tversion, expected_result): assert meta["sw_cversion"] == str(expected_result["sw_cversion"]) assert meta["is_puv"] == is_puv assert meta["total_checks"] == len(checks) + if debug_function: + assert meta["total_checks"] == 1 + + +def test_tversion_invald(): + with pytest.raises(SystemExit): + with pytest.raises(ValueError): + script.prepare(False, "invalid_version", "6.0(1a)", 1) + + +def test_cversion_invald(): + with pytest.raises(SystemExit): + with pytest.raises(ValueError): + script.prepare(False, "6.0(1a)", "invalid_version", 1) @pytest.mark.parametrize( - "icurl_outputs, is_puv, arg_tversion, expected_result", + "icurl_outputs, is_puv, arg_tversion, arg_cversion, debug_function, expected_result", [ # `get_cversion()` failure ( @@ -130,6 +212,8 @@ def test_prepare(mock_icurl, is_puv, arg_tversion, expected_result): }, False, None, + None, + None, """\ Checking current APIC version... @@ -146,6 +230,8 @@ def test_prepare(mock_icurl, is_puv, arg_tversion, expected_result): }, False, None, + None, + None, """\ Gathering Lowest Switch Version from Firmware Repository... @@ -162,6 +248,8 @@ def test_prepare(mock_icurl, is_puv, arg_tversion, expected_result): }, False, None, + None, + None, """\ Collecting VPC Node IDs... @@ -171,12 +259,12 @@ def test_prepare(mock_icurl, is_puv, arg_tversion, expected_result): ), ], ) -def test_prepare_exception(capsys, caplog, mock_icurl, is_puv, arg_tversion, expected_result): +def test_prepare_exception(capsys, caplog, mock_icurl, is_puv, arg_tversion, arg_cversion, debug_function, expected_result): caplog.set_level(logging.CRITICAL) with pytest.raises(SystemExit): with pytest.raises(Exception): - checks = script.get_checks(is_puv) - script.prepare(is_puv, arg_tversion, len(checks)) + checks = script.get_checks(is_puv, debug_function) + script.prepare(is_puv, arg_tversion, arg_cversion, len(checks)) captured = capsys.readouterr() print(captured.out) assert captured.out.endswith(expected_result) From 654c2f18ab5381461a95ec7f8aca47be1c8d8e80 Mon Sep 17 00:00:00 2001 From: tkishida Date: Fri, 11 Jul 2025 22:18:26 -0700 Subject: [PATCH 22/32] Update synthMaintP with the latest schema with ruleId --- aci-preupgrade-validation-script.py | 380 ++++++++++++------------- tests/test_run_checks.py | 2 +- tests/test_synthenticMaintPValidate.py | 67 ++--- 3 files changed, 224 insertions(+), 225 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index e45ce057..0258286a 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -23,6 +23,7 @@ from collections import defaultdict from datetime import datetime from argparse import ArgumentParser +import inspect import shutil import warnings import time @@ -72,17 +73,19 @@ class syntheticMaintPValidate: - def __init__(self, name, description, path=JSON_DIR): + def __init__(self, func_name, name, description, path=JSON_DIR): + self.ruleId = func_name self.name = name self.description = description self.reason = "" - self.criticality = "informational" - self.passed = True - self.recommended_action = "" self.sub_reason = "" + self.recommended_action = "" + self.docUrl = "" + self.severity = "informational" + self.ruleStatus = "passed" # passed|failed self.showValidation = True self.failureDetails = {} - cleaned_name = re.sub(r'[^a-zA-Z0-9_]+|\s+', '_', self.name) + cleaned_name = re.sub(r'[^a-zA-Z0-9_]+|\s+', '_', self.ruleId) self.filename = cleaned_name + '.json' self.path = path @@ -100,52 +103,47 @@ def craftData(column, row): data.append(entry) return data - def updateWithResults(self, result, recommended_action, reason, header, footer, column, row, unformatted_column, unformatted_rows): + def updateWithResults(self, result, recommended_action, reason, doc_url, column, rows, unformatted_column, unformatted_rows): self.reason = reason + self.recommended_action = recommended_action + self.docUrl = doc_url # Show validation if result in [NA, POST]: self.showValidation = False - # Criticality + # Severity if result in [FAIL_O, FAIL_UF]: - self.criticality = "critical" + self.severity = "critical" elif result in [ERROR]: - self.criticality = "major" + self.severity = "major" elif result in [MANUAL]: - self.criticality = "warning" + self.severity = "warning" - # FailureDetails if result not in [NA, PASS]: - self.passed = False - self.recommended_action = recommended_action - self.failureDetails["fail_type"] = result - self.failureDetails["header"] = header - self.failureDetails["footer"] = footer - self.failureDetails["column"] = column - self.failureDetails["row"] = row - self.failureDetails["unformatted_column"] = unformatted_column - self.failureDetails["unformatted_rows"] = unformatted_rows - self.failureDetails["data"] = self.craftData(column, row) + self.ruleStatus = "failed" + self.failureDetails["failType"] = result + self.failureDetails["data"] = self.craftData(column, rows) if unformatted_column and unformatted_rows: - self.failureDetails["data"].extend(self.craftData(unformatted_column, unformatted_rows)) - self.reason += " Parse failure occurred, please check unformatted data in the output data." + self.failureDetails["unformatted_data"] = self.craftData(unformatted_column, unformatted_rows) + self.reason += ( + "\nParse failure occurred for some data, the provided data may not be complete. " + "Please contact Cisco TAC to work on the missing data." + ) def buildResult(self): result = { - "syntheticMaintPValidate": { - "attributes": { - "name": self.name, - "description": self.description, - "reason": self.reason, - "criticality": self.criticality, - "passed": self.passed, - "recommended_action": self.recommended_action, - "sub_reason": self.sub_reason, - "showValidation": self.showValidation, - "failureDetails": self.failureDetails - } - } + "ruleId": self.ruleId, + "name": self.name, + "description": self.description, + "reason": self.reason, + "sub_reason": self.sub_reason, + "recommended_action": self.recommended_action, + "docUrl": self.docUrl, + "severity": self.severity, + "ruleStatus": self.ruleStatus, + "showValidation": self.showValidation, + "failureDetails": self.failureDetails, } return result @@ -1139,18 +1137,18 @@ def print_result(title, result, msg='', unformatted_headers=None, unformatted_data=None, recommended_action='', doc_url='', + func_name='', adjust_title=False, json_output=True): if json_output: - synth = syntheticMaintPValidate(title, "") + synth = syntheticMaintPValidate(func_name, title, "") synth.updateWithResults( result=result, recommended_action=recommended_action, reason=msg, - header="", - footer=doc_url, + doc_url=doc_url, column=headers, - row=data, + rows=data, unformatted_column=unformatted_headers, unformatted_rows=unformatted_data, ) @@ -1365,7 +1363,7 @@ def apic_cluster_health_check(index, total_checks, cversion, **kwargs): elif not data and not unformatted_data: result = PASS print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, - recommended_action, doc_url) + recommended_action, doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -1398,7 +1396,7 @@ def switch_status_check(index, total_checks, **kwargs): msg = 'Switch fabricNode not found!' elif not data: result = PASS - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -1425,7 +1423,7 @@ def maintp_grp_crossing_4_0_check(index, total_checks, cversion, tversion, **kwa data.append([g['maintMaintP']['attributes']['name'], 'Maintenance Group', recommended_action]) else: data.append([g['firmwareFwP']['attributes']['name'], 'Firmware Group', recommended_action]) - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -1459,7 +1457,7 @@ def ntp_status_check(index, total_checks, **kargs): data.append([dn.group('pod'), dn.group('node'), recommended_action]) if not data: result = PASS - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -1503,7 +1501,7 @@ def features_to_disable_check(index, total_checks, cversion, tversion, **kwargs) data.append(['Rogue Endpoint', name, 'Enabled', ra]) if not data: result = PASS - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -1522,7 +1520,8 @@ def switch_group_guideline_check(index, total_checks, **kwargs): result = MANUAL msg = 'No upgrade groups found!' print_result(title, result, msg, headers, data, - recommended_action=recommended_action, doc_url=doc_url) + recommended_action=recommended_action, doc_url=doc_url, + func_name=inspect.currentframe().f_code.co_name) return result spine_type = ['', 'RR ', 'IPN/ISN '] @@ -1619,7 +1618,8 @@ def switch_group_guideline_check(index, total_checks, **kwargs): if not data and not msg: result = PASS print_result(title, result, msg, headers, data, - recommended_action=recommended_action, doc_url=doc_url) + recommended_action=recommended_action, doc_url=doc_url, + func_name=inspect.currentframe().f_code.co_name) return result @@ -1669,7 +1669,7 @@ def switch_bootflash_usage_check(index, total_checks, tversion, **kwargs): if not data: result = PASS msg = 'All below 50% or pre-downloaded' - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -1710,7 +1710,7 @@ def l3out_mtu_check(index, total_checks, **kwargs): if not data and not unformatted_data: result = NA msg = 'No L3Out Interfaces found' - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -1740,7 +1740,7 @@ def port_configured_as_l2_check(index, total_checks, **kwargs): [fc, faultDelegate['faultDelegate']['attributes']['dn'], recommended_action]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -1772,7 +1772,7 @@ def port_configured_as_l3_check(index, total_checks, **kwargs): [fc, faultDelegate['faultDelegate']['attributes']['dn'], recommended_action]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -1795,7 +1795,7 @@ def prefix_already_in_use_check(index, total_checks, **kwargs): filter = '?query-target-filter=and(wcard(faultInst.changeSet,"prefix-entry-already-in-use"),wcard(faultInst.dn,"uni/epp/rtd"))' faultInsts = icurl("class", "faultInst.json" + filter) if not faultInsts: - print_result(title, PASS) + print_result(title, PASS, func_name=inspect.currentframe().f_code.co_name) return PASS vnid2vrf = {} @@ -1837,7 +1837,7 @@ def prefix_already_in_use_check(index, total_checks, **kwargs): data = [["F0467", epg] for epg in conflicts["_"]["_"]["faulted_extepgs"]] if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers_old, data, unformatted_headers, unformatted_data, recommended_action) + print_result(title, result, msg, headers_old, data, unformatted_headers, unformatted_data, recommended_action, func_name=inspect.currentframe().f_code.co_name) return result # Proceed further only for new versions with VRF/prefix data in faults @@ -1881,7 +1881,7 @@ def prefix_already_in_use_check(index, total_checks, **kwargs): if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action, func_name=inspect.currentframe().f_code.co_name) return result @@ -1943,7 +1943,8 @@ def encap_already_in_use_check(index, total_checks, **kwargs): if not data and not unformatted_data: result = PASS print_result(title, result, msg, headers, data, - unformatted_headers, unformatted_data, recommended_action=recommended_action) + unformatted_headers, unformatted_data, recommended_action=recommended_action, + func_name=inspect.currentframe().f_code.co_name) return result @@ -1972,7 +1973,7 @@ def bd_subnet_overlap_check(index, total_checks, **kwargs): unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn'], recommended_action]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2003,7 +2004,7 @@ def bd_duplicate_subnet_check(index, total_checks, **kwargs): recommended_action]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2050,7 +2051,7 @@ def hw_program_fail_check(index, total_checks, cversion, **kwargs): if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2087,7 +2088,7 @@ def switch_ssd_check(index, total_checks, **kwargs): recommended_action.get(fc, 'Resolve the fault')]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2113,7 +2114,7 @@ def apic_ssd_check(index, total_checks, cversion, username, password, **kwargs): controller = icurl('class', 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")') report_other = False if not controller: - print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?') + print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?', func_name=inspect.currentframe().f_code.co_name) return ERROR else: checked_apics = {} @@ -2175,7 +2176,7 @@ def apic_ssd_check(index, total_checks, cversion, username, password, **kwargs): result = ERROR elif not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, adjust_title=adjust_title) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, adjust_title=adjust_title, func_name=inspect.currentframe().f_code.co_name) return result @@ -2203,7 +2204,7 @@ def port_configured_for_apic_check(index, total_checks, **kwargs): unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn'], recommended_action]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2224,7 +2225,7 @@ def overlapping_vlan_pools_check(index, total_checks, **kwargs): infraSetPols = icurl('mo', 'uni/infra/settings.json') if infraSetPols[0]['infraSetPol']['attributes'].get('validateOverlappingVlans') in ['true', 'yes']: msg = '`Enforce EPG VLAN Validation` is enabled. No need to check overlapping VLANs' - print_result(title, result, msg) + print_result(title, result, msg, func_name=inspect.currentframe().f_code.co_name) return result # Get VLAN pools and ports from access policy @@ -2391,7 +2392,7 @@ def overlapping_vlan_pools_check(index, total_checks, **kwargs): impact, ]) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -2418,7 +2419,7 @@ def scalability_faults_check(index, total_checks, **kwargs): unformatted_data.append([f['code'], f['dn'], f['descr'], recommended_action]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2456,7 +2457,7 @@ def apic_disk_space_faults_check(index, total_checks, cversion, **kwargs): unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn'], default_action]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2486,7 +2487,7 @@ def l3out_route_map_direction_check(index, total_checks, **kwargs): data.append(basic + [rmap, dir, recommended_action.format(dir)]) if not data: result = PASS - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2502,7 +2503,7 @@ def l3out_route_map_missing_target_check(index, total_checks, cversion, tversion print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL def is_old(v): @@ -2511,7 +2512,7 @@ def is_old(v): c_is_old = is_old(cversion) t_is_old = is_old(tversion) if (c_is_old and t_is_old) or (not c_is_old and not t_is_old): - print_result(title, NA) + print_result(title, NA, func_name=inspect.currentframe().f_code.co_name) return NA dn_regex = r'uni/tn-(?P[^/]+)/out-(?P[^/]+)/' @@ -2540,7 +2541,7 @@ def is_old(v): ]) if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -2667,7 +2668,7 @@ def l3out_overlapping_loopback_check(index, total_checks, **kwargs): ]) if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -2709,7 +2710,7 @@ def bgp_peer_loopback_check(index, total_checks, **kwargs): dn.group('pod'), dn.group('node'), recommended_action]) if not data: result = PASS - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2736,7 +2737,7 @@ def lldp_with_infra_vlan_mismatch_check(index, total_checks, **kwargs): unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn'], recommended_action]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2750,7 +2751,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** recommended_action = 'Delete the firmware from APIC and re-download' print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.') + print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) return MANUAL image_validaton = True @@ -2765,7 +2766,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** image_validaton = False if not image_validaton: - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result md5s = [] @@ -2842,7 +2843,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** result = ERROR elif not data: result = PASS - print_result(title, result, msg, headers, data, adjust_title=True) + print_result(title, result, msg, headers, data, adjust_title=True, func_name=inspect.currentframe().f_code.co_name) return result @@ -2899,7 +2900,7 @@ def standby_apic_disk_space_check(index, total_checks, **kwargs): elif not data: result = PASS msg = 'all below {}%'.format(threshold) - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2916,7 +2917,7 @@ def r_leaf_compatibility_check(index, total_checks, tversion, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.') + print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) return MANUAL remote_leafs = icurl('class', 'fabricNode.json?&query-target-filter=eq(fabricNode.nodeType,"remote-leaf-wan")') @@ -2940,7 +2941,7 @@ def r_leaf_compatibility_check(index, total_checks, tversion, **kwargs): if ra: result = FAIL_O data.append([str(tversion), "Present", direct_enabled, ra]) - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -2976,7 +2977,7 @@ def ep_announce_check(index, total_checks, cversion, tversion, **kwargs): if current_version_affected and target_version_affected: result = FAIL_O data.append(['CSCvi76161', recommended_action]) - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -3001,7 +3002,7 @@ def vmm_controller_status_check(index, total_checks, **kwargs): result = FAIL_O data.append([domName, hostOrIp, "offline", recommended_action]) - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -3038,7 +3039,7 @@ def vmm_controller_adj_check(index, total_checks, **kwargs): [adj['faultInst']['attributes']['code'], adj['faultInst']['attributes']['dn'], recommended_action]) - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) return result @@ -3068,7 +3069,7 @@ def vpc_paired_switches_check(index, total_checks, vpc_node_ids=None, **kwargs): if not data: result = PASS - print_result(title, result, msg, headers, data, doc_url=doc_url) + print_result(title, result, msg, headers, data, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3111,7 +3112,7 @@ def cimc_compatibilty_check(index, total_checks, tversion, **kwargs): result = MANUAL msg = 'Target version not supplied. Skipping.' - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3146,7 +3147,7 @@ def intersight_upgrade_status_check(index, total_checks, **kwargs): result = NA msg = 'Intersight Device Connector not responding' - print_result(title, result, msg, headers, data, doc_url=doc_url) + print_result(title, result, msg, headers, data, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3189,7 +3190,7 @@ def isis_redis_metric_mpod_msite_check(index, total_checks, **kwargs): data.append([redistribMetric, mpod, msite, recommended_action]) if not data: result = PASS - print_result(title, result, msg, headers, data, doc_url=doc_url) + print_result(title, result, msg, headers, data, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3204,7 +3205,7 @@ def bgp_golf_route_target_type_check(index, total_checks, cversion=None, tversio print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.') + print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) return MANUAL if cversion.older_than("4.2(1a)") and tversion.newer_than("4.2(1a)"): @@ -3226,7 +3227,7 @@ def bgp_golf_route_target_type_check(index, total_checks, cversion=None, tversio if not data: result = PASS - print_result(title, result, msg, headers, data, doc_url=doc_url) + print_result(title, result, msg, headers, data, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3256,7 +3257,7 @@ def docker0_subnet_overlap_check(index, total_checks, **kwargs): result = FAIL_UF data.append([tep, bip, recommended_action]) - print_result(title, result, msg, headers, data) + print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) return result @@ -3278,7 +3279,7 @@ def eventmgr_db_defect_check(index, total_checks, cversion, **kwargs): if data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3309,7 +3310,7 @@ def target_version_compatibility_check(index, total_checks, cversion, tversion, if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3339,7 +3340,7 @@ def gen1_switch_compatibility_check(index, total_checks, tversion, **kwargs): if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3362,7 +3363,7 @@ def contract_22_defect_check(index, total_checks, cversion, tversion, **kwargs): result = FAIL_O data.append(["CSCvz65560", "Target Version susceptible to Defect"]) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3378,11 +3379,11 @@ def llfc_susceptibility_check(index, total_checks, cversion=None, tversion=None, print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.') + print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) return MANUAL if not vpc_node_ids: - print_result(title, result, 'No VPC Nodes found. Not susceptible.') + print_result(title, result, 'No VPC Nodes found. Not susceptible.', func_name=inspect.currentframe().f_code.co_name) return result # Check for Fiber 1000base-SX, CSCvv33100 @@ -3414,7 +3415,7 @@ def llfc_susceptibility_check(index, total_checks, cversion=None, tversion=None, if data: result = MANUAL - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3429,11 +3430,11 @@ def telemetryStatsServerP_object_check(index, total_checks, sw_cversion=None, tv print_title(title, index, total_checks) if not sw_cversion: - print_result(title, MANUAL, "Current switch version not found. Check switch health.") + print_result(title, MANUAL, "Current switch version not found. Check switch health.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if not tversion: - print_result(title, MANUAL, 'Current or target Switch version not supplied. Skipping.') + print_result(title, MANUAL, 'Current or target Switch version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) return MANUAL if sw_cversion.older_than("4.2(4d)") and tversion.newer_than("5.2(2d)"): @@ -3443,7 +3444,7 @@ def telemetryStatsServerP_object_check(index, total_checks, sw_cversion=None, tv result = FAIL_O data.append([str(sw_cversion), str(tversion), 'telemetryStatsServerP.collectorLocation = "apic" Found']) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3458,7 +3459,7 @@ def internal_vlanpool_check(index, total_checks, tversion=None, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.') + print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) return MANUAL if tversion.newer_than("4.2(6a)"): @@ -3507,7 +3508,7 @@ def internal_vlanpool_check(index, total_checks, tversion=None, **kwargs): if [vlanInstP_name, ', '.join(encap_blk_dict[vlanInstP_name]), vmmDomP["vmmDomP"]["attributes"]["dn"], 'VLANs in this Block will be removed from switch Front-Panel if not corrected'] not in data: data.append([vlanInstP_name, ', '.join(encap_blk_dict[vlanInstP_name]), vmmDomP["vmmDomP"]["attributes"]["dn"], 'VLANs in this Block will be removed from switch Front-Panel if not corrected']) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3567,7 +3568,7 @@ def apic_ca_cert_validation(index, total_checks, **kwargs): genrsa_proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) genrsa_proc.communicate()[0].strip() if genrsa_proc.returncode != 0: - print_result(title, ERROR, 'openssl cmd issue, send logs to TAC') + print_result(title, ERROR, 'openssl cmd issue, send logs to TAC', func_name=inspect.currentframe().f_code.co_name) return ERROR # Prep certreq @@ -3595,7 +3596,7 @@ def apic_ca_cert_validation(index, total_checks, **kwargs): if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3611,13 +3612,13 @@ def fabricdomain_name_check(index, total_checks, cversion, tversion, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.') + print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) return MANUAL if tversion.same_as("6.0(2h)"): controller = icurl('class', 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")') if not controller: - print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?') + print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?', func_name=inspect.currentframe().f_code.co_name) return ERROR fabricDomain = controller[0]['topSystem']['attributes']['fabricDomain'] @@ -3626,7 +3627,7 @@ def fabricdomain_name_check(index, total_checks, cversion, tversion, **kwargs): if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3643,7 +3644,7 @@ def sup_hwrev_check(index, total_checks, cversion, tversion, **kwargs): fpga_concern = False if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.') + print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) return MANUAL if cversion.older_than("5.2(8f)"): @@ -3661,7 +3662,7 @@ def sup_hwrev_check(index, total_checks, cversion, tversion, **kwargs): sup_re = r'/.+(?Psupslot-\d+)' sups = icurl('class', 'eqptSpCmnBlk.json?&query-target-filter=wcard(eqptSpromSupBlk.dn,"sup")') if not sups: - print_result(title, ERROR, 'No sups found. This is unlikely.') + print_result(title, ERROR, 'No sups found. This is unlikely.', func_name=inspect.currentframe().f_code.co_name) return ERROR for sup in sups: @@ -3675,7 +3676,7 @@ def sup_hwrev_check(index, total_checks, cversion, tversion, **kwargs): if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3691,7 +3692,7 @@ def uplink_limit_check(index, total_checks, cversion, tversion, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.') + print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) return MANUAL if cversion.older_than("6.0(1a)") and tversion.newer_than("6.0(1a)"): @@ -3710,7 +3711,7 @@ def uplink_limit_check(index, total_checks, cversion, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3730,7 +3731,7 @@ def oob_mgmt_security_check(index, total_checks, cversion, tversion, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL affected_versions = ["4.2(7)", "5.2(1)", "5.2(2)"] @@ -3738,7 +3739,7 @@ def oob_mgmt_security_check(index, total_checks, cversion, tversion, **kwargs): cversion.simple_version in affected_versions and tversion.simple_version in affected_versions ): - print_result(title, NA) + print_result(title, NA, func_name=inspect.currentframe().f_code.co_name) return NA # ACI Node EPGs (providers) @@ -3778,7 +3779,7 @@ def oob_mgmt_security_check(index, total_checks, cversion, tversion, **kwargs): if data: result = MANUAL - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3794,13 +3795,13 @@ def mini_aci_6_0_2_check(index, total_checks, cversion, tversion, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.') + print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) return MANUAL if cversion.older_than("6.0(2a)") and tversion.newer_than("6.0(2a)"): topSystem = icurl('class', 'topSystem.json?query-target-filter=wcard(topSystem.role,"controller")') if not topSystem: - print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?') + print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?', func_name=inspect.currentframe().f_code.co_name) return ERROR for controller in topSystem: if controller['topSystem']['attributes']['nodeType'] == "virtual": @@ -3810,7 +3811,7 @@ def mini_aci_6_0_2_check(index, total_checks, cversion, tversion, **kwargs): if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3826,7 +3827,7 @@ def sup_a_high_memory_check(index, total_checks, tversion, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL affected_versions = ["6.0(3)", "6.0(4)", "6.0(5)"] @@ -3843,7 +3844,7 @@ def sup_a_high_memory_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3879,7 +3880,7 @@ def access_untagged_check(index, total_checks, **kwargs): if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action="", doc_url=doc_url) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action="", doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3931,7 +3932,7 @@ def post_upgrade_cb_check(index, total_checks, cversion, tversion, **kwargs): }, } if not tversion or (tversion and cversion.older_than(str(tversion))): - print_result(title, POST, 'Re-run script after APICs are upgraded and back to Fully-Fit') + print_result(title, POST, 'Re-run script after APICs are upgraded and back to Fully-Fit', func_name=inspect.currentframe().f_code.co_name) return POST for new_mo in new_mo_dict: @@ -3963,7 +3964,7 @@ def post_upgrade_cb_check(index, total_checks, cversion, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -3987,7 +3988,7 @@ def eecdh_cipher_check(index, total_checks, cversion, **kwargs): if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4011,7 +4012,7 @@ def vmm_active_uplinks_check(index, total_checks, **kwargs): # Pre 4.x did not have this class msg = 'cversion does not have class fvUplinkOrderCont' result = NA - print_result(title, result, msg) + print_result(title, result, msg, func_name=inspect.currentframe().f_code.co_name) return result if affected_uplinks: @@ -4020,7 +4021,7 @@ def vmm_active_uplinks_check(index, total_checks, **kwargs): dn = re.search(vmm_epg_regex, uplink['fvUplinkOrderCont']['attributes']['dn']) data.append([dn.group("tenant"), dn.group("ap"), dn.group("epg"), dn.group("dom")]) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4057,7 +4058,7 @@ def fabric_port_down_check(index, total_checks, **kwargs): if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4072,7 +4073,7 @@ def fabric_dpp_check(index, total_checks, tversion, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL lbpol_api = 'lbpPol.json' @@ -4087,7 +4088,7 @@ def fabric_dpp_check(index, total_checks, tversion, **kwargs): result = FAIL_O data.append(["CSCwf05073", "Target Version susceptible to Defect"]) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4102,7 +4103,7 @@ def n9k_c93108tc_fx3p_interface_down_check(index, total_checks, tversion, **kwar print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if ( @@ -4123,7 +4124,7 @@ def n9k_c93108tc_fx3p_interface_down_check(index, total_checks, tversion, **kwar if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4143,7 +4144,7 @@ def subnet_scope_check(index, total_checks, cversion, **kwargs): fvAEPg = icurl('class', epg_api) if not fvAEPg: - print_result(title, NA, "0 EPG Subnets found. Skipping.") + print_result(title, NA, "0 EPG Subnets found. Skipping.", func_name=inspect.currentframe().f_code.co_name) return NA bd_api = 'fvBD.json' @@ -4186,7 +4187,7 @@ def subnet_scope_check(index, total_checks, cversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4201,7 +4202,7 @@ def rtmap_comm_match_defect_check(index, total_checks, tversion, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if (tversion.major1 == "5" and tversion.major2 == "2" and tversion.older_than("5.2(8a)")): @@ -4244,7 +4245,7 @@ def rtmap_comm_match_defect_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4318,7 +4319,7 @@ def fabricPathEp_target_check(index, total_checks, **kwargs): if data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4333,7 +4334,7 @@ def lldp_custom_int_description_defect_check(index, total_checks, tversion, **kw print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if tversion.major1 == '6' and tversion.older_than('6.0(3a)'): @@ -4344,7 +4345,7 @@ def lldp_custom_int_description_defect_check(index, total_checks, tversion, **kw result = FAIL_O data.append(['CSCwf00416']) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4359,11 +4360,11 @@ def unsupported_fec_configuration_ex_check(index, total_checks, sw_cversion, tve print_title(title, index, total_checks) if not sw_cversion: - print_result(title, MANUAL, "Current switch version not found. Check switch health.") + print_result(title, MANUAL, "Current switch version not found. Check switch health.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if not tversion: - print_result(title, MANUAL, "Target switch version not supplied. Skipping.") + print_result(title, MANUAL, "Target switch version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if sw_cversion.older_than('5.0(1a)') and tversion.newer_than("5.0(1a)"): @@ -4390,7 +4391,7 @@ def unsupported_fec_configuration_ex_check(index, total_checks, sw_cversion, tve if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4407,7 +4408,7 @@ def static_route_overlap_check(index, total_checks, cversion, tversion, **kwargs bd_subnet_regex = r'uni/tn-(?P[^/]+)/BD-(?P[^/]+)/subnet-\[(?P[^/]+/\d{2})\]' if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.') + print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) return MANUAL if (cversion.older_than("5.2(6e)") and tversion.newer_than("5.0(1a)") and tversion.older_than("5.2(6e)")): @@ -4455,7 +4456,7 @@ def static_route_overlap_check(index, total_checks, cversion, tversion, **kwargs if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4470,11 +4471,11 @@ def vzany_vzany_service_epg_check(index, total_checks, cversion, tversion, **kwa print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if not (cversion.older_than("5.0(1a)") and tversion.newer_than("5.0(1a)")): - print_result(title, NA) + print_result(title, NA, func_name=inspect.currentframe().f_code.co_name) return NA tn_regex = r"uni/tn-(?P[^/]+)" @@ -4487,7 +4488,7 @@ def vzany_vzany_service_epg_check(index, total_checks, cversion, tversion, **kwa for vzRsSubjGraphAtt in vzRsSubjGraphAtts: graphAtt_rns = vzRsSubjGraphAtt["vzRsSubjGraphAtt"]["attributes"]["dn"].split("/") if len(graphAtt_rns) < 3: - print_result(title, ERROR, "Failed to get contract DN from vzRsSubjGraphAtt DN.") + print_result(title, ERROR, "Failed to get contract DN from vzRsSubjGraphAtt DN.", func_name=inspect.currentframe().f_code.co_name) return ERROR # Get vzAny(VRF) relations of the contract. There can be multiple VRFs per contract. @@ -4527,7 +4528,7 @@ def vzany_vzany_service_epg_check(index, total_checks, cversion, tversion, **kwa data.append([vrf, contract, sg]) if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4542,11 +4543,11 @@ def validate_32_64_bit_image_check(index, total_checks, cversion, tversion, **kw print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if cversion.older_than("6.0(2a)") and tversion.newer_than("6.0(2a)"): - print_result(title, POST, 'Re-run after APICs are upgraded to 6.0(2) or later') + print_result(title, POST, 'Re-run after APICs are upgraded to 6.0(2) or later', func_name=inspect.currentframe().f_code.co_name) return POST if cversion.newer_than("6.0(2a)") and tversion.newer_than("6.0(2a)"): @@ -4578,7 +4579,7 @@ def validate_32_64_bit_image_check(index, total_checks, cversion, tversion, **kw result = NA msg = 'Target version below 6.0(2)' - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4657,7 +4658,7 @@ def fabric_link_redundancy_check(index, total_checks, **kwargs): elif not sp_missing and t1_missing: recommended_action = t1_recommended_action - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4674,7 +4675,7 @@ def cloudsec_encryption_depr_check(index, total_checks, tversion, **kwargs): cloudsec_api = 'cloudsecPreSharedKey.json' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL try: @@ -4682,7 +4683,7 @@ def cloudsec_encryption_depr_check(index, total_checks, tversion, **kwargs): except OldVerClassNotFound: msg = 'cversion does not have class cloudsecPreSharedKey' result = NA - print_result(title, result, msg) + print_result(title, result, msg, func_name=inspect.currentframe().f_code.co_name) return result if tversion.newer_than("6.0(6a)"): @@ -4694,7 +4695,7 @@ def cloudsec_encryption_depr_check(index, total_checks, tversion, **kwargs): result = MANUAL else: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4724,7 +4725,7 @@ def out_of_service_ports_check(index, total_checks, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4743,7 +4744,7 @@ def fc_ex_model_check(index, total_checks, tversion, **kwargs): fabricNode_api += '?query-target-filter=wcard(fabricNode.model,".*EX")' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if (tversion.newer_than("6.0(7a)") and tversion.older_than("6.0(9c)")) or tversion.same_as("6.1(1f)"): @@ -4763,7 +4764,7 @@ def fc_ex_model_check(index, total_checks, tversion, **kwargs): data.append([node_dn, model]) if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4792,7 +4793,7 @@ def tep_to_tep_ac_counter_check(index, total_checks, **kwargs): if data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4836,7 +4837,7 @@ def clock_signal_component_failure_check(index, total_checks, **kwargs): result = MANUAL recommended_action += sn_string[:-1] - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4857,7 +4858,7 @@ def stale_decomissioned_spine_check(index, total_checks, tversion, **kwargs): active_spine_api += '?query-target-filter=eq(topSystem.role,"spine")' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if tversion.newer_than("5.2(3d)") and tversion.older_than("6.0(3d)"): @@ -4874,7 +4875,7 @@ def stale_decomissioned_spine_check(index, total_checks, tversion, **kwargs): data.append([node_id, name, state]) if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4892,7 +4893,7 @@ def n9408_model_check(index, total_checks, tversion, **kwargs): eqptCh_api += '?query-target-filter=eq(eqptCh.model,"N9K-C9400-SW-GX2A")' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if tversion.newer_than("6.1(3a)"): @@ -4904,7 +4905,7 @@ def n9408_model_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4924,7 +4925,7 @@ def pbr_high_scale_check(index, total_checks, tversion, **kwargs): count_filter = '?rsp-subtree-include=count' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if tversion.older_than("5.3(2c)"): @@ -4940,7 +4941,7 @@ def pbr_high_scale_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -4957,10 +4958,10 @@ def https_throttle_rate_check(index, total_checks, cversion, tversion, **kwargs) # Applicable only when crossing 6.1(2) as upgrade instead of downgrade. if cversion.newer_than("6.1(2a)"): - print_result(title, NA) + print_result(title, NA, func_name=inspect.currentframe().f_code.co_name) return NA if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL commHttpses = icurl("class", "commHttps.json") @@ -4989,7 +4990,7 @@ def https_throttle_rate_check(index, total_checks, cversion, tversion, **kwargs) recommended_action = "6.1(2)+ will reject this config. " + recommended_action else: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -5008,7 +5009,7 @@ def standby_sup_sync_check(index, total_checks, cversion, tversion, **kwargs): eqptSupC_api += '?query-target-filter=eq(eqptSupC.rdSt,"standby")' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if ( @@ -5028,7 +5029,7 @@ def standby_sup_sync_check(index, total_checks, cversion, tversion, **kwargs): if data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -5069,7 +5070,7 @@ def equipment_disk_limits_exceeded(index, total_checks, **kwargs): if data or unformatted_data: result = FAIL_UF - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action, doc_url) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action, doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -5088,7 +5089,7 @@ def aes_encryption_check(index, total_checks, tversion, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if tversion.newer_than("6.1(2a)"): @@ -5107,7 +5108,7 @@ def aes_encryption_check(index, total_checks, tversion, **kwargs): else: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -5128,11 +5129,11 @@ def service_bd_forceful_routing_check(index, total_checks, cversion, tversion, * print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if not (cversion.older_than("6.0(2a)") and tversion.newer_than("6.0(2a)")): - print_result(title, NA) + print_result(title, NA, func_name=inspect.currentframe().f_code.co_name) return NA dn_regex = r"uni/tn-(?P[^/]+)/BD-(?P[^/]+)/" @@ -5152,7 +5153,7 @@ def service_bd_forceful_routing_check(index, total_checks, cversion, tversion, * if data or unformatted_data: result = MANUAL - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action, doc_url) + print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action, doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -5172,7 +5173,7 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): controllers = icurl('class', topSystem_api) if not controllers: - print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?') + print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?', func_name=inspect.currentframe().f_code.co_name) return ERROR has_error = False prints('') @@ -5217,7 +5218,7 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): result = ERROR elif data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, adjust_title=True) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, adjust_title=True, func_name=inspect.currentframe().f_code.co_name) return result @@ -5235,7 +5236,7 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): ave_api += '?query-target-filter=eq(vmmDomP.enableAVE,"true")' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if tversion.newer_than("6.0(1a)"): @@ -5246,7 +5247,7 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -5262,7 +5263,7 @@ def stale_pcons_ra_mo_check(index, total_checks, cversion, tversion, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if cversion.older_than("6.0(3d)") and tversion.newer_than("6.0(3c)") and tversion.older_than("6.1(4a)"): @@ -5297,12 +5298,12 @@ def stale_pcons_ra_mo_check(index, total_checks, cversion, tversion, **kwargs): if pcons_ra_dn_mo: data.append([pcons_ra_dn, policy_dn]) else: - print_result(title, NA, "Target version not supplied or not applicable. Skipping.") + print_result(title, NA, "Target version not supplied or not applicable. Skipping.", func_name=inspect.currentframe().f_code.co_name) return NA if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -5317,7 +5318,7 @@ def isis_database_byte_check(index, total_checks, tversion, **kwargs): print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.") + print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) return MANUAL if tversion.newer_than("6.1(1a)") and tversion.older_than("6.1(3g)"): @@ -5348,10 +5349,10 @@ def isis_database_byte_check(index, total_checks, tversion, **kwargs): break else: - print_result(title, NA, "Target version not affected") + print_result(title, NA, "Target version not affected", func_name=inspect.currentframe().f_code.co_name) return NA - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url) + print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) return result @@ -5529,14 +5530,11 @@ def run_checks(checks, inputs): prints('\n\n!!! KeyboardInterrupt !!!\n') break except Exception as e: - # synth.writeResult() uses the first arg in `print_result()` (i.e. title) as - # the filename. When a check has an error and ends up here, we don't know the - # title and we cannot use the error message as the title/filename either. - # Thus, using the func name of the check as the title/filename. + func_name = check.__name__ prints('') - print_title(" " * len(check.__name__)) # not showing the func name in the stdout + print_title(" " * len(func_name)) # not showing the func name in the stdout msg = 'Unexpected Error: %s' % e - print_result(check.__name__, ERROR, msg) + print_result(func_name, ERROR, msg, func_name=func_name) summary[ERROR] += 1 logging.exception(e) diff --git a/tests/test_run_checks.py b/tests/test_run_checks.py index ebf76ba2..1483c6f0 100644 --- a/tests/test_run_checks.py +++ b/tests/test_run_checks.py @@ -12,7 +12,7 @@ def _check(index, total_checks, **kwargs): if result == script.ERROR: raise Exception("This is a test exception to result in `script.ERROR`.") else: - script.print_result(title, result) + script.print_result(title, result, func_name=func_name) return result return _check diff --git a/tests/test_synthenticMaintPValidate.py b/tests/test_synthenticMaintPValidate.py index 1a8e628f..221415a8 100644 --- a/tests/test_synthenticMaintPValidate.py +++ b/tests/test_synthenticMaintPValidate.py @@ -6,171 +6,171 @@ @pytest.mark.parametrize( - "name, description, result, recommended_action, reason, header, footer, column, row, unformatted_column, unformatted_rows, expected_show, expected_criticality, expected_passed", + "func_name, name, description, result, recommended_action, reason, doc_url, column, row, unformatted_column, unformatted_rows, expected_show, expected_criticality, expected_passed", [ # Check 1: NA ( + "fake_func_name_NA_test", "NA", "", script.NA, "", "", "", - "", ["col1", "col2"], [["row1", "row2"], ["row3", "row4"]], ["col1", "col2"], [["row1", "row2"], ["row3", "row4"]], False, "informational", - True + "passed" ), # Check 2: PASS ( + "fake_func_name_PASS_test", "PASS", "", script.PASS, "", "", "", - "", [], [], [], [], True, "informational", - True + "passed" ), # Check 3: POST ( + "fake_func_name_POST_test", "POST", "", script.POST, "reboot", "test reason", - "test header", - "test footer", + "https://test_doc_url.html", ["col1", "col2"], [["row1", "row2"], ["row3", "row4"]], ["col1", "col2"], [["row1", "row2"], ["row3", "row4"]], False, "informational", - False + "failed" ), # Check 4: MANUAL ( + "fake_func_name_MANUAL_test", "MANUAL", "", script.MANUAL, "reboot", "test reason", - "test header", - "test footer", + "https://test_doc_url.html", ["col1", "col2"], [["row1", "row2"], ["row3", "row4"]], ["col1", "col2"], [["row1", "row2"], ["row3", "row4"]], True, "warning", - False + "failed" ), # Check 5: ERROR ( + "fake_func_name_ERROR_test", "ERROR", "", script.ERROR, "reboot", "test reason", - "test header", - "test footer", + "https://test_doc_url.html", ["col1", "col2"], [["row1", "row2"], ["row3", "row4"]], ["col1", "col2"], [["row1", "row2"], ["row3", "row4"]], True, "major", - False + "failed" ), # Check 6: FAIL_UF ( + "fake_func_name_FAIL_UF_test", "FAIL_UF", "", script.FAIL_UF, "reboot", "test reason", - "test header", - "test footer", + "https://test_doc_url.html", ["col1", "col2"], [["row1", "row2"], ["row3", "row4"]], ["col1", "col2"], [["row1", "row2"], ["row3", "row4"]], True, "critical", - False + "failed" ), # Check 7: FAIL_O ( + "fake_func_name_FAIL_O_test", "FAIL_O", "", script.FAIL_O, "reboot", "test reason", - "test header", - "test footer", + "https://test_doc_url.html", ["col1", "col2", "col3"], [["row1", "row2", "row3"], ["row4", "row5", "row6"]], ["col4", "col5"], [["row1", "row2"], ["row3", "row4"]], True, "critical", - False + "failed" ), # Check 8: FAIL_O Formatted only ( + "fake_func_name_FAIL_O_formatted_only_test", "FAIL_O Formatted only", "", script.FAIL_O, "reboot", "test reason", - "test header", - "test footer", + "https://test_doc_url.html", ["col1", "col2", "col3"], [["row1", "row2", "row3"], ["row4", "row5", "row6"]], [], [], True, "critical", - False + "failed" ), # Check 9: FAIL_O ( + "fake_func_name_FAIL_O_unformatted_only_test", "FAIL_O Unformatted only", "", script.FAIL_O, "reboot", "test reason", - "test header", - "test footer", + "https://test_doc_url.html", [], [], ["col1", "col2", "col3"], [["row1", "row2", "row3"], ["row4", "row5", "row6"]], True, "critical", - False + "failed" ), ], ) def test_syntheticMaintPValidate( + func_name, name, description, result, recommended_action, reason, - header, - footer, + doc_url, column, row, unformatted_column, @@ -179,11 +179,12 @@ def test_syntheticMaintPValidate( expected_criticality, expected_passed, ): - synth = script.syntheticMaintPValidate(name, description) - synth.updateWithResults(result, recommended_action, reason, header, footer, column, row, unformatted_column, unformatted_rows) + synth = script.syntheticMaintPValidate(func_name, name, description) + synth.updateWithResults(result, recommended_action, reason, doc_url, column, row, unformatted_column, unformatted_rows) file = synth.writeResult() with open(file, "r") as f: data = json.load(f) - assert data["syntheticMaintPValidate"]["attributes"]["showValidation"] == expected_show - assert data["syntheticMaintPValidate"]["attributes"]["criticality"] == expected_criticality - assert data["syntheticMaintPValidate"]["attributes"]["passed"] == expected_passed + assert data["ruleId"] == func_name + assert data["showValidation"] == expected_show + assert data["severity"] == expected_criticality + assert data["ruleStatus"] == expected_passed From cebba59d917bf0ad8f7e1532120b6a8cda4d8efc Mon Sep 17 00:00:00 2001 From: tkishida Date: Sat, 12 Jul 2025 23:20:12 -0700 Subject: [PATCH 23/32] Add decorator `@check_wrapper` for all check functions A new decorator `@check_wrapper` is to move most of the I/O functionalities, such as printing and writing the result into a file, outside of each check function so that each check can focus on the validation logic itself by minimizing the impact from a requirement change in the output format and so on. To support this, a new class `Result` is also introduced to make it clear what a check function is expected to return. As long as `Result` class is returned, the decorator `@check_wrapper` handles the printing them to stdout and files. --- aci-preupgrade-validation-script.py | 1876 +++++++++++------------- tests/test_run_checks.py | 152 +- tests/test_synthenticMaintPValidate.py | 17 + 3 files changed, 1004 insertions(+), 1041 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 0258286a..39f6e5a8 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -16,14 +16,14 @@ from __future__ import division from __future__ import print_function -from six import iteritems +from six import iteritems, text_type from six.moves import input from textwrap import TextWrapper from getpass import getpass from collections import defaultdict from datetime import datetime from argparse import ArgumentParser -import inspect +import functools import shutil import warnings import time @@ -36,6 +36,7 @@ import re SCRIPT_VERSION = "v2.6.0" +# result constants DONE = 'DONE' PASS = 'PASS' FAIL_O = 'FAIL - OUTAGE WARNING!!' @@ -44,6 +45,10 @@ MANUAL = 'MANUAL CHECK REQUIRED' POST = 'POST UPGRADE CHECK REQUIRED' NA = 'N/A' +# message constants +TVER_MISSING = "Target version not supplied. Skipping." +VER_NOT_AFFECTED = "Version not affected." +# regex constants node_regex = r'topology/pod-(?P\d+)/node-(?P\d+)' port_regex = node_regex + r'/sys/phys-\[(?P.+)\]' path_regex = ( @@ -72,89 +77,6 @@ warnings.simplefilter(action='ignore', category=FutureWarning) -class syntheticMaintPValidate: - def __init__(self, func_name, name, description, path=JSON_DIR): - self.ruleId = func_name - self.name = name - self.description = description - self.reason = "" - self.sub_reason = "" - self.recommended_action = "" - self.docUrl = "" - self.severity = "informational" - self.ruleStatus = "passed" # passed|failed - self.showValidation = True - self.failureDetails = {} - cleaned_name = re.sub(r'[^a-zA-Z0-9_]+|\s+', '_', self.ruleId) - self.filename = cleaned_name + '.json' - self.path = path - - @staticmethod - def craftData(column, row): - data = [] - if isinstance(row, list) and isinstance(column, list): - for i in range(len(row)): - entry = {} - for j in range(len(column)): - if j < len(row[i]): - entry[column[j]] = row[i][j] - else: - entry[column[j]] = None - data.append(entry) - return data - - def updateWithResults(self, result, recommended_action, reason, doc_url, column, rows, unformatted_column, unformatted_rows): - self.reason = reason - self.recommended_action = recommended_action - self.docUrl = doc_url - - # Show validation - if result in [NA, POST]: - self.showValidation = False - - # Severity - if result in [FAIL_O, FAIL_UF]: - self.severity = "critical" - elif result in [ERROR]: - self.severity = "major" - elif result in [MANUAL]: - self.severity = "warning" - - if result not in [NA, PASS]: - self.ruleStatus = "failed" - self.failureDetails["failType"] = result - self.failureDetails["data"] = self.craftData(column, rows) - if unformatted_column and unformatted_rows: - self.failureDetails["unformatted_data"] = self.craftData(unformatted_column, unformatted_rows) - self.reason += ( - "\nParse failure occurred for some data, the provided data may not be complete. " - "Please contact Cisco TAC to work on the missing data." - ) - - def buildResult(self): - result = { - "ruleId": self.ruleId, - "name": self.name, - "description": self.description, - "reason": self.reason, - "sub_reason": self.sub_reason, - "recommended_action": self.recommended_action, - "docUrl": self.docUrl, - "severity": self.severity, - "ruleStatus": self.ruleStatus, - "showValidation": self.showValidation, - "failureDetails": self.failureDetails, - } - return result - - def writeResult(self): - if not os.path.isdir(self.path): - os.mkdir(self.path) - with open(os.path.join(self.path, self.filename), "w") as f: - json.dump(self.buildResult(), f, indent=2) - return "{}/{}".format(self.path, self.filename) - - class OldVerClassNotFound(Exception): """ Later versions of ACI can have class properties not found in older versions """ pass @@ -247,7 +169,7 @@ def start_log(self): if self.log is not None and self._log is None: # if self.log is a string, then attempt to open file pointer (do not catch exception, we want it # to die if there's an error opening the logfile) - if isinstance(self.log, str) or isinstance(self.log, unicode): + if isinstance(self.log, str) or isinstance(self.log, text_type): self._log = open(self.log, "ab") else: self._log = self.log @@ -351,7 +273,6 @@ def login(self, max_attempts=7, timeout=17): "prompt": self.prompt } - last_match = None while max_attempts > 0: max_attempts -= 1 match = self.__expect(matches, timeout) @@ -383,7 +304,6 @@ def login(self, max_attempts=7, timeout=17): elif match == "timeout": logging.debug("timeout received but connection still opened, send enter") self.child.sendline("\r\n") - last_match = match # did not find prompt within max attempts, failed login logging.error("failed to login after multiple attempts") return False @@ -999,6 +919,150 @@ def is_firstver_gt_secondver(first_ver, second_ver): return result +class syntheticMaintPValidate: + """ + APIC uses an object called `syntheticMaintPValidate` to store the results of + each rule/check in the pre-upgrade validation which is run during the upgrade + workflow in the APIC GUI. When this script is invoked during the workflow, it + is expected to write the results of each rule/check to a JSON file (one per rule) + in a specific format. + """ + # Expected keys in the JSON file + __slots__ = ( + "ruleId", "name", "description", "reason", "sub_reason", "recommended_action", + "docUrl", "severity", "ruleStatus", "showValidation", "failureDetails", + ) + + def __init__(self, func_name, name, description): + self.ruleId = func_name + self.name = name + self.description = description + self.reason = "" + self.sub_reason = "" + self.recommended_action = "" + self.docUrl = "" + self.severity = "informational" + self.ruleStatus = "passed" # passed|failed + self.showValidation = True + self.failureDetails = { + "failType": "", + "data": [], + "unformatted_data": [], + } + + @staticmethod + def craftData(column, rows): + if not (isinstance(rows, list) and isinstance(column, list)): + raise TypeError("Rows and column must be lists.") + data = [] + for i in range(len(rows)): + entry = {} + for j in range(len(column)): + if j < len(rows[i]): + entry[column[j]] = rows[i][j] + else: + entry[column[j]] = None + data.append(entry) + return data + + def updateWithResults(self, result, recommended_action, msg, doc_url, headers, data, unformatted_headers, unformatted_data): + self.reason = msg + self.recommended_action = recommended_action + self.docUrl = doc_url + + # Show validation + if result in [NA, POST]: + self.showValidation = False + + # Severity + if result in [FAIL_O, FAIL_UF]: + self.severity = "critical" + elif result in [ERROR]: + self.severity = "major" + elif result in [MANUAL]: + self.severity = "warning" + + if result not in [NA, PASS]: + self.ruleStatus = "failed" + self.failureDetails["failType"] = result + self.failureDetails["data"] = self.craftData(headers, data) + if unformatted_headers and unformatted_data: + self.failureDetails["unformatted_data"] = self.craftData(unformatted_headers, unformatted_data) + if self.reason: + self.reason += "\n" + self.reason += ( + "Parse failure occurred, the provided data may not be complete. " + "Please contact Cisco TAC to identify the missing data." + ) + + def buildResult(self): + return {slot: getattr(self, slot) for slot in self.__slots__} + + def writeResult(self, path=JSON_DIR): + filename = re.sub(r'[^a-zA-Z0-9_]+|\s+', '_', self.ruleId) + '.json' + if not os.path.isdir(path): + os.mkdir(path) + with open(os.path.join(path, filename), "w") as f: + json.dump(self.buildResult(), f, indent=2) + return "{}/{}".format(path, filename) + + +class Result: + """Class to hold the result of a check.""" + __slots__ = ("result", "msg", "headers", "data", "unformatted_headers", "unformatted_data", "recommended_action", "doc_url", "adjust_title") + + def __init__(self, result=PASS, msg="", headers=None, data=None, unformatted_headers=None, unformatted_data=None, recommended_action="", doc_url="", adjust_title=False): + self.result = result + self.msg = msg + self.headers = headers if headers is not None else [] + self.data = data if data is not None else [] + self.unformatted_headers = unformatted_headers if unformatted_headers is not None else [] + self.unformatted_data = unformatted_data if unformatted_data is not None else [] + self.recommended_action = recommended_action + self.doc_url = doc_url + self.adjust_title = adjust_title + + def as_dict(self): + return {slot: getattr(self, slot) for slot in self.__slots__} + + def as_dict_for_json_result(self): + return {slot: getattr(self, slot) for slot in self.__slots__ if slot != "adjust_title"} + + +def check_wrapper(check_title): + """ + Decorator to wrap a check function to handle the printing of title and results, + and to write the results in a file in a JSON format. + """ + def decorator(check_func): + @functools.wraps(check_func) + def wrapper(index, total_checks, *args, **kwargs): + # Print `[Check 1/81] ...` + print_title(check_title, index, total_checks) + + try: + # Run check, expecting it to return a `Result` object + r = check_func(*args, **kwargs) + except Exception as e: + r = Result(result=ERROR, msg='Unexpected Error: {}'.format(e)) + logging.exception(e) + + # Print `[Check 1/81] <title>... <result> + <failure details>` + print_result(title=check_title, **r.as_dict()) + + # Write results in JSON + # Using `wrapper.__name__` instead of `check_func.__name` because + # both show the original check func name and `wrapper.__name__` can + # be dynamically changed inside each check func if needed. (mainly + # for test or debugging) + synth = syntheticMaintPValidate(wrapper.__name__, check_title, "") + synth.updateWithResults(**r.as_dict_for_json_result()) + synth.writeResult() + return r.result + return wrapper + return decorator + + def format_table(headers, data, min_width=5, left_padding=2, hdr_sp='-', col_sp=' '): """ get string results in table format @@ -1137,22 +1201,7 @@ def print_result(title, result, msg='', unformatted_headers=None, unformatted_data=None, recommended_action='', doc_url='', - func_name='', - adjust_title=False, - json_output=True): - if json_output: - synth = syntheticMaintPValidate(func_name, title, "") - synth.updateWithResults( - result=result, - recommended_action=recommended_action, - reason=msg, - doc_url=doc_url, - column=headers, - rows=data, - unformatted_column=unformatted_headers, - unformatted_rows=unformatted_data, - ) - synth.writeResult() + adjust_title=False): padding = 120 - len(title) - len(msg) if adjust_title: padding += len(title) + 18 output = '{}{:>{}}'.format(msg, result, padding) @@ -1161,7 +1210,7 @@ def print_result(title, result, msg='', output += '\n' + format_table(headers, data) if unformatted_data: unformatted_data.sort() - output += '\n' + format_table(unformatted_headers, unformatted_data) + output += '\n\n' + format_table(unformatted_headers, unformatted_data) if data or unformatted_data: output += '\n' if recommended_action: @@ -1329,21 +1378,19 @@ def get_switch_version(): return None -def apic_cluster_health_check(index, total_checks, cversion, **kwargs): - title = 'APIC Cluster is Fully-Fit' +@check_wrapper(check_title="APIC Cluster is Fully-Fit") +def apic_cluster_health_check(cversion, **kwargs): result = FAIL_UF msg = '' headers = ['APIC-ID\n(Seen By)', 'APIC-ID\n(Affected)', 'Admin State', 'Operational State', 'Health State'] unformatted_headers = ['Affected DN', 'Admin State', 'Operational State', 'Health State'] data = [] unformatted_data = [] - doc_url = 'ACI Troubleshooting Guide 2nd Edition - http://cs.co/9003ybZ1d' - print_title(title, index, total_checks) + doc_url = 'http://cs.co/9003ybZ1d' # ACI Troubleshooting Guide 2nd Edition if cversion.older_than("4.2(1a)"): recommended_action = 'Follow "Initial Fabric Setup" in ACI Troubleshooting Guide 2nd Edition' else: recommended_action = 'Troubleshoot by running "acidiag cluster" on APIC CLI' - dn_regex = node_regex + r'/av/node-(?P<winode>\d)' infraWiNodes = icurl('class', 'infraWiNode.json') for av in infraWiNodes: @@ -1362,19 +1409,16 @@ def apic_cluster_health_check(index, total_checks, cversion, **kwargs): msg = 'infraWiNode (Appliance Vector) not found!' elif not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, - recommended_action, doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, msg=msg, headers=headers, data=data, unformatted_headers=unformatted_headers, unformatted_data=unformatted_data, recommended_action=recommended_action, doc_url=doc_url) -def switch_status_check(index, total_checks, **kwargs): - title = 'Switches are all in Active state' +@check_wrapper(check_title="Switches are all in Active state") +def switch_status_check(**kwargs): result = FAIL_UF msg = '' - headers = ['Pod-ID', 'Node-ID', 'State', 'Recommended Action'] + headers = ['Pod-ID', 'Node-ID', 'State'] data = [] recommended_action = 'Bring this node back to "active"' - print_title(title, index, total_checks) # fabricNode.fabricSt shows `disabled` for both Decommissioned and Maintenance (GIR). # fabricRsDecommissionNode.debug==yes is required to show `disabled (Maintenance)`. fabricNodes = icurl('class', 'fabricNode.json?&query-target-filter=ne(fabricNode.role,"controller")') @@ -1390,55 +1434,49 @@ def switch_status_check(index, total_checks, **kwargs): for gir in girNodes: if node_id == gir['fabricRsDecommissionNode']['attributes']['targetId']: state = state + ' (Maintenance)' - data.append([pod_id, node_id, state, recommended_action]) + data.append([pod_id, node_id, state]) if not fabricNodes: result = MANUAL msg = 'Switch fabricNode not found!' elif not data: result = PASS - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action) -def maintp_grp_crossing_4_0_check(index, total_checks, cversion, tversion, **kwargs): - title = 'Firmware/Maintenance Groups when crossing 4.0 Release' +@check_wrapper(check_title="Firmware/Maintenance Groups when crossing 4.0 Release") +def maintp_grp_crossing_4_0_check(cversion, tversion, **kwargs): result = PASS msg = '' - headers = ["Group Name", "Group Type", "Recommended Action"] + headers = ["Group Name", "Group Type"] data = [] recommended_action = 'Remove the group prior to APIC upgrade. Create a new switch group once APICs are upgraded to post-4.0.' - print_title(title, index, total_checks) - + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#firmwaremaintenance-groups-when-crossing-40-release" if (int(cversion.major1) >= 4) or (tversion and (int(tversion.major1) <= 3)): result = NA - msg = 'Versions not applicable' + msg = VER_NOT_AFFECTED elif (int(cversion.major1) < 4) and not tversion: result = MANUAL - msg = 'Target version not supplied. Skipping.' + msg = TVER_MISSING else: groups = icurl('mo', '/uni/fabric.json?query-target=children&target-subtree-class=maintMaintP,firmwareFwP') for g in groups: result = FAIL_O if g.get('maintMaintP'): - data.append([g['maintMaintP']['attributes']['name'], 'Maintenance Group', recommended_action]) + data.append([g['maintMaintP']['attributes']['name'], 'Maintenance Group']) else: - data.append([g['firmwareFwP']['attributes']['name'], 'Firmware Group', recommended_action]) - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + data.append([g['firmwareFwP']['attributes']['name'], 'Firmware Group']) + return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def ntp_status_check(index, total_checks, **kargs): - title = 'NTP Status' +@check_wrapper(check_title="NTP Status") +def ntp_status_check(**kargs): result = FAIL_UF - msg = '' - headers = ["Pod-ID", "Node-ID", "Recommended Action"] + headers = ["Pod-ID", "Node-ID"] data = [] recommended_action = 'Not Synchronized. Check NTP config and NTP server reachability.' - print_title(title, index, total_checks) - + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#ntp-status" fabricNodes = icurl('class', 'fabricNode.json') nodes = [fn['fabricNode']['attributes']['id'] for fn in fabricNodes] - apicNTPs = icurl('class', 'datetimeNtpq.json') switchNTPs = icurl('class', 'datetimeClkPol.json') for apicNTP in apicNTPs: @@ -1454,24 +1492,22 @@ def ntp_status_check(index, total_checks, **kargs): for fn in fabricNodes: if fn['fabricNode']['attributes']['id'] in nodes: dn = re.search(node_regex, fn['fabricNode']['attributes']['dn']) - data.append([dn.group('pod'), dn.group('node'), recommended_action]) + data.append([dn.group('pod'), dn.group('node')]) if not data: result = PASS - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def features_to_disable_check(index, total_checks, cversion, tversion, **kwargs): - title = 'Features that need to be Disabled prior to Upgrade' +@check_wrapper(check_title="Features that need to be Disabled prior to Upgrade") +def features_to_disable_check(cversion, tversion, **kwargs): result = FAIL_O - msg = '' headers = ["Feature", "Name", "Status", "Recommended Action"] data = [] - print_title(title, index, total_checks) + recommended_action = 'Disable the feature prior to upgrade' + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#features-that-need-to-be-disabled-prior-to-upgrade" apPlugins = icurl('class', 'apPlugin.json?&query-target-filter=ne(apPlugin.pluginSt,"inactive")') infraMOs = icurl('mo', 'uni/infra.json?query-target=subtree&target-subtree-class=infrazoneZone,epControlP') - default_apps = ['IntersightDC', 'NIALite', 'NIBASE', 'ApicVision'] default_appDNs = ['pluginContr/plugin-Cisco_' + app for app in default_apps] if apPlugins: @@ -1501,28 +1537,20 @@ def features_to_disable_check(index, total_checks, cversion, tversion, **kwargs) data.append(['Rogue Endpoint', name, 'Enabled', ra]) if not data: result = PASS - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def switch_group_guideline_check(index, total_checks, **kwargs): - title = 'Switch Upgrade Group Guidelines' +@check_wrapper(check_title="Switch Upgrade Group Guidelines") +def switch_group_guideline_check(**kwargs): result = FAIL_O - msg = '' headers = ['Group Name', 'Pod-ID', 'Node-IDs', 'Failure Reason'] data = [] recommended_action = 'Upgrade nodes in each line above separately in another group.' - doc_url = 'Guidelines for Switch Upgrades in ACI Firmware Upgrade Overview' - print_title(title, index, total_checks) + doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#switch-upgrade-group-guidelines' maints = icurl('class', 'maintMaintGrp.json?rsp-subtree=children') if not maints: - result = MANUAL - msg = 'No upgrade groups found!' - print_result(title, result, msg, headers, data, - recommended_action=recommended_action, doc_url=doc_url, - func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=MANUAL, msg='No upgrade groups found!', doc_url=doc_url) spine_type = ['', 'RR ', 'IPN/ISN '] f_spines = [defaultdict(list) for t in spine_type] @@ -1615,21 +1643,19 @@ def switch_group_guideline_check(index, total_checks, **kwargs): data.append([m_name, m_vpc_peers[0]['pod'], ','.join(x['node'] for x in m_vpc_peers), reason_vpc]) - if not data and not msg: + if not data: result = PASS - print_result(title, result, msg, headers, data, - recommended_action=recommended_action, doc_url=doc_url, - func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def switch_bootflash_usage_check(index, total_checks, tversion, **kwargs): - title = 'Switch Node /bootflash usage' +@check_wrapper(check_title="Switch Node /bootflash usage") +def switch_bootflash_usage_check(tversion, **kwargs): result = FAIL_UF msg = '' headers = ["Pod-ID", "Node-ID", "Utilization", "Alert"] data = [] - print_title(title, index, total_checks) + recommended_action = "Over 50% usage! Contact Cisco TAC for Support" + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#switch-node-bootflash-usage" partitions_api = 'eqptcapacityFSPartition.json' partitions_api += '?query-target-filter=eq(eqptcapacityFSPartition.path,"/bootflash")' @@ -1640,8 +1666,7 @@ def switch_bootflash_usage_check(index, total_checks, tversion, **kwargs): partitions = icurl('class', partitions_api) if not partitions: - result = ERROR - msg = 'bootflash objects not found' + return Result(result=ERROR, msg='bootflash objects not found', doc_url=doc_url) predownloaded_nodes = [] try: @@ -1664,25 +1689,25 @@ def switch_bootflash_usage_check(index, total_checks, tversion, **kwargs): usage = (used / (avail + used)) * 100 if (usage >= 50) and (node not in predownloaded_nodes): - data.append([pod, node, usage, "Over 50% usage! Contact Cisco TAC for Support"]) + data.append([pod, node, usage]) if not data: result = PASS msg = 'All below 50% or pre-downloaded' - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def l3out_mtu_check(index, total_checks, **kwargs): - title = 'L3Out MTU' +@check_wrapper(check_title="L3Out MTU") +def l3out_mtu_check(**kwargs): result = MANUAL - msg = 'Verify that these MTUs match with connected devices' + msg = "" headers = ["Tenant", "L3Out", "Node Profile", "Logical Interface Profile", "Pod", "Node", "Interface", "Type", "IP Address", "MTU"] data = [] unformatted_headers = ['L3 DN', "Type", "IP Address", "MTU"] unformatted_data = [] - print_title(title, index, total_checks) + recommended_action = 'Verify that these MTUs match with connected devices' + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#l3out-mtu" dn_regex = r'tn-(?P<tenant>[^/]+)/out-(?P<l3out>[^/]+)/lnodep-(?P<lnodep>[^/]+)/lifp-(?P<lifp>[^/]+)/rspathL3OutAtt-\[topology/pod-(?P<pod>[^/]+)/.*paths-(?P<nodes>\d{3,4}|\d{3,4}-\d{3,4})/pathep-\[(?P<int>.+)\]\]' response_json = icurl('class', 'l3extRsPathL3OutAtt.json') @@ -1710,20 +1735,27 @@ def l3out_mtu_check(index, total_checks, **kwargs): if not data and not unformatted_data: result = NA msg = 'No L3Out Interfaces found' - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + msg=msg, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def port_configured_as_l2_check(index, total_checks, **kwargs): - title = 'L3 Port Config (F0467 port-configured-as-l2)' +@check_wrapper(check_title="L3 Port Config (F0467 port-configured-as-l2)") +def port_configured_as_l2_check(**kwargs): result = FAIL_O - msg = '' headers = ['Fault', 'Tenant', 'L3Out', 'Node', 'Path', 'Recommended Action'] data = [] unformatted_headers = ['Fault', 'Fault DN', 'Recommended Action'] unformatted_data = [] recommended_action = 'Resolve the conflict by removing this config or other configs using this port as L2' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#l2l3-port-config" l2dn_regex = r'uni/tn-(?P<tenant>[^/]+)/out-(?P<l3out>[^/]+)/fd-\[.+rtdOutDef-.+/node-(?P<node>\d{3,4})/(?P<path>.+)/nwissues' l2response_json = icurl('class', @@ -1732,28 +1764,31 @@ def port_configured_as_l2_check(index, total_checks, **kwargs): fc = faultDelegate['faultDelegate']['attributes']['code'] dn = re.search(l2dn_regex, faultDelegate['faultDelegate']['attributes']['dn']) if dn: - data.append([fc, dn.group('tenant'), dn.group('l3out'), - dn.group('node'), dn.group('path'), - recommended_action]) + data.append([fc, dn.group('tenant'), dn.group('l3out'), dn.group('node'), dn.group('path')]) else: - unformatted_data.append( - [fc, faultDelegate['faultDelegate']['attributes']['dn'], recommended_action]) + unformatted_data.append([fc, faultDelegate['faultDelegate']['attributes']['dn']]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def port_configured_as_l3_check(index, total_checks, **kwargs): - title = 'L2 Port Config (F0467 port-configured-as-l3)' +@check_wrapper(check_title="L2 Port Config (F0467 port-configured-as-l3)") +def port_configured_as_l3_check(**kwargs): result = FAIL_O - msg = '' - headers = ['Fault', 'Pod', 'Node', 'Tenant', 'AP', 'EPG', 'Port', 'Recommended Action'] + headers = ['Fault', 'Pod', 'Node', 'Tenant', 'AP', 'EPG', 'Port'] data = [] - unformatted_headers = ['Fault', 'Fault DN', 'Recommended Action'] + unformatted_headers = ['Fault', 'Fault DN'] unformatted_data = [] recommended_action = 'Resolve the conflict by removing this config or other configs using this port as L3' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#l2l3-port-config" l3affected_regex = r'topology/(?P<pod>[^/]+)/(?P<node>[^/]+)/.+uni/tn-(?P<tenant>[^/]+)/ap-(?P<ap>[^/]+)/epg-(?P<epg>\w+).+(?P<port>eth\d+/\d+)' l3response_json = icurl('class', @@ -1761,32 +1796,36 @@ def port_configured_as_l3_check(index, total_checks, **kwargs): for faultDelegate in l3response_json: fc = faultDelegate['faultDelegate']['attributes']['code'] affected_array = re.search(l3affected_regex, faultDelegate['faultDelegate']['attributes']['dn']) - if affected_array: - data.append( - [fc, affected_array.group("pod"), affected_array.group("node"), affected_array.group("tenant"), - affected_array.group("ap"), affected_array.group("epg"), affected_array.group("port"), - recommended_action]) + data.append([ + fc, affected_array.group("pod"), affected_array.group("node"), affected_array.group("tenant"), + affected_array.group("ap"), affected_array.group("epg"), affected_array.group("port") + ]) else: - unformatted_data.append( - [fc, faultDelegate['faultDelegate']['attributes']['dn'], recommended_action]) + unformatted_data.append([fc, faultDelegate['faultDelegate']['attributes']['dn']]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def prefix_already_in_use_check(index, total_checks, **kwargs): - title = 'L3Out Subnets (F0467 prefix-entry-already-in-use)' +@check_wrapper(check_title="L3Out Subnets (F0467 prefix-entry-already-in-use)") +def prefix_already_in_use_check(**kwargs): result = FAIL_O - msg = '' headers = ["VRF Name", "Prefix", "L3Out EPGs without F0467", "L3Out EPGs with F0467"] headers_old = ["Fault", "Failed L3Out EPG"] data = [] unformatted_headers = ['Fault', 'Fault Description', 'Fault DN'] unformatted_data = [] recommended_action = 'Resolve the conflict by removing the overlapping prefix from the faulted L3Out EPG.' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#l3out-subnets" # Old versions (pre-CSCvq93592) do not show VRF VNID and prefix in use (2nd line) desc_regex = r'Configuration failed for (?P<failedEpg>.+) due to Prefix Entry Already Used in Another EPG' @@ -1795,8 +1834,7 @@ def prefix_already_in_use_check(index, total_checks, **kwargs): filter = '?query-target-filter=and(wcard(faultInst.changeSet,"prefix-entry-already-in-use"),wcard(faultInst.dn,"uni/epp/rtd"))' faultInsts = icurl("class", "faultInst.json" + filter) if not faultInsts: - print_result(title, PASS, func_name=inspect.currentframe().f_code.co_name) - return PASS + return Result(result=PASS) vnid2vrf = {} fvCtxs = icurl("class", "fvCtx.json") @@ -1837,8 +1875,15 @@ def prefix_already_in_use_check(index, total_checks, **kwargs): data = [["F0467", epg] for epg in conflicts["_"]["_"]["faulted_extepgs"]] if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers_old, data, unformatted_headers, unformatted_data, recommended_action, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers_old, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) # Proceed further only for new versions with VRF/prefix data in faults # Get L3Out DNs in the VRFs mentioned by the faults @@ -1881,20 +1926,26 @@ def prefix_already_in_use_check(index, total_checks, **kwargs): if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def encap_already_in_use_check(index, total_checks, **kwargs): - title = 'Encap Already In Use (F0467 encap-already-in-use)' +@check_wrapper(check_title="Encap Already In Use (F0467 encap-already-in-use)") +def encap_already_in_use_check(**kwargs): result = FAIL_O - msg = '' headers = ["Faulted EPG/L3Out", "Node", "Port", "In Use Encap(s)", "In Use by EPG/L3Out"] data = [] unformatted_headers = ['Fault Description'] unformatted_data = [] recommended_action = 'Resolve the overlapping encap configuration prior to upgrade' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#encap-already-in-use" # <port> can be `ethX/X` or the name of I/F policy group # <vlan> is not there for older versions @@ -1942,22 +1993,26 @@ def encap_already_in_use_check(index, total_checks, **kwargs): if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, - unformatted_headers, unformatted_data, recommended_action=recommended_action, - func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def bd_subnet_overlap_check(index, total_checks, **kwargs): - title = 'BD Subnets (F1425 subnet-overlap)' +@check_wrapper(check_title="BD Subnets (F1425 subnet-overlap)") +def bd_subnet_overlap_check(**kwargs): result = FAIL_O - msg = '' - headers = ["Fault", "Pod", "Node", "VRF", "Interface", "Address", "Recommended Action"] + headers = ["Fault", "Pod", "Node", "VRF", "Interface", "Address"] data = [] unformatted_headers = ['Fault', 'Fault DN', 'Recommended Action'] unformatted_data = [] recommended_action = 'Resolve the conflict by removing BD subnets causing the overlap' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#bd-subnets" dn_regex = node_regex + r'/.+dom-(?P<vrf>[^/]+)/if-(?P<int>[^/]+)/addr-\[(?P<addr>[^/]+/\d{2})' faultInsts = icurl('class', 'faultInst.json?query-target-filter=wcard(faultInst.changeSet,"subnet-overlap")') @@ -1968,25 +2023,31 @@ def bd_subnet_overlap_check(index, total_checks, **kwargs): dn_array = re.search(dn_regex, faultInst['faultInst']['attributes']['dn']) if dn_array: data.append([fc, dn_array.group("pod"), dn_array.group("node"), dn_array.group("vrf"), - dn_array.group("int"), dn_array.group("addr"), recommended_action]) + dn_array.group("int"), dn_array.group("addr")]) else: - unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn'], recommended_action]) + unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn']]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def bd_duplicate_subnet_check(index, total_checks, **kwargs): - title = 'BD Subnets (F0469 duplicate-subnets-within-ctx)' +@check_wrapper(check_title="BD Subnets (F0469 duplicate-subnets-within-ctx)") +def bd_duplicate_subnet_check(**kwargs): result = FAIL_O - msg = '' - headers = ["Fault", "Pod", "Node", "Bridge Domain 1", "Bridge Domain 2", "Recommended Action"] + headers = ["Fault", "Pod", "Node", "Bridge Domain 1", "Bridge Domain 2"] data = [] - unformatted_headers = ['Fault', 'Fault DN', 'Fault Description', 'Recommended Action'] + unformatted_headers = ['Fault', 'Fault DN', 'Fault Description'] unformatted_data = [] recommended_action = 'Resolve the conflict by removing BD subnets causing the duplicate' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#bd-subnets" descr_regex = r'duplicate-subnets-within-ctx: (?P<bd1>.+)\s,(?P<bd2>.+)' faultInsts = icurl('class', @@ -1996,22 +2057,25 @@ def bd_duplicate_subnet_check(index, total_checks, **kwargs): dn = re.search(node_regex, faultInst['faultInst']['attributes']['dn']) descr = re.search(descr_regex, faultInst['faultInst']['attributes']['descr']) if dn and descr: - data.append([fc, dn.group("pod"), dn.group("node"), - descr.group("bd1"), descr.group("bd2"), recommended_action]) + data.append([fc, dn.group("pod"), dn.group("node"), descr.group("bd1"), descr.group("bd2")]) else: - unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn'], - faultInst['faultInst']['attributes']['descr'], - recommended_action]) + unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn'], faultInst['faultInst']['attributes']['descr']]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def hw_program_fail_check(index, total_checks, cversion, **kwargs): - title = 'HW Programming Failure (F3544 L3Out Prefixes, F3545 Contracts, actrl-resource-unavailable)' +@check_wrapper(check_title="HW Programming Failure (F3544 L3Out Prefixes, F3545 Contracts, actrl-resource-unavailable)") +def hw_program_fail_check(cversion, **kwargs): result = FAIL_O - msg = '' headers = ["Fault", "Pod", "Node", "Fault Description", "Recommended Action"] data = [] unformatted_headers = ['Fault', 'Fault DN', 'Fault Description', 'Recommended Action'] @@ -2022,7 +2086,7 @@ def hw_program_fail_check(index, total_checks, cversion, **kwargs): 'F3544': 'Ensure that LPM and host routes usage are below the capacity and resolve the fault', 'F3545': 'Ensure that Policy CAM usage is below the capacity and resolve the fault' } - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#hw-programming-failure" # Faults F3544 and F3545 don't exist until 4.1(1a)+ if cversion.older_than("4.1(1a)"): @@ -2047,18 +2111,21 @@ def hw_program_fail_check(index, total_checks, cversion, **kwargs): fc, faultInst['faultInst']['attributes']['dn'], faultInst['faultInst']['attributes']['descr'], recommended_action.get(fc, 'Resolve the fault')]) - if not data and not unformatted_data: result = PASS - - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + doc_url=doc_url, + ) -def switch_ssd_check(index, total_checks, **kwargs): - title = 'Switch SSD Health (F3073, F3074 equipment-flash-warning)' +@check_wrapper(check_title="Switch SSD Health (F3073, F3074 equipment-flash-warning)") +def switch_ssd_check(**kwargs): result = FAIL_O - msg = '' headers = ["Fault", "Pod", "Node", "SSD Model", "% Threshold Crossed", "Recommended Action"] data = [] unformatted_headers = ["Fault", "Fault DN", "% Threshold Crossed", "Recommended Action"] @@ -2068,7 +2135,7 @@ def switch_ssd_check(index, total_checks, **kwargs): 'F3073': 'Contact Cisco TAC for replacement procedure', 'F3074': 'Monitor (no impact to upgrades)' } - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#switch-ssd-health" cs_regex = r'model \(New: (?P<model>\w+)\),' faultInsts = icurl('class', @@ -2088,78 +2155,79 @@ def switch_ssd_check(index, total_checks, **kwargs): recommended_action.get(fc, 'Resolve the fault')]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + doc_url=doc_url, + ) -# Connection based check -def apic_ssd_check(index, total_checks, cversion, username, password, **kwargs): - title = 'APIC SSD Health' +# Connection Based Check +@check_wrapper(check_title="APIC SSD Health") +def apic_ssd_check(cversion, username, password, **kwargs): result = FAIL_UF - msg = '' headers = ["Pod", "Node", "Storage Unit", "% lifetime remaining", "Recommended Action"] data = [] unformatted_headers = ["Pod", "Node", "Storage Unit", "% lifetime remaining", "Recommended Action"] unformatted_data = [] recommended_action = "Contact TAC for replacement" - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#apic-ssd-health" has_error = False dn_regex = node_regex + r'/.+p-\[(?P<storage>.+)\]-f' faultInsts = icurl('class', 'faultInst.json?query-target-filter=eq(faultInst.code,"F2731")') adjust_title = False if len(faultInsts) == 0 and (cversion.older_than("4.2(7f)") or cversion.older_than("5.2(1g)")): - print('') - adjust_title = True controller = icurl('class', 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")') - report_other = False if not controller: - print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?', func_name=inspect.currentframe().f_code.co_name) - return ERROR - else: - checked_apics = {} - for apic in controller: - attr = apic['topSystem']['attributes'] - if attr['address'] in checked_apics: continue - checked_apics[attr['address']] = 1 - pod_id = attr['podId'] - node_id = attr['id'] - node_title = 'Checking %s...' % attr['name'] - print_title(node_title) - try: - c = Connection(attr['address']) - c.username = username - c.password = password - c.log = LOG_FILE - c.connect() - except Exception as e: - data.append([attr['id'], attr['name'], '-', '-', '-', str(e)]) - print_result(node_title, ERROR, json_output=False) - has_error = True - continue - try: - c.cmd( - 'grep -oE "SSD Wearout Indicator is [0-9]+" /var/log/dme/log/svc_ifc_ae.bin.log | tail -1') - except Exception as e: - data.append([attr['id'], attr['name'], '-', '-', '-', str(e)]) - print_result(node_title, ERROR, json_output=False) - has_error = True - continue + return Result(result=ERROR, msg="topSystem response empty. Is the cluster healthy?", doc_url=doc_url) - wearout_ind = re.search(r'SSD Wearout Indicator is (?P<wearout>[0-9]+)', c.output) - if wearout_ind is not None: - wearout = wearout_ind.group('wearout') - if int(wearout) < 5: - data.append([pod_id, node_id, "Solid State Disk", - wearout, recommended_action]) - report_other = True + print('') + adjust_title = True + report_other = False + checked_apics = {} + for apic in controller: + attr = apic['topSystem']['attributes'] + if attr['address'] in checked_apics: continue + checked_apics[attr['address']] = 1 + pod_id = attr['podId'] + node_id = attr['id'] + node_title = 'Checking %s...' % attr['name'] + print_title(node_title) + try: + c = Connection(attr['address']) + c.username = username + c.password = password + c.log = LOG_FILE + c.connect() + except Exception as e: + data.append([attr['id'], attr['name'], '-', '-', '-', str(e)]) + print_result(node_title, ERROR) + has_error = True + continue + try: + c.cmd( + 'grep -oE "SSD Wearout Indicator is [0-9]+" /var/log/dme/log/svc_ifc_ae.bin.log | tail -1') + except Exception as e: + data.append([attr['id'], attr['name'], '-', '-', '-', str(e)]) + print_result(node_title, ERROR) + has_error = True + continue - print_result(node_title, DONE, json_output=False) - continue - if report_other: - data.append([pod_id, node_id, "Solid State Disk", - wearout, "No Action Required"]) - print_result(node_title, DONE, json_output=False) + wearout_ind = re.search(r'SSD Wearout Indicator is (?P<wearout>[0-9]+)', c.output) + if wearout_ind is not None: + wearout = wearout_ind.group('wearout') + if int(wearout) < 5: + data.append([pod_id, node_id, "Solid State Disk", wearout, recommended_action]) + report_other = True + print_result(node_title, DONE) + continue + if report_other: + data.append([pod_id, node_id, "Solid State Disk", wearout, "No Action Required"]) + print_result(node_title, DONE) else: headers = ["Fault", "Pod", "Node", "Storage Unit", "% lifetime remaining", "Recommended Action"] unformatted_headers = ["Fault", "Fault DN", "% lifetime remaining", "Recommended Action"] @@ -2176,20 +2244,26 @@ def apic_ssd_check(index, total_checks, cversion, username, password, **kwargs): result = ERROR elif not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, adjust_title=adjust_title, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + doc_url=doc_url, + adjust_title=adjust_title, + ) -def port_configured_for_apic_check(index, total_checks, **kwargs): - title = 'Config On APIC Connected Port (F0467 port-configured-for-apic)' +@check_wrapper(check_title="Config On APIC Connected Port (F0467 port-configured-for-apic)") +def port_configured_for_apic_check(**kwargs): result = FAIL_UF - msg = '' - headers = ["Fault", "Pod", "Node", "Port", "EPG", "Recommended Action"] + headers = ["Fault", "Pod", "Node", "Port", "EPG"] data = [] - unformatted_headers = ['Fault', 'Fault DN', 'Recommended Action'] + unformatted_headers = ['Fault', 'Fault DN'] unformatted_data = [] recommended_action = 'Remove config overlapping with APIC Connected Interfaces' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#config-on-apic-connected-port" dn_regex = node_regex + r'/.+fv-\[(?P<epg>.+)\]/node-\d{3,4}/.+\[(?P<port>eth\d{1,2}/\d{1,2}).+/nwissues' faultInsts = icurl('class', @@ -2198,20 +2272,25 @@ def port_configured_for_apic_check(index, total_checks, **kwargs): fc = faultInst['faultInst']['attributes']['code'] dn = re.search(dn_regex, faultInst['faultInst']['attributes']['dn']) if dn: - data.append([fc, dn.group("pod"), dn.group("node"), dn.group("port"), - dn.group("epg"), recommended_action]) + data.append([fc, dn.group("pod"), dn.group("node"), dn.group("port"), dn.group("epg")]) else: - unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn'], recommended_action]) + unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn']]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def overlapping_vlan_pools_check(index, total_checks, **kwargs): - title = 'Overlapping VLAN Pools' +@check_wrapper(check_title="Overlapping VLAN Pools") +def overlapping_vlan_pools_check(**kwargs): result = PASS - msg = '' headers = ['Tenant', 'AP', 'EPG', 'Node', 'Port', 'VLAN Scope', 'VLAN ID', 'VLAN Pools (Domains)', 'Impact'] data = [] recommended_action = """ @@ -2220,13 +2299,10 @@ def overlapping_vlan_pools_check(index, total_checks, **kwargs): When `Impact` shows `Flood Scope`, you should check whether it is ok that STP BPDUs, or any BUM traffic when using Flood-in-Encap, may not be flooded within the same VLAN ID across all the nodes/ports. Note that only the nodes causing the overlap are shown above.""" doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#overlapping-vlan-pool' - print_title(title, index, total_checks) infraSetPols = icurl('mo', 'uni/infra/settings.json') if infraSetPols[0]['infraSetPol']['attributes'].get('validateOverlappingVlans') in ['true', 'yes']: - msg = '`Enforce EPG VLAN Validation` is enabled. No need to check overlapping VLANs' - print_result(title, result, msg, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=PASS, msg="`Enforce EPG VLAN Validation` is enabled. No need to check overlapping VLANs") # Get VLAN pools and ports from access policy mo_classes = AciAccessPolicyParser.get_classes() @@ -2391,21 +2467,18 @@ def overlapping_vlan_pools_check(index, total_checks, **kwargs): ', '.join(vpool_domains), impact, ]) - - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def scalability_faults_check(index, total_checks, **kwargs): - title = 'Scalability (faults related to Capacity Dashboard)' +@check_wrapper(check_title="Scalability (faults related to Capacity Dashboard)") +def scalability_faults_check(**kwargs): result = FAIL_O - msg = '' - headers = ["Fault", "Pod", "Node", "Description", "Recommended Action"] + headers = ["Fault", "Pod", "Node", "Description"] data = [] - unformatted_headers = ["Fault", "Fault DN", "Description", "Recommended Action"] + unformatted_headers = ["Fault", "Fault DN", "Description"] unformatted_data = [] recommended_action = 'Review config and reduce the usage' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#scalability-faults-related-to-capacity-dashboard" faultInsts = icurl('class', 'eqptcapacityEntity.json?rsp-subtree-include=faults,no-scoped') for fault in faultInsts: @@ -2414,23 +2487,30 @@ def scalability_faults_check(index, total_checks, **kwargs): f = fault['faultInst']['attributes'] dn = re.search(node_regex, f['dn']) if dn: - data.append([f['code'], dn.group('pod'), dn.group('node'), f['descr'], recommended_action]) + data.append([f['code'], dn.group('pod'), dn.group('node'), f['descr']]) else: - unformatted_data.append([f['code'], f['dn'], f['descr'], recommended_action]) + unformatted_data.append([f['code'], f['dn'], f['descr']]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def apic_disk_space_faults_check(index, total_checks, cversion, **kwargs): - title = 'APIC Disk Space Usage (F1527, F1528, F1529 equipment-full)' +@check_wrapper(check_title="APIC Disk Space Usage (F1527, F1528, F1529 equipment-full)") +def apic_disk_space_faults_check(cversion, **kwargs): result = FAIL_UF - msg = '' headers = ['Fault', 'Pod', 'Node', 'Mount Point', 'Current Usage %', 'Recommended Action'] data = [] unformatted_headers = ['Fault', 'Fault DN', 'Recommended Action'] unformatted_data = [] + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#apic-disk-space-usage" recommended_action = { '/firmware': 'Remove unneeded images', '/techsupport': 'Remove unneeded techsupports/cores' @@ -2438,7 +2518,6 @@ def apic_disk_space_faults_check(index, total_checks, cversion, **kwargs): default_action = 'Contact Cisco TAC.' if cversion.same_as('4.0(1h)') or cversion.older_than('3.2(6i)'): default_action += ' A typical issue is CSCvn13119.' - print_title(title, index, total_checks) dn_regex = node_regex + r'/.+p-\[(?P<mountpoint>.+)\]-f' desc_regex = r'is (?P<usage>\d{2}%) full' @@ -2457,20 +2536,25 @@ def apic_disk_space_faults_check(index, total_checks, cversion, **kwargs): unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn'], default_action]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + doc_url=doc_url, + ) -def l3out_route_map_direction_check(index, total_checks, **kwargs): +@check_wrapper(check_title="L3Out Route Map import/export direction") +def l3out_route_map_direction_check(**kwargs): """ Implementation change due to CSCvm75395 - 4.1(1) """ - title = 'L3Out Route Map import/export direction' result = FAIL_O - msg = '' headers = ["Tenant", "L3Out", "External EPG", "Subnet", "Subnet Scope", "Route Map", "Direction", "Recommended Action", ] data = [] recommended_action = 'The subnet scope must have {}-rtctrl' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#l3out-route-map-importexport-direction" dn_regex = r'uni/tn-(?P<tenant>[^/]+)/out-(?P<l3out>[^/]+)/instP-(?P<epg>[^/]+)/extsubnet-\[(?P<subnet>[^\]]+)\]' l3extSubnets = icurl('class', @@ -2487,24 +2571,20 @@ def l3out_route_map_direction_check(index, total_checks, **kwargs): data.append(basic + [rmap, dir, recommended_action.format(dir)]) if not data: result = PASS - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, doc_url=doc_url) -def l3out_route_map_missing_target_check(index, total_checks, cversion, tversion, **kwargs): +@check_wrapper(check_title="L3Out Route Map Match Rule with missing-target") +def l3out_route_map_missing_target_check(cversion, tversion, **kwargs): """ Implementation change due to CSCwc11570 - 5.2.8/6.0.2 """ - title = 'L3Out Route Map Match Rule with missing-target' result = FAIL_O - msg = '' headers = ['Tenant', 'L3Out', 'Route Map', 'Context', 'Action', 'Match Rule'] data = [] recommended_action = 'The configured match rules do not exist. Update the route maps with existing match rules.' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#l3out-route-map-match-rule-with-missing-target' - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) def is_old(v): return True if v.older_than("5.2(8a)") or v.simple_version == "6.0(1)" else False @@ -2512,8 +2592,7 @@ def is_old(v): c_is_old = is_old(cversion) t_is_old = is_old(tversion) if (c_is_old and t_is_old) or (not c_is_old and not t_is_old): - print_result(title, NA, func_name=inspect.currentframe().f_code.co_name) - return NA + return Result(result=NA, msg=VER_NOT_AFFECTED) dn_regex = r'uni/tn-(?P<tenant>[^/]+)/out-(?P<l3out>[^/]+)/' # Get a missing-target match rule in a route map with type `combinable` @@ -2541,19 +2620,16 @@ def is_old(v): ]) if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def l3out_overlapping_loopback_check(index, total_checks, **kwargs): - title = 'L3Out Loopback IP Overlap With L3Out Interfaces' +@check_wrapper(check_title="L3Out Loopback IP Overlap With L3Out Interfaces") +def l3out_overlapping_loopback_check(**kwargs): result = FAIL_O - msg = '' headers = ['Tenant:VRF', 'Node ID', 'Loopback IP (Tenant:L3Out:NodeP)', 'Interface IP (Tenant:L3Out:NodeP:IFP)'] data = [] recommended_action = 'Change either the loopback or L3Out interface IP subnet to avoid overlap.' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#l3out-loopback-ip-overlap-with-l3out-interfaces' - print_title(title, index, total_checks) tn_regex = r'uni/tn-(?P<tenant>[^/]+)/' path_regex = r'topology/pod-(?P<pod>\d+)/(?:prot)?paths-(?P<node1>\d+)(?:-(?P<node2>\d+))?' @@ -2668,19 +2744,17 @@ def l3out_overlapping_loopback_check(index, total_checks, **kwargs): ]) if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def bgp_peer_loopback_check(index, total_checks, **kwargs): +@check_wrapper(check_title="BGP Peer Profile at node level without Loopback") +def bgp_peer_loopback_check(**kwargs): """ Implementation change due to CSCvm28482 - 4.1(2) """ - title = 'BGP Peer Profile at node level without Loopback' result = FAIL_O - msg = '' - headers = ["Tenant", "L3Out", "Node Profile", "Pod", "Node", "Recommended Action"] + headers = ["Tenant", "L3Out", "Node Profile", "Pod", "Node"] data = [] recommended_action = 'Configure a loopback or configure bgpPeerP under interfaces instead of nodes' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#bgp-peer-profile-at-node-level-without-loopback" name_regex = r'uni/tn-(?P<tenant>[^/]+)/out-(?P<l3out>[^/]+)/lnodep-(?P<nodep>[^/]+)' l3extLNodePs = icurl('class', @@ -2707,23 +2781,21 @@ def bgp_peer_loopback_check(index, total_checks, **kwargs): dn = re.search(node_regex, l3extLNodeP_child['l3extRsNodeL3OutAtt']['attributes']['tDn']) data.append([ name.group('tenant'), name.group('l3out'), name.group('nodep'), - dn.group('pod'), dn.group('node'), recommended_action]) + dn.group('pod'), dn.group('node')]) if not data: result = PASS - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def lldp_with_infra_vlan_mismatch_check(index, total_checks, **kwargs): - title = 'Different infra VLAN via LLDP (F0454 infra-vlan-mismatch)' +@check_wrapper(check_title="Different infra VLAN via LLDP (F0454 infra-vlan-mismatch)") +def lldp_with_infra_vlan_mismatch_check(**kwargs): result = FAIL_O - msg = '' - headers = ["Fault", "Pod", "Node", "Port", "Recommended Action"] + headers = ["Fault", "Pod", "Node", "Port"] data = [] unformatted_headers = ["Fault", "Fault DN", "Failure Reason"] unformatted_data = [] recommended_action = 'Disable LLDP on this port if it is expected to receive LLDP with a mismatched infra VLAN' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#different-infra-vlan-via-lldp" dn_regex = node_regex + r'/sys/lldp/inst/if-\[(?P<port>eth\d{1,2}/\d{1,2})\]/fault-F0454' faultInsts = icurl('class', @@ -2732,27 +2804,33 @@ def lldp_with_infra_vlan_mismatch_check(index, total_checks, **kwargs): fc = faultInst['faultInst']['attributes']['code'] dn = re.search(dn_regex, faultInst['faultInst']['attributes']['dn']) if dn: - data.append([fc, dn.group("pod"), dn.group("node"), dn.group("port"), recommended_action]) + data.append([fc, dn.group("pod"), dn.group("node"), dn.group("port")]) else: - unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn'], recommended_action]) + unformatted_data.append([fc, faultInst['faultInst']['attributes']['dn']]) if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -# Connection based check -def apic_version_md5_check(index, total_checks, tversion, username, password, **kwargs): - title = 'APIC Target version image and MD5 hash' +# Connection Based Check +@check_wrapper(check_title="APIC Target version image and MD5 hash") +def apic_version_md5_check(tversion, username, password, **kwargs): result = FAIL_UF - msg = '' - headers = ['APIC', 'Firmware', 'md5sum', 'Failure', 'Recommended Action'] + headers = ['APIC', 'Firmware', 'md5sum', 'Failure'] data = [] recommended_action = 'Delete the firmware from APIC and re-download' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#apic-target-version-image-and-md5-hash" + if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) image_validaton = True mo = icurl('mo', 'fwrepo/fw-aci-apic-dk9.%s.json' % tversion.dot_version) @@ -2761,13 +2839,11 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** desc = fm_mo["firmwareFirmware"]['attributes']["description"] md5 = fm_mo["firmwareFirmware"]['attributes']["checksum"] if "Image signing verification failed" in desc: - data.append(["All", str(tversion), md5, - 'Target image is corrupted', 'Delete and Upload Again']) + data.append(["All", str(tversion), md5, 'Target image is corrupted']) image_validaton = False if not image_validaton: - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=FAIL_UF, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) md5s = [] md5_names = [] @@ -2789,7 +2865,7 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** c.connect() except Exception as e: data.append([apic_name, '-', '-', str(e), '-']) - print_result(node_title, ERROR, json_output=False) + print_result(node_title, ERROR) has_error = True continue @@ -2799,12 +2875,12 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** except Exception as e: data.append([apic_name, '-', '-', 'ls command via ssh failed due to:{}'.format(str(e)), '-']) - print_result(node_title, ERROR, json_output=False) + print_result(node_title, ERROR) has_error = True continue if "No such file or directory" in c.output: - data.append([apic_name, str(tversion), '-', 'image not found', recommended_action]) - print_result(node_title, FAIL_UF, json_output=False) + data.append([apic_name, str(tversion), '-', 'image not found']) + print_result(node_title, FAIL_UF) continue try: @@ -2813,12 +2889,12 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** except Exception as e: data.append([apic_name, str(tversion), '-', 'failed to check md5sum via ssh due to:{}'.format(str(e)), '-']) - print_result(node_title, ERROR, json_output=False) + print_result(node_title, ERROR) has_error = True continue if "No such file or directory" in c.output: - data.append([apic_name, str(tversion), '-', 'md5sum file not found', recommended_action]) - print_result(node_title, FAIL_UF, json_output=False) + data.append([apic_name, str(tversion), '-', 'md5sum file not found']) + print_result(node_title, FAIL_UF) continue for line in c.output.split("\n"): words = line.split() @@ -2830,33 +2906,32 @@ def apic_version_md5_check(index, total_checks, tversion, username, password, ** md5_names.append(apic_name) break else: - data.append([apic_name, str(tversion), '-', 'unexpected output when checking md5sum file', recommended_action]) - print_result(node_title, ERROR, json_output=False) + data.append([apic_name, str(tversion), '-', 'unexpected output when checking md5sum file']) + print_result(node_title, ERROR) has_error = True continue - print_result(node_title, DONE, json_output=False) + print_result(node_title, DONE) if len(set(md5s)) > 1: for name, md5 in zip(md5_names, md5s): - data.append([name, str(tversion), md5, 'md5sum do not match on all APICs', recommended_action]) + data.append([name, str(tversion), md5, 'md5sum do not match on all APICs']) if has_error: result = ERROR elif not data: result = PASS - print_result(title, result, msg, headers, data, adjust_title=True, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url, adjust_title=True) # Connection Based Check -def standby_apic_disk_space_check(index, total_checks, **kwargs): - title = 'Standby APIC Disk Space Usage' +@check_wrapper(check_title="Standby APIC Disk Space Usage") +def standby_apic_disk_space_check(**kwargs): result = FAIL_UF msg = '' - headers = ['SN', 'OOB', 'Mount Point', 'Current Usage %', 'Recommended Action'] + headers = ['SN', 'OOB', 'Mount Point', 'Current Usage %'] data = [] recommended_action = 'Contact Cisco TAC' + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#standby-apic-disk-space-usage" threshold = 75 # usage (%) - print_title(title, index, total_checks) has_error = False checked_stby = [] @@ -2891,7 +2966,7 @@ def standby_apic_disk_space_check(index, total_checks, **kwargs): directory = fs.group(1) usage = fs.group(5) if int(usage) >= threshold: - data.append([stb['mbSn'], stb['oobIpAddr'], directory, usage, recommended_action]) + data.append([stb['mbSn'], stb['oobIpAddr'], directory, usage]) if not infraSnNodes: result = NA msg = 'No standby APIC found' @@ -2900,123 +2975,108 @@ def standby_apic_disk_space_check(index, total_checks, **kwargs): elif not data: result = PASS msg = 'all below {}%'.format(threshold) - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def r_leaf_compatibility_check(index, total_checks, tversion, **kwargs): - title = 'Remote Leaf Compatibility' +@check_wrapper(check_title="Remote Leaf Compatibility") +def r_leaf_compatibility_check(tversion, **kwargs): result = PASS - msg = '' - headers = ['Target Version', 'Remote Leaf', 'Direct Traffic Forwarding', 'Recommended Action'] + headers = ['Target Version', 'Remote Leaf', 'Direct Traffic Forwarding'] data = [] recommended_action_4_2_2 = 'Upgrade remote leaf nodes before spine nodes or\ndisable Direct Traffic Forwarding (CSCvs16767)' recommended_action_5a = 'Direct Traffic Forwarding is required on 5.0 or later. Enable the feature before the upgrade' recommended_action_5b = ('Direct Traffic Forwarding is required on 5.0 or later.\n' 'Upgrade to 4.1(2)-4.2(x) first to enable the feature before upgrading to 5.0 or later.') - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#compatibility-remote-leaf-switch" if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) remote_leafs = icurl('class', 'fabricNode.json?&query-target-filter=eq(fabricNode.nodeType,"remote-leaf-wan")') if not remote_leafs: - result = NA - msg = 'No Remote Leaf Found' - else: - infraSetPols = icurl('mo', 'uni/infra/settings.json') - direct = infraSetPols[0]['infraSetPol']['attributes'].get('enableRemoteLeafDirect') - direct_enabled = 'Not Supported' - if direct: - direct_enabled = direct == 'yes' - - ra = '' - if tversion.simple_version == "4.2(2)" and direct_enabled is True: - ra = recommended_action_4_2_2 - elif int(tversion.major1) >= 5 and direct_enabled is False: - ra = recommended_action_5a - elif int(tversion.major1) >= 5 and direct_enabled == 'Not Supported': - ra = recommended_action_5b - if ra: - result = FAIL_O - data.append([str(tversion), "Present", direct_enabled, ra]) - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=NA, msg="No Remote Leaf Found") + + infraSetPols = icurl('mo', 'uni/infra/settings.json') + direct = infraSetPols[0]['infraSetPol']['attributes'].get('enableRemoteLeafDirect') + direct_enabled = 'Not Supported' + if direct: + direct_enabled = direct == 'yes' + + ra = '' + if tversion.simple_version == "4.2(2)" and direct_enabled is True: + ra = recommended_action_4_2_2 + elif int(tversion.major1) >= 5 and direct_enabled is False: + ra = recommended_action_5a + elif int(tversion.major1) >= 5 and direct_enabled == 'Not Supported': + ra = recommended_action_5b + if ra: + result = FAIL_O + data.append([str(tversion), "Present", direct_enabled]) + return Result(result=result, headers=headers, data=data, recommended_action=ra, doc_url=doc_url) -def ep_announce_check(index, total_checks, cversion, tversion, **kwargs): - title = 'EP Announce Compatibility' +@check_wrapper(check_title="EP Announce Compatibility") +def ep_announce_check(cversion, tversion, **kwargs): result = PASS - msg = '' headers = ['Susceptible Defect', 'Recommended Action'] data = [] recommended_action = ('For fabrics running a pre-12.2(4p) ACI switch release, ' 'upgrade to 12.2(4r) and then upgrade to the desired destination release.\n' 'For fabrics running a 12.3(1) ACI switch release, ' 'upgrade to 13.1(2v) and then upgrade to the desired destination release.') - print_title(title, index, total_checks) fixed_versions = ["2.2(4p)", "2.2(4q)", "2.2(4r)"] current_version_affected = False target_version_affected = False if not tversion: - result = MANUAL - msg = 'Target version not supplied. Skipping.' - else: - if cversion.version not in fixed_versions and int(cversion.major1) < 3: - current_version_affected = True + return Result(result=MANUAL, msg=TVER_MISSING) - if tversion.major1 == "3": - if int(tversion.major2) >= 2 and int(tversion.maint) >= 2: - target_version_affected = True - elif int(tversion.major1) >= 4: + if cversion.version not in fixed_versions and int(cversion.major1) < 3: + current_version_affected = True + + if tversion.major1 == "3": + if int(tversion.major2) >= 2 and int(tversion.maint) >= 2: target_version_affected = True + elif int(tversion.major1) >= 4: + target_version_affected = True - if current_version_affected and target_version_affected: - result = FAIL_O - data.append(['CSCvi76161', recommended_action]) - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + if current_version_affected and target_version_affected: + result = FAIL_O + data.append(['CSCvi76161', recommended_action]) + return Result(result=result, headers=headers, data=data) -def vmm_controller_status_check(index, total_checks, **kwargs): - title = 'VMM Domain Controller Status' +@check_wrapper(check_title="VMM Domain Controller Status") +def vmm_controller_status_check(**kwargs): result = PASS - msg = '' - headers = ['VMM Domain', 'vCenter IP or Hostname', 'Current State', 'Recommended Action'] + headers = ['VMM Domain', 'vCenter IP or Hostname', 'Current State'] data = [] recommended_action = 'Check network connectivity to the vCenter.' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#vmm-domain-controller-status" vmmDoms = icurl('class', 'compCtrlr.json') if not vmmDoms: - result = NA - msg = 'No VMM Domains Found' - else: - for dom in vmmDoms: - if dom['compCtrlr']['attributes']['operSt'] == "offline": - domName = dom['compCtrlr']['attributes']['domName'] - hostOrIp = dom['compCtrlr']['attributes']['hostOrIp'] - result = FAIL_O - data.append([domName, hostOrIp, "offline", recommended_action]) - - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=NA, msg='No VMM Domains Found') + for dom in vmmDoms: + if dom['compCtrlr']['attributes']['operSt'] == "offline": + domName = dom['compCtrlr']['attributes']['domName'] + hostOrIp = dom['compCtrlr']['attributes']['hostOrIp'] + result = FAIL_O + data.append([domName, hostOrIp, "offline"]) + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def vmm_controller_adj_check(index, total_checks, **kwargs): - title = 'VMM Domain LLDP/CDP Adjacency Status' +@check_wrapper(check_title="VMM Domain LLDP/CDP Adjacency Status") +def vmm_controller_adj_check(**kwargs): result = PASS msg = '' - headers = ['VMM Domain', 'Host IP or Hostname', 'Recommended Action'] + headers = ['VMM Domain', 'Host IP or Hostname'] data = [] - unformatted_headers = ['Fault', 'Fault DN', 'Recommended Action'] + unformatted_headers = ['Fault', 'Fault DN'] unformatted_data = [] - recommended_action = 'Ensure consistent use of expected Discovery Protocol from Hypervisor to ACI Leaf.' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#vmm-domain-lldpcdp-adjacency-status" adjFaults = icurl('class', 'faultInst.json?query-target-filter=eq(faultInst.code,"F606391")') adj_regex = r'adapters on the host: (?P<host>[^\(]+)' @@ -3029,59 +3089,53 @@ def vmm_controller_adj_check(index, total_checks, **kwargs): if "prov-VMware" in adj['faultInst']['attributes']['dn']: r1 = re.search(adj_regex, adj['faultInst']['attributes']['descr']) r2 = re.search(dom_reg, adj['faultInst']['attributes']['dn']) + result = FAIL_O if r1 and r2: host = r1.group("host") dom = r2.group("dom") - result = FAIL_O - data.append([dom, host, recommended_action]) + data.append([dom, host]) else: - unformatted_data.append( - [adj['faultInst']['attributes']['code'], adj['faultInst']['attributes']['dn'], - recommended_action]) - - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, func_name=inspect.currentframe().f_code.co_name) - return result + unformatted_data.append([adj['faultInst']['attributes']['code'], adj['faultInst']['attributes']['dn']]) + return Result( + result=result, + msg=msg, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def vpc_paired_switches_check(index, total_checks, vpc_node_ids=None, **kwargs): - title = 'VPC-paired Leaf switches' - result = FAIL_O - msg = '' - headers = ["Node ID", "Node Name", "Recommended Action"] +@check_wrapper(check_title="VPC-paired Leaf switches") +def vpc_paired_switches_check(vpc_node_ids, **kwargs): + result = PASS + headers = ["Node ID", "Node Name"] data = [] - recommended_action = 'Determine if dataplane redundancy is available if this node goes down' + recommended_action = 'Determine if dataplane redundancy is available if these nodes go down.' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#vpc-paired-leaf-switches' - print_title(title, index, total_checks) - - if not vpc_node_ids: - msg = 'No VPC definitions found!' - vpc_node_ids = [] top_system = icurl('class', 'topSystem.json') - for node in top_system: node_id = node['topSystem']['attributes']['id'] role = node['topSystem']['attributes']['role'] if role == 'leaf' and (node_id not in vpc_node_ids): result = MANUAL name = node['topSystem']['attributes']['name'] - data.append([node_id, name, recommended_action]) + data.append([node_id, name]) - if not data: - result = PASS - print_result(title, result, msg, headers, data, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def cimc_compatibilty_check(index, total_checks, tversion, **kwargs): - title = 'APIC CIMC Compatibility' +@check_wrapper(check_title="APIC CIMC Compatibility") +def cimc_compatibilty_check(tversion, **kwargs): result = FAIL_UF - msg = '' headers = ["Node ID", "Model", "Current CIMC version", "Catalog Recommended CIMC Version", "Warning"] data = [] recommended_action = 'Check Release note of APIC Model/version for latest recommendations.' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#compatibility-cimc-version' - print_title(title, index, total_checks) + apic_obj = icurl('class', 'eqptCh.json?query-target-filter=wcard(eqptCh.descr,"APIC")') if apic_obj and tversion: try: @@ -3106,25 +3160,21 @@ def cimc_compatibilty_check(index, total_checks, tversion, **kwargs): result = PASS except KeyError: - result = MANUAL - msg = 'eqptCh does not have cimcVersion parameter on this version' + return Result(result=MANUAL, msg="eqptCh does not have cimcVersion parameter on this version", headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) else: - result = MANUAL - msg = 'Target version not supplied. Skipping.' + return Result(result=MANUAL, msg=TVER_MISSING) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def intersight_upgrade_status_check(index, total_checks, **kwargs): - title = 'Intersight Device Connector upgrade status' +@check_wrapper(check_title="Intersight Device Connector upgrade status") +def intersight_upgrade_status_check(**kwargs): result = FAIL_UF msg = '' - headers = ["Connector Status", "Recommended Action"] + headers = ["Connector Status"] data = [] recommended_action = 'Wait a few minutes for the upgrade to complete' - doc_url = '"Intersight Device Connector is upgrading" in Pre-Upgrade Check Lists' - print_title(title, index, total_checks) + doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#intersight-device-connector-upgrade-status' cmd = ['icurl', '-gks', 'https://127.0.0.1/connector/UpgradeStatus'] @@ -3135,7 +3185,7 @@ def intersight_upgrade_status_check(index, total_checks, **kwargs): try: if resp_json[0]['Status'] != 'Idle': - data.append([resp_json[0]['UpgradeNotification'], recommended_action]) + data.append([resp_json[0]['UpgradeNotification']]) except KeyError: if resp_json['code'] == 'InternalServerError': msg = 'Connector reporting InternalServerError, Non-Upgrade issue' @@ -3147,19 +3197,16 @@ def intersight_upgrade_status_check(index, total_checks, **kwargs): result = NA msg = 'Intersight Device Connector not responding' - print_result(title, result, msg, headers, data, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def isis_redis_metric_mpod_msite_check(index, total_checks, **kwargs): - title = 'ISIS Redistribution metric for MPod/MSite' +@check_wrapper(check_title="ISIS Redistribution metric for MPod/MSite") +def isis_redis_metric_mpod_msite_check(**kwargs): result = FAIL_O - msg = '' - headers = ["ISIS Redistribution Metric", "MPod Deployment", "MSite Deployment", "Recommendation"] + headers = ["ISIS Redistribution Metric", "MPod Deployment", "MSite Deployment"] data = [] - recommended_action = None - doc_url = '"ISIS Redistribution Metric" from ACI Best Practices Quick Summary - http://cs.co/9001zNNr7' - print_title(title, index, total_checks) + recommended_action = "" + doc_url = 'http://cs.co/9001zNNr7' # "ISIS Redistribution Metric" from ACI Best Practices Quick Summary isis_mo = icurl('mo', 'uni/fabric/isisDomP-default.json') redistribMetric = isis_mo[0]['isisDomPol']['attributes'].get('redistribMetric') @@ -3187,26 +3234,22 @@ def isis_redis_metric_mpod_msite_check(index, total_checks, **kwargs): pods_list.append(podid) mpod = (len(pods_list) > 1) if mpod or msite: - data.append([redistribMetric, mpod, msite, recommended_action]) + data.append([redistribMetric, mpod, msite]) if not data: result = PASS - print_result(title, result, msg, headers, data, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def bgp_golf_route_target_type_check(index, total_checks, cversion=None, tversion=None, **kwargs): - title = 'BGP route target type for GOLF over L2EVPN' +@check_wrapper(check_title="BGP route target type for GOLF over L2EVPN") +def bgp_golf_route_target_type_check(cversion, tversion, **kwargs): result = FAIL_O - msg = '' - headers = ["VRF DN", "Global Name", "Route Target", "Recommendation"] + headers = ["VRF DN", "Global Name", "Route Target"] data = [] recommended_action = "Reconfigure extended: RT with prefix route-target: " doc_url = 'https://bst.cloudapps.cisco.com/bugsearch/bug/CSCvm23100' - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if cversion.older_than("4.2(1a)") and tversion.newer_than("4.2(1a)"): fvctx_mo = icurl('class', 'fvCtx.json?rsp-subtree=full&rsp-subtree-class=l3extGlobalCtxName,bgpRtTarget&rsp-subtree-include=required') @@ -3223,22 +3266,19 @@ def bgp_golf_route_target_type_check(index, total_checks, cversion=None, tversio if child.get('bgpRtTargetP'): for bgprt in child['bgpRtTargetP']['children']: if bgprt.get('bgpRtTarget') and not bgprt['bgpRtTarget']['attributes']['rt'].startswith('route-target:'): - data.append([vrfdn, globalname, bgprt['bgpRtTarget']['attributes']['rt'], recommended_action]) - + data.append([vrfdn, globalname, bgprt['bgpRtTarget']['attributes']['rt']]) if not data: result = PASS - print_result(title, result, msg, headers, data, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def docker0_subnet_overlap_check(index, total_checks, **kwargs): - title = 'APIC Container Bridge IP Overlap with APIC TEP' +@check_wrapper(check_title="APIC Container Bridge IP Overlap with APIC TEP") +def docker0_subnet_overlap_check(**kwargs): result = PASS - msg = '' - headers = ["Container Bridge IP", "APIC TEP", "Recommended Action"] + headers = ["Container Bridge IP", "APIC TEP"] data = [] recommended_action = 'Change the container bridge IP via "Apps > Settings" on the APIC GUI' - print_title(title, index, total_checks) + doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#apic-container-bridge-ip-overlap-with-apic-tep" containerPols = icurl('mo', 'pluginPolContr/ContainerPol.json') if not containerPols: @@ -3255,21 +3295,18 @@ def docker0_subnet_overlap_check(index, total_checks, **kwargs): for tep in teps: if IPAddress.ip_in_subnet(tep, bip): result = FAIL_UF - data.append([tep, bip, recommended_action]) + data.append([tep, bip]) - print_result(title, result, msg, headers, data, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def eventmgr_db_defect_check(index, total_checks, cversion, **kwargs): - title = 'Eventmgr DB size defect susceptibility' +@check_wrapper(check_title="Eventmgr DB size defect susceptibility") +def eventmgr_db_defect_check(cversion, **kwargs): result = PASS - msg = '' headers = ["Potential Defect", "Doc URL"] data = [] recommended_action = 'Contact Cisco TAC to check the DB size via root' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#eventmgr-db-size-defect-susceptibility' - print_title(title, index, total_checks) if cversion.older_than('3.2(5d)') or (cversion.major1 == '4' and cversion.older_than('4.1(1i)')): data.append(['CSCvn20175', 'https://bst.cloudapps.cisco.com/bugsearch/bug/CSCvn20175']) @@ -3279,112 +3316,92 @@ def eventmgr_db_defect_check(index, total_checks, cversion, **kwargs): if data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def target_version_compatibility_check(index, total_checks, cversion, tversion, **kwargs): - title = 'Target version compatibility' +@check_wrapper(check_title="Target version compatibility") +def target_version_compatibility_check(cversion, tversion, **kwargs): result = FAIL_UF - msg = '' headers = ["Current version", "Target Version", "Warning"] data = [] recommended_action = '' doc_url = 'APIC Upgrade/Downgrade Support Matrix - http://cs.co/9005ydMQP' - print_title(title, index, total_checks) - if not tversion: - result = MANUAL - msg = 'Target version not supplied. Skipping.' - else: - if cversion.simple_version != tversion.simple_version: - compatRsUpgRelString = "uni/fabric/compcat-default/ctlrfw-apic-" + cversion.simple_version + \ - "/rsupgRel-[uni/fabric/compcat-default/ctlrfw-apic-" + tversion.simple_version + "].json" - compatRsUpgRel = icurl('mo', compatRsUpgRelString) - if not compatRsUpgRel: - compatRtUpgRelString = "uni/fabric/compcat-default/ctlrfw-apic-" + cversion.simple_version + \ - "/rtupgRel-[uni/fabric/compcat-default/ctlrfw-apic-" + tversion.simple_version + "].json" - compatRtUpgRel = icurl('mo', compatRtUpgRelString) - if not compatRtUpgRel: - data.append([str(cversion), str(tversion), 'Target version not a supported hop']) - if not data: - result = PASS + if not tversion: + return Result(result=MANUAL, msg=TVER_MISSING) + if cversion.simple_version != tversion.simple_version: + compatRsUpgRelString = "uni/fabric/compcat-default/ctlrfw-apic-" + cversion.simple_version + \ + "/rsupgRel-[uni/fabric/compcat-default/ctlrfw-apic-" + tversion.simple_version + "].json" + compatRsUpgRel = icurl('mo', compatRsUpgRelString) + if not compatRsUpgRel: + compatRtUpgRelString = "uni/fabric/compcat-default/ctlrfw-apic-" + cversion.simple_version + \ + "/rtupgRel-[uni/fabric/compcat-default/ctlrfw-apic-" + tversion.simple_version + "].json" + compatRtUpgRel = icurl('mo', compatRtUpgRelString) + if not compatRtUpgRel: + data.append([str(cversion), str(tversion), 'Target version not a supported hop']) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + if not data: + result = PASS + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def gen1_switch_compatibility_check(index, total_checks, tversion, **kwargs): - title = 'Gen 1 switch compatibility' +@check_wrapper(check_title="Gen 1 switch compatibility") +def gen1_switch_compatibility_check(tversion, **kwargs): result = FAIL_UF - msg = '' headers = ["Target Version", "Node ID", "Model", "Warning"] gen1_models = ["N9K-C9336PQ", "N9K-X9736PQ", "N9K-C9504-FM", "N9K-C9508-FM", "N9K-C9516-FM", "N9K-C9372PX-E", "N9K-C9372TX-E", "N9K-C9332PQ", "N9K-C9372PX", "N9K-C9372TX", "N9K-C9396PX", "N9K-C9396TX", "N9K-C93128TX"] data = [] recommended_action = 'Select supported target version or upgrade hardware' - doc_url = 'ACI 5.0(1) Switch Release Notes - http://cs.co/9001ydKCV' - print_title(title, index, total_checks) - if not tversion: - result = MANUAL - msg = 'Target version not supplied. Skipping.' - else: - if tversion.newer_than("5.0(1a)"): - fabric_node = icurl('class', 'fabricNode.json') - for node in fabric_node: - if node['fabricNode']['attributes']['model'] in gen1_models: - data.append([str(tversion), node['fabricNode']['attributes']['id'], - node['fabricNode']['attributes']['model'], 'Not supported on 5.x+']) - - if not data: - result = PASS + doc_url = 'http://cs.co/9001ydKCV' - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + if not tversion: + return Result(result=MANUAL, msg=TVER_MISSING) + if tversion.newer_than("5.0(1a)"): + fabric_node = icurl('class', 'fabricNode.json') + for node in fabric_node: + if node['fabricNode']['attributes']['model'] in gen1_models: + data.append([str(tversion), node['fabricNode']['attributes']['id'], + node['fabricNode']['attributes']['model'], 'Not supported on 5.x+']) + if not data: + result = PASS + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def contract_22_defect_check(index, total_checks, cversion, tversion, **kwargs): - title = 'Contract Port 22 Defect' +@check_wrapper(check_title="Contract Port 22 Defect") +def contract_22_defect_check(cversion, tversion, **kwargs): result = PASS - msg = '' headers = ["Potential Defect", "Reason"] data = [] - recommended_action = 'Review Software Advisory for details' - doc_url = 'Cisco Software Advisory Notices for CSCvz65560 - http://cs.co/9007yh22H' - print_title(title, index, total_checks) + recommended_action = 'Review Cisco Software Advisory Notices for CSCvz65560' + doc_url = 'http://cs.co/9007yh22H' if not tversion: - result = MANUAL - msg = 'Target version not supplied. Skipping.' - else: - if cversion.older_than("5.0(1a)") and (tversion.newer_than("5.0(1a)") and - tversion.older_than("5.2(2g)")): - result = FAIL_O - data.append(["CSCvz65560", "Target Version susceptible to Defect"]) + return Result(result=MANUAL, msg=TVER_MISSING) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + if cversion.older_than("5.0(1a)") and (tversion.newer_than("5.0(1a)") and + tversion.older_than("5.2(2g)")): + result = FAIL_O + data.append(["CSCvz65560", "Target Version susceptible to Defect"]) + + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def llfc_susceptibility_check(index, total_checks, cversion=None, tversion=None, vpc_node_ids=None, **kwargs): - title = 'Link Level Flow Control' +@check_wrapper(check_title="Link Level Flow Control") +def llfc_susceptibility_check(cversion, tversion, vpc_node_ids, **kwargs): result = PASS - msg = '' headers = ["Pod", "NodeId", "Int", "Type", "BugId", "Warning"] data = [] sx_affected = t_affected = False recommended_action = 'Manually change Peer devices Transmit(send) Flow Control to off prior to switch Upgrade' doc_url = 'https://bst.cloudapps.cisco.com/bugsearch/bug/CSCvo27498' - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if not vpc_node_ids: - print_result(title, result, 'No VPC Nodes found. Not susceptible.', func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=PASS, msg="No VPC Nodes found. Not susceptible.") # Check for Fiber 1000base-SX, CSCvv33100 if cversion.older_than("4.2(6d)") and tversion.newer_than("4.2(6c)"): @@ -3415,27 +3432,22 @@ def llfc_susceptibility_check(index, total_checks, cversion=None, tversion=None, if data: result = MANUAL - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def telemetryStatsServerP_object_check(index, total_checks, sw_cversion=None, tversion=None, **kwargs): - title = 'telemetryStatsServerP Object' +@check_wrapper(check_title="telemetryStatsServerP Object") +def telemetryStatsServerP_object_check(sw_cversion, tversion, **kwargs): result = PASS - msg = '' headers = ["Current version", "Target Version", "Warning"] data = [] recommended_action = 'Change telemetryStatsServerP.collectorLocation to "none" prior to upgrade' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#telemetrystatserverp-object' - print_title(title, index, total_checks) if not sw_cversion: - print_result(title, MANUAL, "Current switch version not found. Check switch health.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg="Current switch version not found. Check switch health.") if not tversion: - print_result(title, MANUAL, 'Current or target Switch version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if sw_cversion.older_than("4.2(4d)") and tversion.newer_than("5.2(2d)"): telemetryStatsServerP_json = icurl('class', 'telemetryStatsServerP.json') @@ -3444,23 +3456,19 @@ def telemetryStatsServerP_object_check(index, total_checks, sw_cversion=None, tv result = FAIL_O data.append([str(sw_cversion), str(tversion), 'telemetryStatsServerP.collectorLocation = "apic" Found']) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def internal_vlanpool_check(index, total_checks, tversion=None, **kwargs): - title = 'Internal VLAN Pool' +@check_wrapper(check_title="Internal VLAN Pool") +def internal_vlanpool_check(tversion, **kwargs): result = PASS - msg = '' headers = ["VLAN Pool", "Internal VLAN Block(s)", "Non-AVE Domain", "Warning"] data = [] recommended_action = 'Ensure Leaf Front-Panel VLAN Blocks are explicitly set to "external (on the wire)"' doc_url = 'https://bst.cloudapps.cisco.com/bugsearch/bug/CSCvw33061' - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if tversion.newer_than("4.2(6a)"): fvnsVlanInstP_json = icurl('class', 'fvnsVlanInstP.json?rsp-subtree=children&rsp-subtree-class=fvnsRtVlanNs,fvnsEncapBlk&rsp-subtree-include=required') @@ -3508,19 +3516,16 @@ def internal_vlanpool_check(index, total_checks, tversion=None, **kwargs): if [vlanInstP_name, ', '.join(encap_blk_dict[vlanInstP_name]), vmmDomP["vmmDomP"]["attributes"]["dn"], 'VLANs in this Block will be removed from switch Front-Panel if not corrected'] not in data: data.append([vlanInstP_name, ', '.join(encap_blk_dict[vlanInstP_name]), vmmDomP["vmmDomP"]["attributes"]["dn"], 'VLANs in this Block will be removed from switch Front-Panel if not corrected']) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def apic_ca_cert_validation(index, total_checks, **kwargs): - title = 'APIC CA Cert Validation' +@check_wrapper(check_title="APIC CA Cert Validation") +def apic_ca_cert_validation(**kwargs): result = FAIL_O - msg = '' headers = ["Certreq Response"] data = [] recommended_action = "Contact Cisco TAC to fix APIC CA Certs" doc_url = 'https://bst.cloudapps.cisco.com/bugsearch/bug/CSCvy35257' - print_title(title, index, total_checks) certreq_out = kwargs.get("certreq_out") if not certreq_out: @@ -3568,8 +3573,7 @@ def apic_ca_cert_validation(index, total_checks, **kwargs): genrsa_proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) genrsa_proc.communicate()[0].strip() if genrsa_proc.returncode != 0: - print_result(title, ERROR, 'openssl cmd issue, send logs to TAC', func_name=inspect.currentframe().f_code.co_name) - return ERROR + return Result(result=ERROR, msg="openssl cmd issue, send logs to TAC.") # Prep certreq with open(sign) as f: @@ -3596,30 +3600,24 @@ def apic_ca_cert_validation(index, total_checks, **kwargs): if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def fabricdomain_name_check(index, total_checks, cversion, tversion, **kwargs): - title = 'FabricDomain Name' +@check_wrapper(check_title="FabricDomain Name") +def fabricdomain_name_check(cversion, tversion, **kwargs): result = FAIL_O - msg = '' headers = ["FabricDomain", "Reason"] data = [] recommended_action = "Do not upgrade to 6.0(2)" doc_url = 'https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwf80352' - print_title(title, index, total_checks) - if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if tversion.same_as("6.0(2h)"): controller = icurl('class', 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")') if not controller: - print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?', func_name=inspect.currentframe().f_code.co_name) - return ERROR + return Result(result=ERROR, msg='topSystem response empty. Is the cluster healthy?') fabricDomain = controller[0]['topSystem']['attributes']['fabricDomain'] if re.search(r'#|;', fabricDomain): @@ -3627,25 +3625,21 @@ def fabricdomain_name_check(index, total_checks, cversion, tversion, **kwargs): if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def sup_hwrev_check(index, total_checks, cversion, tversion, **kwargs): - title = 'Spine SUP HW Revision' +@check_wrapper(check_title="Spine SUP HW Revision") +def sup_hwrev_check(cversion, tversion, **kwargs): result = FAIL_O - msg = '' headers = ["Pod", "Node", "Sup Slot", "Part Number", "VRM Concern", "FPGA Concern"] data = [] recommended_action = "Review Field Notice FN74050 within Reference Document for all details." doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#spine-sup-hw-revision' - print_title(title, index, total_checks) vrm_concern = False fpga_concern = False if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if cversion.older_than("5.2(8f)"): vrm_concern = True @@ -3662,8 +3656,7 @@ def sup_hwrev_check(index, total_checks, cversion, tversion, **kwargs): sup_re = r'/.+(?P<supslot>supslot-\d+)' sups = icurl('class', 'eqptSpCmnBlk.json?&query-target-filter=wcard(eqptSpromSupBlk.dn,"sup")') if not sups: - print_result(title, ERROR, 'No sups found. This is unlikely.', func_name=inspect.currentframe().f_code.co_name) - return ERROR + return Result(result=ERROR, msg='No sups found. This is unlikely.') for sup in sups: prtNum = sup['eqptSpCmnBlk']['attributes']['prtNum'] @@ -3676,24 +3669,19 @@ def sup_hwrev_check(index, total_checks, cversion, tversion, **kwargs): if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def uplink_limit_check(index, total_checks, cversion, tversion, **kwargs): - title = 'Per-Leaf Fabric Uplink Limit Validation' +@check_wrapper(check_title="Per-Leaf Fabric Uplink Limit") +def uplink_limit_check(cversion, tversion, **kwargs): result = PASS - msg = '' headers = ["Node", "Uplink Count"] data = [] recommended_action = "Reduce Per-Leaf Port Profile Uplinks to supported scale; 56 or less." doc_url = 'http://cs.co/ACI_Access_Interfaces_Config_Guide' - print_title(title, index, total_checks) - if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if cversion.older_than("6.0(1a)") and tversion.newer_than("6.0(1a)"): port_profiles = icurl('class', 'eqptPortP.json?query-target-filter=eq(eqptPortP.ctrl,"uplink")') @@ -3711,15 +3699,13 @@ def uplink_limit_check(index, total_checks, cversion, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def oob_mgmt_security_check(index, total_checks, cversion, tversion, **kwargs): +@check_wrapper(check_title="OoB Mgmt Security") +def oob_mgmt_security_check(cversion, tversion, **kwargs): """Implementation change due to CSCvx29282/CSCvz96117""" - title = "OoB Mgmt Security" result = PASS - msg = "" headers = ["ACI Node EPG", "External Instance (Subnets)", "OoB Contracts"] data = [] recommended_action = ( @@ -3728,19 +3714,15 @@ def oob_mgmt_security_check(index, total_checks, cversion, tversion, **kwargs): ) doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#oob-mgmt-security" - print_title(title, index, total_checks) - if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) affected_versions = ["4.2(7)", "5.2(1)", "5.2(2)"] if cversion.simple_version not in affected_versions or ( cversion.simple_version in affected_versions and tversion.simple_version in affected_versions ): - print_result(title, NA, func_name=inspect.currentframe().f_code.co_name) - return NA + return Result(result=NA, msg=VER_NOT_AFFECTED) # ACI Node EPGs (providers) mgmtOoBs = icurl("class", "mgmtOoB.json?rsp-subtree=children") @@ -3779,30 +3761,24 @@ def oob_mgmt_security_check(index, total_checks, cversion, tversion, **kwargs): if data: result = MANUAL - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def mini_aci_6_0_2_check(index, total_checks, cversion, tversion, **kwargs): - title = 'Mini ACI Upgrade to 6.0(2)+' +@check_wrapper(check_title="Mini ACI Upgrade to 6.0(2)+") +def mini_aci_6_0_2_check(cversion, tversion, **kwargs): result = FAIL_UF - msg = '' headers = ["Pod ID", "Node ID", "APIC Type", "Failure Reason"] data = [] recommended_action = "All virtual APICs must be removed from the cluster prior to upgrading to 6.0(2)+." doc_url = 'Upgrading Mini ACI - http://cs.co/9009bBTQB' - print_title(title, index, total_checks) - if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if cversion.older_than("6.0(2a)") and tversion.newer_than("6.0(2a)"): topSystem = icurl('class', 'topSystem.json?query-target-filter=wcard(topSystem.role,"controller")') if not topSystem: - print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?', func_name=inspect.currentframe().f_code.co_name) - return ERROR + return Result(result=ERROR, msg='topSystem response empty. Is the cluster healthy?') for controller in topSystem: if controller['topSystem']['attributes']['nodeType'] == "virtual": pod_id = controller["topSystem"]["attributes"]["podId"] @@ -3811,24 +3787,19 @@ def mini_aci_6_0_2_check(index, total_checks, cversion, tversion, **kwargs): if not data: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def sup_a_high_memory_check(index, total_checks, tversion, **kwargs): - title = "SUP-A/A+ High Memory Usage" +@check_wrapper(check_title="SUP-A/A+ High Memory Usage") +def sup_a_high_memory_check(tversion, **kwargs): result = PASS - msg = "" headers = ["Pod ID", "Node ID", "SUP Model", "Active/Standby"] data = [] recommended_action = "Change the target version to the one with memory optimization in a near-future 6.0 release." doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#sup-aa-high-memory-usage" - print_title(title, index, total_checks) - if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) affected_versions = ["6.0(3)", "6.0(4)", "6.0(5)"] if tversion.simple_version in affected_versions: @@ -3844,21 +3815,18 @@ def sup_a_high_memory_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def access_untagged_check(index, total_checks, **kwargs): - title = 'Access (Untagged) Port Config (F0467 native-or-untagged-encap-failure)' +@check_wrapper(check_title="Access (Untagged) Port Config (F0467 native-or-untagged-encap-failure)") +def access_untagged_check(**kwargs): result = FAIL_O - msg = '' headers = ["Fault", "POD ID", "Node ID", "Port", "Tenant", "Application Profile", "Application EPG", "Recommended Action"] unformatted_headers = ['Fault', 'Fault Description', 'Recommended Action'] unformatted_data = [] data = [] recommended_action = 'Resolve the conflict by removing this config or other configs using this port in Access(untagged) or native mode.' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#access-untagged-port-config' - print_title(title, index, total_checks) faultInsts = icurl('class', 'faultInst.json?&query-target-filter=wcard(faultInst.changeSet,"native-or-untagged-encap-failure")') fault_dn_regex = r"topology/pod-(?P<podid>\d+)/node-(?P<nodeid>[^/]+)/[^/]+/[^/]+/uni/epp/fv-\[uni/tn-(?P<tenant>[^/]+)/ap-(?P<app_profile>[^/]+)/epg-(?P<epg_name>[^/]+)\]/[^/]+/stpathatt-\[(?P<port>.+)\]/nwissues/fault-F0467" @@ -3880,19 +3848,24 @@ def access_untagged_check(index, total_checks, **kwargs): if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action="", doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def post_upgrade_cb_check(index, total_checks, cversion, tversion, **kwargs): - title = 'Post Upgrade Callback Integrity' +@check_wrapper(check_title="Post Upgrade Callback Integrity") +def post_upgrade_cb_check(cversion, tversion, **kwargs): result = PASS - msg = '' headers = ["Missed Objects", "Impact"] data = [] recommended_action = 'Contact Cisco TAC with Output' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#post-upgrade-callback-integrity' - print_title(title, index, total_checks) new_mo_dict = { "infraImplicitSetPol": { @@ -3932,8 +3905,7 @@ def post_upgrade_cb_check(index, total_checks, cversion, tversion, **kwargs): }, } if not tversion or (tversion and cversion.older_than(str(tversion))): - print_result(title, POST, 'Re-run script after APICs are upgraded and back to Fully-Fit', func_name=inspect.currentframe().f_code.co_name) - return POST + return Result(result=POST, msg="Re-run script after APICs are upgraded and back to Fully-Fit") for new_mo in new_mo_dict: skip_current_mo = False @@ -3964,21 +3936,17 @@ def post_upgrade_cb_check(index, total_checks, cversion, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def eecdh_cipher_check(index, total_checks, cversion, **kwargs): - title = 'EECDH SSL Cipher' +@check_wrapper(check_title="EECDH SSL Cipher") +def eecdh_cipher_check(cversion, **kwargs): result = FAIL_UF - msg = '' headers = ["DN", "Cipher", "State", "Failure Reason"] data = [] recommended_action = "Re-enable EECDH key exchange prior to APIC upgrade." doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#eecdh-ssl-cipher' - print_title(title, index, total_checks) - if cversion.newer_than("4.2(1a)"): commCipher = icurl('class', 'commCipher.json') for cipher in commCipher: @@ -3987,20 +3955,16 @@ def eecdh_cipher_check(index, total_checks, cversion, **kwargs): if not data: result = PASS - - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def vmm_active_uplinks_check(index, total_checks, **kwargs): - title = 'fvUplinkOrderCont with blank active uplinks definition' +@check_wrapper(check_title="fvUplinkOrderCont with blank active uplinks definition") +def vmm_active_uplinks_check(**kwargs): result = PASS - msg = '' headers = ["Tenant", "Application Profile", "Application EPG", "VMM Domain"] data = [] recommended_action = 'Identify Active Uplinks and apply this to the VMM domain association of each EPG' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#vmm-uplink-container-with-empty-actives' - print_title(title, index, total_checks) uplink_api = 'fvUplinkOrderCont.json' uplink_api += '?query-target-filter=eq(fvUplinkOrderCont.active,"")' @@ -4010,10 +3974,7 @@ def vmm_active_uplinks_check(index, total_checks, **kwargs): affected_uplinks = icurl('class', uplink_api) except OldVerClassNotFound: # Pre 4.x did not have this class - msg = 'cversion does not have class fvUplinkOrderCont' - result = NA - print_result(title, result, msg, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=NA, msg="cversion does not have class fvUplinkOrderCont") if affected_uplinks: result = FAIL_O @@ -4021,21 +3982,18 @@ def vmm_active_uplinks_check(index, total_checks, **kwargs): dn = re.search(vmm_epg_regex, uplink['fvUplinkOrderCont']['attributes']['dn']) data.append([dn.group("tenant"), dn.group("ap"), dn.group("epg"), dn.group("dom")]) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def fabric_port_down_check(index, total_checks, **kwargs): - title = 'Fabric Port is Down (F1394 ethpm-if-port-down-fabric)' +@check_wrapper(check_title="Fabric Port is Down (F1394 ethpm-if-port-down-fabric)") +def fabric_port_down_check(**kwargs): result = FAIL_O - msg = '' headers = ["Pod", "Node", "Int", "Reason", "Lifecycle"] unformatted_headers = ['dn', 'Fault Description', 'Lifecycle'] unformatted_data = [] data = [] recommended_action = 'Identify if these ports are needed for redundancy and reason for being down' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#fabric-port-is-down' - print_title(title, index, total_checks) fault_api = 'faultInst.json' fault_api += '?&query-target-filter=and(eq(faultInst.code,"F1394")' @@ -4058,23 +4016,27 @@ def fabric_port_down_check(index, total_checks, **kwargs): if not data and not unformatted_data: result = PASS - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def fabric_dpp_check(index, total_checks, tversion, **kwargs): - title = 'CoS 3 with Dynamic Packet Prioritization' +@check_wrapper(check_title='CoS 3 with Dynamic Packet Prioritization') +def fabric_dpp_check(tversion, **kwargs): result = PASS - msg = '' headers = ["Potential Defect", "Reason"] data = [] recommended_action = 'Change the target version to the fixed version of CSCwf05073' doc_url = 'https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwf05073' - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) lbpol_api = 'lbpPol.json' lbpol_api += '?query-target-filter=eq(lbpPol.pri,"on")' @@ -4088,23 +4050,19 @@ def fabric_dpp_check(index, total_checks, tversion, **kwargs): result = FAIL_O data.append(["CSCwf05073", "Target Version susceptible to Defect"]) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def n9k_c93108tc_fx3p_interface_down_check(index, total_checks, tversion, **kwargs): - title = 'N9K-C93108TC-FX3P/FX3H Interface Down' +@check_wrapper(check_title='N9K-C93108TC-FX3P/FX3H Interface Down') +def n9k_c93108tc_fx3p_interface_down_check(tversion, **kwargs): result = PASS - msg = '' headers = ["Node ID", "Node Name", "Product ID"] data = [] recommended_action = 'Change the target version to the fixed version of CSCwh81430' doc_url = 'https://www.cisco.com/c/en/us/support/docs/field-notices/740/fn74085.html' - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if ( tversion.older_than("5.2(8h)") @@ -4124,19 +4082,16 @@ def n9k_c93108tc_fx3p_interface_down_check(index, total_checks, tversion, **kwar if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def subnet_scope_check(index, total_checks, cversion, **kwargs): - title = 'BD and EPG Subnet Scope Consistency' +@check_wrapper(check_title='BD and EPG Subnet Scope Consistency') +def subnet_scope_check(cversion, **kwargs): result = PASS - msg = '' headers = ["BD DN", "BD Scope", "EPG DN", "EPG Scope"] data = [] recommended_action = 'Configure the same Scope for the identified subnet pairings' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#bd-and-epg-subnet-scope-consistency' - print_title(title, index, total_checks) if cversion.older_than("4.2(6d)") or (cversion.major1 == "5" and cversion.older_than("5.1(1h)")): epg_api = 'fvAEPg.json?' @@ -4144,8 +4099,7 @@ def subnet_scope_check(index, total_checks, cversion, **kwargs): fvAEPg = icurl('class', epg_api) if not fvAEPg: - print_result(title, NA, "0 EPG Subnets found. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return NA + return Result(result=NA, msg="No EPG Subnets found. Skipping.") bd_api = 'fvBD.json' bd_api += '?rsp-subtree=children&rsp-subtree-class=fvSubnet&rsp-subtree-include=required' @@ -4187,23 +4141,19 @@ def subnet_scope_check(index, total_checks, cversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def rtmap_comm_match_defect_check(index, total_checks, tversion, **kwargs): - title = 'Route-map Community Match Defect' +@check_wrapper(check_title='Route-map Community Match Defect') +def rtmap_comm_match_defect_check(tversion, **kwargs): result = PASS - msg = '' headers = ["Route-map DN", "Route-map Match DN", "Failure Reason"] data = [] recommended_action = 'Add a prefix list match to each route-map prior to upgrading.' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#route-map-community-match' - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if (tversion.major1 == "5" and tversion.major2 == "2" and tversion.older_than("5.2(8a)")): rtctrlSubjPs = icurl('class', 'rtctrlSubjP.json?rsp-subtree=full&rsp-subtree-class=rtctrlMatchCommFactor,rtctrlMatchRtDest&rsp-subtree-include=required') @@ -4245,19 +4195,16 @@ def rtmap_comm_match_defect_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def fabricPathEp_target_check(index, total_checks, **kwargs): - title = 'Invalid fabricPathEp Targets' +@check_wrapper(check_title='Invalid fabricPathEp Targets') +def fabricPathEp_target_check(**kwargs): result = PASS - msg = '' headers = ["Invalid DN", "Reason"] data = [] recommended_action = 'Contact TAC for cleanup procedure' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#invalid-fex-fabricpathep-dn-references' - print_title(title, index, total_checks) fabricPathEp_regex = r"topology/pod-\d+/(?:\w+)?paths-\d+(?:-\d+)?(?:/ext(?:\w+)?paths-(?P<fexA>\d+)(?:-(?P<fexB>\d+))?)?/pathep-\[(?P<path>.+)\]" eth_regex = r'eth(?P<first>\d+)/(?P<second>\d+)(?:/(?P<third>\d+))?' @@ -4319,23 +4266,19 @@ def fabricPathEp_target_check(index, total_checks, **kwargs): if data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def lldp_custom_int_description_defect_check(index, total_checks, tversion, **kwargs): - title = 'LLDP Custom Interface Description Defect' +@check_wrapper(check_title='LLDP Custom Interface Description Defect') +def lldp_custom_int_description_defect_check(tversion, **kwargs): result = PASS - msg = '' headers = ["Potential Defect"] data = [] recommended_action = 'Target version is not recommended; Custom interface descriptions and lazy VMM domain attachments found.' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#lldp-custom-interface-description' - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if tversion.major1 == '6' and tversion.older_than('6.0(3a)'): custom_int_count = icurl('class', 'infraPortBlk.json?query-target-filter=ne(infraPortBlk.descr,"")&rsp-subtree-include=count')[0]['moCount']['attributes']['count'] @@ -4345,27 +4288,22 @@ def lldp_custom_int_description_defect_check(index, total_checks, tversion, **kw result = FAIL_O data.append(['CSCwf00416']) - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def unsupported_fec_configuration_ex_check(index, total_checks, sw_cversion, tversion, **kwargs): - title = 'Unsupported FEC Configuration For N9K-C93180YC-EX' +@check_wrapper(check_title='Unsupported FEC Configuration For N9K-C93180YC-EX') +def unsupported_fec_configuration_ex_check(sw_cversion, tversion, **kwargs): result = PASS - msg = '' headers = ["Pod ID", "Node ID", "Switch Model", "Interface", "FEC Mode"] data = [] recommended_action = 'Nexus C93180YC-EX switches do not support IEEE-RS-FEC or CONS16-RS-FEC mode. Misconfigured ports will be hardware disabled upon upgrade. Remove unsupported FEC configuration prior to upgrade.' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#unsupported-fec-configuration-for-n9k-c93180yc-ex' - print_title(title, index, total_checks) if not sw_cversion: - print_result(title, MANUAL, "Current switch version not found. Check switch health.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg="Current switch version not found. Check switch health.") if not tversion: - print_result(title, MANUAL, "Target switch version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if sw_cversion.older_than('5.0(1a)') and tversion.newer_than("5.0(1a)"): api = 'topSystem.json' @@ -4391,25 +4329,21 @@ def unsupported_fec_configuration_ex_check(index, total_checks, sw_cversion, tve if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def static_route_overlap_check(index, total_checks, cversion, tversion, **kwargs): - title = 'L3out /32 Static Route and BD Subnet Overlap' +@check_wrapper(check_title='L3out /32 Static Route and BD Subnet Overlap') +def static_route_overlap_check(cversion, tversion, **kwargs): result = PASS - msg = '' headers = ['L3out', '/32 Static Route', 'BD', 'BD Subnet'] data = [] recommended_action = 'Change /32 static route design or target a fixed version' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#l3out-32-overlap-with-bd-subnet' - print_title(title, index, total_checks) iproute_regex = r'uni/tn-(?P<tenant>[^/]+)/out-(?P<l3out>[^/]+)/lnodep-(?P<nodeprofile>[^/]+)/rsnodeL3OutAtt-\[topology/pod-(?P<pod>[^/]+)/node-(?P<node>\d{3,4})\]/rt-\[(?P<addr>[^/]+)/(?P<netmask>\d{1,2})\]' bd_subnet_regex = r'uni/tn-(?P<tenant>[^/]+)/BD-(?P<bd>[^/]+)/subnet-\[(?P<subnet>[^/]+/\d{2})\]' if not tversion: - print_result(title, MANUAL, 'Target version not supplied. Skipping.', func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if (cversion.older_than("5.2(6e)") and tversion.newer_than("5.0(1a)") and tversion.older_than("5.2(6e)")): slash32filter = 'ipRouteP.json?query-target-filter=and(wcard(ipRouteP.dn,"/32"))' @@ -4456,27 +4390,22 @@ def static_route_overlap_check(index, total_checks, cversion, tversion, **kwargs if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def vzany_vzany_service_epg_check(index, total_checks, cversion, tversion, **kwargs): - title = "vzAny-to-vzAny Service Graph when crossing 5.0 release" +@check_wrapper(check_title='vzAny-to-vzAny Service Graph when crossing 5.0 release') +def vzany_vzany_service_epg_check(cversion, tversion, **kwargs): result = PASS - msg = "" headers = ["VRF (Tn:VRF)", "Contract (Tn:Contract)", "Service Graph (Tn:SG)"] data = [] recommended_action = "Be aware of transient traffic disruption for vzAny-to-vzAny Service Graph during APIC upgrade." doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#vzany-to-vzany-service-graph-when-crossing-50-release" - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if not (cversion.older_than("5.0(1a)") and tversion.newer_than("5.0(1a)")): - print_result(title, NA, func_name=inspect.currentframe().f_code.co_name) - return NA + return Result(result=NA, msg=VER_NOT_AFFECTED) tn_regex = r"uni/tn-(?P<tn>[^/]+)" vrf_regex = tn_regex + r"/ctx-(?P<vrf>[^/]+)" @@ -4488,8 +4417,7 @@ def vzany_vzany_service_epg_check(index, total_checks, cversion, tversion, **kwa for vzRsSubjGraphAtt in vzRsSubjGraphAtts: graphAtt_rns = vzRsSubjGraphAtt["vzRsSubjGraphAtt"]["attributes"]["dn"].split("/") if len(graphAtt_rns) < 3: - print_result(title, ERROR, "Failed to get contract DN from vzRsSubjGraphAtt DN.", func_name=inspect.currentframe().f_code.co_name) - return ERROR + return Result(result=ERROR, msg="Failed to get contract DN from vzRsSubjGraphAtt DN") # Get vzAny(VRF) relations of the contract. There can be multiple VRFs per contract. vrfs = defaultdict(set) # key: VRF, value: vzRtAnyToCons, vzRtAnyToProv @@ -4528,27 +4456,22 @@ def vzany_vzany_service_epg_check(index, total_checks, cversion, tversion, **kwa data.append([vrf, contract, sg]) if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def validate_32_64_bit_image_check(index, total_checks, cversion, tversion, **kwargs): - title = '32 and 64-Bit Firmware Image for Switches' +@check_wrapper(check_title='32 and 64-Bit Firmware Image for Switches') +def validate_32_64_bit_image_check(cversion, tversion, **kwargs): result = PASS - msg = '' headers = ["Target Switch Version", "32-Bit Image Result", "64-Bit Image Result"] data = [] recommended_action = 'Upload the missing 32 or 64 bit Switch Image to the Firmware repository' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#602-requires-32-and-64-bit-switch-images' - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if cversion.older_than("6.0(2a)") and tversion.newer_than("6.0(2a)"): - print_result(title, POST, 'Re-run after APICs are upgraded to 6.0(2) or later', func_name=inspect.currentframe().f_code.co_name) - return POST + return Result(result=POST, msg="Re-run after APICs are upgraded to 6.0(2) or later") if cversion.newer_than("6.0(2a)") and tversion.newer_than("6.0(2a)"): result_32 = result_64 = "Not Found" @@ -4574,26 +4497,21 @@ def validate_32_64_bit_image_check(index, total_checks, cversion, tversion, **kw if result_32 in ["Not Found", "INVALID"] or result_64 in ["Not Found", "INVALID"]: result = FAIL_UF data.append([target_sw_ver, result_32, result_64]) - else: - result = NA - msg = 'Target version below 6.0(2)' + return Result(result=NA, msg="Target version below 6.0(2)") - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def fabric_link_redundancy_check(index, total_checks, **kwargs): - title = 'Fabric Link Redundancy' +@check_wrapper(check_title='Fabric Link Redundancy') +def fabric_link_redundancy_check(**kwargs): result = PASS - msg = '' headers = ["Leaf Name", "Fabric Link Adjacencies", "Problem"] data = [] recommended_action = "" sp_recommended_action = "Connect the leaf switch(es) to multiple spine switches for redundancy" t1_recommended_action = "Connect the tier 2 leaf switch(es) to multiple tier1 leaf switches for redundancy" doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#fabric-link-redundancy" - print_title(title, index, total_checks) fabric_nodes_api = 'fabricNode.json' fabric_nodes_api += '?query-target-filter=and(or(eq(fabricNode.role,"leaf"),eq(fabricNode.role,"spine")),eq(fabricNode.fabricSt,"active"))' @@ -4658,33 +4576,26 @@ def fabric_link_redundancy_check(index, total_checks, **kwargs): elif not sp_missing and t1_missing: recommended_action = t1_recommended_action - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def cloudsec_encryption_depr_check(index, total_checks, tversion, **kwargs): - title = 'CloudSec Encrpytion Deprecated' +@check_wrapper(check_title='CloudSec Encryption Deprecated') +def cloudsec_encryption_depr_check(tversion, **kwargs): result = NA - msg = '' headers = ["Findings"] data = [] recommended_action = 'Validate if CloudSec Encryption is enabled within Nexus Dashboard Orchestrator' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#cloudsec-encryption-deprecated' - print_title(title, index, total_checks) cloudsec_api = 'cloudsecPreSharedKey.json' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) try: cloudsecPreSharedKey = icurl('class', cloudsec_api) except OldVerClassNotFound: - msg = 'cversion does not have class cloudsecPreSharedKey' - result = NA - print_result(title, result, msg, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=NA, msg="cversion does not have class cloudsecPreSharedKey") if tversion.newer_than("6.0(6a)"): if len(cloudsecPreSharedKey) > 1: @@ -4695,19 +4606,16 @@ def cloudsec_encryption_depr_check(index, total_checks, tversion, **kwargs): result = MANUAL else: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def out_of_service_ports_check(index, total_checks, **kwargs): - title = 'Out-of-Service Ports' +@check_wrapper(check_title='Out-of-Service Ports') +def out_of_service_ports_check(**kwargs): result = PASS - msg = '' headers = ["Pod ID", "Node ID", "Port ID", "Operational State", "Usage"] data = [] recommended_action = 'Remove Out-of-service Policy on identified "up" ports or they will remain "down" after switch Upgrade' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#out-of-service-ports' - print_title(title, index, total_checks) ethpmPhysIf_api = 'ethpmPhysIf.json' ethpmPhysIf_api += '?query-target-filter=and(eq(ethpmPhysIf.operSt,"2"),bw(ethpmPhysIf.usage,"32","34"))' @@ -4725,27 +4633,23 @@ def out_of_service_ports_check(index, total_checks, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def fc_ex_model_check(index, total_checks, tversion, **kwargs): - title = 'FC/FCOE support removed for -EX platforms' +@check_wrapper(check_title='FC/FCOE support removed for -EX platforms') +def fc_ex_model_check(tversion, **kwargs): result = PASS - msg = '' headers = ["FC/FCOE Node ID", "Model"] data = [] recommended_action = 'Select a different target version. Refer to the doc for additional details.' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#fcfcoe-support-for-ex-switches' - print_title(title, index, total_checks) fcEntity_api = "fcEntity.json" fabricNode_api = 'fabricNode.json' fabricNode_api += '?query-target-filter=wcard(fabricNode.model,".*EX")' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if (tversion.newer_than("6.0(7a)") and tversion.older_than("6.0(9c)")) or tversion.same_as("6.1(1f)"): fcEntitys = icurl('class', fcEntity_api) @@ -4764,19 +4668,16 @@ def fc_ex_model_check(index, total_checks, tversion, **kwargs): data.append([node_dn, model]) if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def tep_to_tep_ac_counter_check(index, total_checks, **kwargs): - title = 'TEP-to-TEP Atomic Counter scalability' +@check_wrapper(check_title='TEP-to-TEP Atomic Counter scalability') +def tep_to_tep_ac_counter_check(**kwargs): result = NA - msg = '' headers = ["dbgAcPath Count", "Supported Maximum"] data = [] recommended_action = 'Assess and cleanup dbgAcPath policies to drop below the supported maximum' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#tep-to-tep-atomic-counters-scalability' - print_title(title, index, total_checks) ac_limit = 1600 atomic_counter_api = 'dbgAcPath.json' @@ -4793,19 +4694,16 @@ def tep_to_tep_ac_counter_check(index, total_checks, **kwargs): if data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def clock_signal_component_failure_check(index, total_checks, **kwargs): - title = 'Nexus 950X FM or LC Might Fail to boot after reload' +@check_wrapper(check_title='Nexus 950X FM or LC Might Fail to boot after reload') +def clock_signal_component_failure_check(**kwargs): result = PASS - msg = '' headers = ['Pod', "Node", "Slot", "Model", "Serial Number"] data = [] recommended_action = 'Run the SN string through the Serial Number Validation tool (linked within doc url) to check for FN64251.\n\tSN String:\n\t' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#nexus-950x-fm-or-lc-might-fail-to-boot-after-reload' - print_title(title, index, total_checks) eqptFC_api = 'eqptFC.json' eqptFC_api += '?query-target-filter=or(eq(eqptFC.model,"N9K-C9504-FM-E"),eq(eqptFC.model,"N9K-C9508-FM-E"))' @@ -4837,29 +4735,23 @@ def clock_signal_component_failure_check(index, total_checks, **kwargs): result = MANUAL recommended_action += sn_string[:-1] - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def stale_decomissioned_spine_check(index, total_checks, tversion, **kwargs): - title = 'Stale decomissioned Spine' +@check_wrapper(check_title='Stale Decomissioned Spine') +def stale_decomissioned_spine_check(tversion, **kwargs): result = PASS - msg = '' headers = ["Susceptible Spine Node Id", "Spine Name", "Current Node State"] data = [] recommended_action = 'Remove fabricRsDecommissionNode objects pointing to above Spine Nodes before APIC upgrade' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#stale-decommissioned-spine' - print_title(title, index, total_checks) decomissioned_api = 'fabricRsDecommissionNode.json' - active_spine_api = 'topSystem.json' active_spine_api += '?query-target-filter=eq(topSystem.role,"spine")' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if tversion.newer_than("5.2(3d)") and tversion.older_than("6.0(3d)"): decomissioned_switches = icurl('class', decomissioned_api) @@ -4875,26 +4767,22 @@ def stale_decomissioned_spine_check(index, total_checks, tversion, **kwargs): data.append([node_id, name, state]) if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def n9408_model_check(index, total_checks, tversion, **kwargs): - title = 'N9K-C9408 Platform Model' +@check_wrapper(check_title='N9K-C9408 Platform Model') +def n9408_model_check(tversion, **kwargs): result = PASS - msg = '' headers = ["Node ID", "Model"] data = [] recommended_action = 'Identified N9K-C9408 must be decommissioned then recomissioned after upgrade to 6.1(3)' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#n9k-c9408-platform-model' - print_title(title, index, total_checks) eqptCh_api = 'eqptCh.json' eqptCh_api += '?query-target-filter=eq(eqptCh.model,"N9K-C9400-SW-GX2A")' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if tversion.newer_than("6.1(3a)"): eqptCh = icurl('class', eqptCh_api) @@ -4905,19 +4793,16 @@ def n9408_model_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def pbr_high_scale_check(index, total_checks, tversion, **kwargs): - title = 'PBR High Scale' +@check_wrapper(check_title='PBR High Scale') +def pbr_high_scale_check(tversion, **kwargs): result = PASS - msg = '' headers = ["Fabric-Wide PBR Object Count"] data = [] recommended_action = 'High PBR scale detected, target a fixed version for CSCwi66348' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#pbr-high-scale' - print_title(title, index, total_checks) # Not querying fvAdjDefCons as it fails from APIC vnsAdjacencyDefCont_api = 'vnsAdjacencyDefCont.json' @@ -4925,8 +4810,7 @@ def pbr_high_scale_check(index, total_checks, tversion, **kwargs): count_filter = '?rsp-subtree-include=count' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if tversion.older_than("5.3(2c)"): vnsAdj = icurl('class', vnsAdjacencyDefCont_api+count_filter) @@ -4941,28 +4825,22 @@ def pbr_high_scale_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def https_throttle_rate_check(index, total_checks, cversion, tversion, **kwargs): - title = "HTTPS Request Throttle Rate" +@check_wrapper(check_title='HTTPS Request Throttle Rate') +def https_throttle_rate_check(cversion, tversion, **kwargs): result = PASS - msg = "" headers = ["Mgmt Access Policy", "HTTPS Throttle Rate"] data = [] recommended_action = "Reduce the throttle rate to 40 (req/sec), 2400 (req/min) or lower." doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#https-request-throttle-rate" - print_title(title, index, total_checks) - # Applicable only when crossing 6.1(2) as upgrade instead of downgrade. if cversion.newer_than("6.1(2a)"): - print_result(title, NA, func_name=inspect.currentframe().f_code.co_name) - return NA + return Result(result=NA, msg=VER_NOT_AFFECTED) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) commHttpses = icurl("class", "commHttps.json") for commHttps in commHttpses: @@ -4990,27 +4868,23 @@ def https_throttle_rate_check(index, total_checks, cversion, tversion, **kwargs) recommended_action = "6.1(2)+ will reject this config. " + recommended_action else: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def standby_sup_sync_check(index, total_checks, cversion, tversion, **kwargs): - title = 'Standby Sup Image Sync' +@check_wrapper(check_title='Standby SUP Image Sync') +def standby_sup_sync_check(cversion, tversion, **kwargs): result = PASS - msg = '' headers = ["Pod ID", "Node ID", "Standby SUP Slot"] data = [] recommended_action = 'Target an interim image with fix for CSCwa44220 that is smaller than 2Gigs, such as 5.2(8i)' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#standby-sup-image-sync' - print_title(title, index, total_checks) sup_regex = node_regex + r'/sys/ch/supslot-(?P<slot>\d)' eqptSupC_api = 'eqptSupC.json' eqptSupC_api += '?query-target-filter=eq(eqptSupC.rdSt,"standby")' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if ( (cversion.older_than("4.2(7t)") or (cversion.major_version == "5.2" and cversion.older_than("5.2(5d)"))) @@ -5029,14 +4903,12 @@ def standby_sup_sync_check(index, total_checks, cversion, tversion, **kwargs): if data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def equipment_disk_limits_exceeded(index, total_checks, **kwargs): - title = 'Equipment Disk Limits Exceeded' +@check_wrapper(check_title='Equipment Disk Limits Exceeded') +def equipment_disk_limits_exceeded(**kwargs): result = PASS - msg = '' headers = ['Pod', 'Node', 'Code', '%', 'Description',] data = [] unformatted_headers = ['Fault DN', '%', 'Recommended Action'] @@ -5044,8 +4916,6 @@ def equipment_disk_limits_exceeded(index, total_checks, **kwargs): recommended_action = 'Review the reference document for commands to validate disk usage' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/##equipment-disk-limits-exceeded' - print_title(title, index, total_checks) - usage_regex = r"avail \(New: (?P<avail>\d+)\).+used \(New: (?P<used>\d+)\)" f182x_api = 'faultInst.json' f182x_api += '?query-target-filter=or(eq(faultInst.code,"F1820"),eq(faultInst.code,"F1821"),eq(faultInst.code,"F1822"))' @@ -5070,14 +4940,20 @@ def equipment_disk_limits_exceeded(index, total_checks, **kwargs): if data or unformatted_data: result = FAIL_UF - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action, doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) -def aes_encryption_check(index, total_checks, tversion, **kwargs): - title = "Global AES Encryption" +@check_wrapper(check_title='Global AES Encryption') +def aes_encryption_check(tversion, **kwargs): result = FAIL_UF - msg = "" headers = ["Target Version", "Global AES Encryption", "Impact"] data = [] recommended_action = ( @@ -5086,11 +4962,8 @@ def aes_encryption_check(index, total_checks, tversion, **kwargs): ) doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#global-aes-encryption" - print_title(title, index, total_checks) - if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if tversion.newer_than("6.1(2a)"): impact = "Upgrade Failure" @@ -5108,14 +4981,12 @@ def aes_encryption_check(index, total_checks, tversion, **kwargs): else: result = PASS - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def service_bd_forceful_routing_check(index, total_checks, cversion, tversion, **kwargs): - title = "Service Graph BD Forceful Routing" +@check_wrapper(check_title='Service Graph BD Forceful Routing') +def service_bd_forceful_routing_check(cversion, tversion, **kwargs): result = PASS - msg = "" headers = ["Bridge Domain (Tenant:BD)", "Service Graph Device (Tenant:Device)"] data = [] unformatted_headers = ["DN of fvRtEPpInfoToBD"] @@ -5126,15 +4997,11 @@ def service_bd_forceful_routing_check(index, total_checks, cversion, tversion, * ) doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#service-graph-bd-forceful-routing" - print_title(title, index, total_checks) - if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if not (cversion.older_than("6.0(2a)") and tversion.newer_than("6.0(2a)")): - print_result(title, NA, func_name=inspect.currentframe().f_code.co_name) - return NA + return Result(result=NA, msg=VER_NOT_AFFECTED) dn_regex = r"uni/tn-(?P<bd_tn>[^/]+)/BD-(?P<bd>[^/]+)/" dn_regex += r"rtvnsEPpInfoToBD-\[uni/tn-(?P<sg_tn>[^/])+/LDevInst-\[uni/tn-(?P<ldev_tn>[^/]+)/lDevVip-(?P<ldev>[^\]]+)\].*\]" @@ -5153,28 +5020,33 @@ def service_bd_forceful_routing_check(index, total_checks, cversion, tversion, * if data or unformatted_data: result = MANUAL - print_result(title, result, msg, headers, data, unformatted_headers, unformatted_data, recommended_action, doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result( + result=result, + headers=headers, + data=data, + unformatted_headers=unformatted_headers, + unformatted_data=unformatted_data, + recommended_action=recommended_action, + doc_url=doc_url, + ) # Connection Base Check -def observer_db_size_check(index, total_checks, username, password, **kwargs): - title = 'Observer Database Size' +@check_wrapper(check_title='Observer Database Size') +def observer_db_size_check(username, password, **kwargs): result = PASS - msg = '' headers = ["Node", "File Location", "Size (GB)"] data = [] recommended_action = 'Contact TAC to analyze and truncate large DB files' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations#observer-database-size' - print_title(title, index, total_checks) topSystem_api = 'topSystem.json' topSystem_api += '?query-target-filter=eq(topSystem.role,"controller")' controllers = icurl('class', topSystem_api) if not controllers: - print_result(title, ERROR, 'topSystem response empty. Is the cluster healthy?', func_name=inspect.currentframe().f_code.co_name) - return ERROR + return Result(result=ERROR, msg='topSystem response empty. Is the cluster healthy?') + has_error = False prints('') for apic in controllers: @@ -5189,7 +5061,7 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): c.connect() except Exception as e: data.append([attr['id'], attr['name'], str(e)]) - print_result(node_title, ERROR, json_output=False) + print_result(node_title, ERROR) has_error = True continue try: @@ -5197,7 +5069,7 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): c.cmd(cmd) if "No such file or directory" in c.output: data.append([attr['id'], '/data2/dbstats/ not found', "Check user permissions or retry as 'apic#fallback\\\\admin'"]) - print_result(node_title, ERROR, json_output=False) + print_result(node_title, ERROR) has_error = True continue dbstats = c.output.split("\n") @@ -5208,36 +5080,32 @@ def observer_db_size_check(index, total_checks, username, password, **kwargs): file_size = size_match.group("size") file_name = "/data2/dbstats/" + size_match.group("file") data.append([attr['id'], file_name, file_size]) - print_result(node_title, DONE, json_output=False) + print_result(node_title, DONE) except Exception as e: data.append([attr['id'], attr['name'], str(e)]) - print_result(node_title, ERROR, json_output=False) + print_result(node_title, ERROR) has_error = True continue if has_error: result = ERROR elif data: result = FAIL_UF - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, adjust_title=True, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url, adjust_title=True) -def ave_eol_check(index, total_checks, tversion, **kwargs): - title = 'AVE End-of-Life' +@check_wrapper(check_title='AVE End-of-Life') +def ave_eol_check(tversion, **kwargs): result = NA - msg = '' headers = ["AVE Domain Name"] data = [] recommended_action = 'AVE domain(s) must be migrated to supported domain types prior to 6.0+ upgrade' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#ave-end-of-life' - print_title(title, index, total_checks) ave_api = 'vmmDomP.json' ave_api += '?query-target-filter=eq(vmmDomP.enableAVE,"true")' if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if tversion.newer_than("6.0(1a)"): ave = icurl('class', ave_api) @@ -5247,24 +5115,19 @@ def ave_eol_check(index, total_checks, tversion, **kwargs): if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def stale_pcons_ra_mo_check(index, total_checks, cversion, tversion, **kwargs): - title = 'Stale pconsRA Objects' +@check_wrapper(check_title='Stale pconsRA Objects') +def stale_pcons_ra_mo_check(cversion, tversion, **kwargs): result = PASS - msg = '' headers = ["Stale pconsRA DN", "Non-Existing DN"] - data = [] recommended_action = 'Contact Cisco TAC to delete stale pconsRA before upgrading' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#stale-pconsra-object' - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if cversion.older_than("6.0(3d)") and tversion.newer_than("6.0(3c)") and tversion.older_than("6.1(4a)"): pcons_rssubtreedep_api = 'pconsRsSubtreeDep.json?query-target-filter=wcard(pconsRsSubtreeDep.tDn,"/instdn-")' @@ -5298,28 +5161,23 @@ def stale_pcons_ra_mo_check(index, total_checks, cversion, tversion, **kwargs): if pcons_ra_dn_mo: data.append([pcons_ra_dn, policy_dn]) else: - print_result(title, NA, "Target version not supplied or not applicable. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return NA + return Result(result=NA, msg=VER_NOT_AFFECTED) if data: result = FAIL_O - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) -def isis_database_byte_check(index, total_checks, tversion, **kwargs): - title = 'ISIS DTEPs Byte Size' +@check_wrapper(check_title='ISIS DTEPs Byte Size') +def isis_database_byte_check(tversion, **kwargs): result = PASS - msg = '' headers = ["ISIS DTEPs Byte Size", "ISIS DTEPs"] data = [] recommended_action = 'Upgrade to a version with the fix for CSCwp15375. Current target version is affected.' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#isis-dteps-byte-size' - print_title(title, index, total_checks) if not tversion: - print_result(title, MANUAL, "Target version not supplied. Skipping.", func_name=inspect.currentframe().f_code.co_name) - return MANUAL + return Result(result=MANUAL, msg=TVER_MISSING) if tversion.newer_than("6.1(1a)") and tversion.older_than("6.1(3g)"): isisDTEp_api = 'isisDTEp.json' @@ -5347,13 +5205,9 @@ def isis_database_byte_check(index, total_checks, tversion, **kwargs): result = FAIL_O data.append([total_bytes, combined_dteps]) break - else: - print_result(title, NA, "Target version not affected", func_name=inspect.currentframe().f_code.co_name) - return NA - - print_result(title, result, msg, headers, data, recommended_action=recommended_action, doc_url=doc_url, func_name=inspect.currentframe().f_code.co_name) - return result + return Result(result=NA, msg=VER_NOT_AFFECTED) + return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) # ---- Script Execution ---- @@ -5529,14 +5383,6 @@ def run_checks(checks, inputs): except KeyboardInterrupt: prints('\n\n!!! KeyboardInterrupt !!!\n') break - except Exception as e: - func_name = check.__name__ - prints('') - print_title(" " * len(func_name)) # not showing the func name in the stdout - msg = 'Unexpected Error: %s' % e - print_result(func_name, ERROR, msg, func_name=func_name) - summary[ERROR] += 1 - logging.exception(e) prints('\n=== Summary Result ===\n') res = max(summary_headers, key=len) diff --git a/tests/test_run_checks.py b/tests/test_run_checks.py index 1483c6f0..753d322e 100644 --- a/tests/test_run_checks.py +++ b/tests/test_run_checks.py @@ -1,37 +1,74 @@ import importlib import logging +import json +import os script = importlib.import_module("aci-preupgrade-validation-script") AciVersion = script.AciVersion +JSON_DIR = script.JSON_DIR +ApicResult = script.syntheticMaintPValidate +Result = script.Result +check_wrapper = script.check_wrapper -def check_builder(func_name, title, result): - def _check(index, total_checks, **kwargs): - _check.__name__ = func_name - script.print_title(title, index, total_checks) +ERROR_REASON = "This is a test exception to result in `script.ERROR`." + + +def check_builder(func_name, title, result, others): + @check_wrapper(check_title=title) + def _check(**kwargs): + _check.__name__ = func_name # Set the function name for the check if result == script.ERROR: - raise Exception("This is a test exception to result in `script.ERROR`.") + raise Exception(ERROR_REASON) else: - script.print_result(title, result, func_name=func_name) - return result - + return Result(result=result, **others) return _check +fake_data_full = { + "msg": "test msg", + "headers": ["H1", "H2", "H3"], + "data": [["Data1", "Data2", "Data3"], ["Data4", "Data5", "Data6"], ["Loooooong Data7", "Data8", "Data9"]], + "unformatted_headers": ["Unformatted_H1"], + "unformatted_data": [["Data1"], ["Data2"]], + "recommended_action": "This is your recommendation to remediate the issue", + "doc_url": "https://fake_doc_url.local/path1/#section1", +} + +fake_data_no_msg_no_unform = { + "headers": ["H1", "H2", "H3"], + "data": [["Data1", "Data2", "Data3"], ["Data4", "Data5", "Data6"], ["Loooooong Data7", "Data8", "Data9"]], + "recommended_action": "This is your recommendation to remediate the issue", + "doc_url": "https://fake_doc_url.local/path1/#section1", +} + +fake_data_error = { + "msg": "Error msg. This should not be printed", +} + +fake_data_only_msg = { + "msg": "test msg", +} + +fake_checks_meta = [ + ("fake_check1", "Test Check 1", script.PASS, {}), + ("fake_check2", "Test Check 2", script.FAIL_O, fake_data_full), + ("fake_check3", "Test Check 3", script.FAIL_UF, fake_data_no_msg_no_unform), + ("fake_check4", "Test Check 4", script.MANUAL, fake_data_only_msg), + ("fake_check5", "Test Check 5", script.POST, fake_data_only_msg), + ("fake_check6", "Test Check 6", script.NA, fake_data_only_msg), + ("fake_check7", "Test Check 7", script.ERROR, fake_data_error), + ("fake_check8", "Test Check 8", script.PASS, fake_data_only_msg), +] + fake_checks = [ - check_builder(func_name, title, result) - for func_name, title, result in [ - ("check1", "Test Check 1", script.PASS), - ("check2", "Test Check 2", script.FAIL_O), - ("check3", "Test Check 3", script.FAIL_UF), - ("check4", "Test Check 4", script.MANUAL), - ("check5", "Test Check 5", script.POST), - ("check6", "Test Check 6", script.NA), - ("check7", "Test Check 7", script.ERROR), - ("check8", "Test Check 8", script.PASS), - ] + check_builder(func_name, title, result, others) + for func_name, title, result, others in fake_checks_meta ] +fake_result_filenames = [ + "{}.json".format(func_name) for func_name, _, _, _ in fake_checks_meta +] fake_inputs = { "username": "admin", @@ -52,14 +89,38 @@ def test_run_checks(capsys, caplog): captured.out == """\ [Check 1/8] Test Check 1... PASS -[Check 2/8] Test Check 2... FAIL - OUTAGE WARNING!! +[Check 2/8] Test Check 2... test msg FAIL - OUTAGE WARNING!! + H1 H2 H3 + -- -- -- + Data1 Data2 Data3 + Data4 Data5 Data6 + Loooooong Data7 Data8 Data9 + + Unformatted_H1 + -------------- + Data1 + Data2 + + Recommended Action: This is your recommendation to remediate the issue + Reference Document: https://fake_doc_url.local/path1/#section1 + + [Check 3/8] Test Check 3... FAIL - UPGRADE FAILURE!! -[Check 4/8] Test Check 4... MANUAL CHECK REQUIRED -[Check 5/8] Test Check 5... POST UPGRADE CHECK REQUIRED -[Check 6/8] Test Check 6... N/A -[Check 7/8] Test Check 7... - ... Unexpected Error: This is a test exception to result in `script.ERROR`. ERROR !! -[Check 8/8] Test Check 8... PASS + H1 H2 H3 + -- -- -- + Data1 Data2 Data3 + Data4 Data5 Data6 + Loooooong Data7 Data8 Data9 + + Recommended Action: This is your recommendation to remediate the issue + Reference Document: https://fake_doc_url.local/path1/#section1 + + +[Check 4/8] Test Check 4... test msg MANUAL CHECK REQUIRED +[Check 5/8] Test Check 5... test msg POST UPGRADE CHECK REQUIRED +[Check 6/8] Test Check 6... test msg N/A +[Check 7/8] Test Check 7... Unexpected Error: This is a test exception to result in `script.ERROR`. ERROR !! +[Check 8/8] Test Check 8... test msg PASS === Summary Result === @@ -73,3 +134,42 @@ def test_run_checks(capsys, caplog): TOTAL : 8 """ # noqa: W291 ) + + json_files = [f for f in os.listdir(JSON_DIR) if f in fake_result_filenames] + assert json_files, "Result JSON file not created" + + for json_file in json_files: + with open(os.path.join(JSON_DIR, json_file)) as f: + data = json.load(f) + + for func_name, title, result, others, in fake_checks_meta: + if data["ruleId"] == func_name: + assert data["name"] == title + # reason + if result == script.ERROR: + assert data["reason"].endswith(ERROR_REASON) + elif others.get("unformatted_data"): + assert data["reason"] == others.get("msg", "") + ( + "\n" + "Parse failure occurred, the provided data may not be complete. " + "Please contact Cisco TAC to identify the missing data." + ) + else: + assert data["reason"] == others.get("msg", "") + # failureDetails.failType + if result not in [script.PASS, script.NA]: + assert data["failureDetails"]["failType"] == result + else: + assert data["failureDetails"]["failType"] == "" + # failureDetails.data + assert data["failureDetails"]["data"] == ApicResult.craftData( + others.get("headers", []), others.get("data", []) + ) + assert data["failureDetails"]["unformatted_data"] == ApicResult.craftData( + others.get("unformatted_headers", []), others.get("unformatted_data", []) + ) + # other fields + assert data["recommended_action"] == others.get("recommended_action", "") + assert data["docUrl"] == others.get("doc_url", "") + assert data["description"] == "" + assert data["sub_reason"] == "" diff --git a/tests/test_synthenticMaintPValidate.py b/tests/test_synthenticMaintPValidate.py index 221415a8..329f56aa 100644 --- a/tests/test_synthenticMaintPValidate.py +++ b/tests/test_synthenticMaintPValidate.py @@ -188,3 +188,20 @@ def test_syntheticMaintPValidate( assert data["showValidation"] == expected_show assert data["severity"] == expected_criticality assert data["ruleStatus"] == expected_passed + + +@pytest.mark.parametrize( + "headers, data", + [ + ("", []), # invalid headers (columns) + ([], {}), # invalid data (rows) + ("", {}), # invalid headers and data + ] +) +def test_invalid_headers_or_data(headers, data): + with pytest.raises(TypeError): + synth = script.syntheticMaintPValidate("func_name", "Check Title", "A Description") + synth.craftData( + column=headers, + rows=data, + ) \ No newline at end of file From df586d986edbf7a276fec8ba80f3ff9688681649 Mon Sep 17 00:00:00 2001 From: Gabriel <gmonroy@cisco.com> Date: Mon, 14 Jul 2025 15:39:21 -0400 Subject: [PATCH 24/32] rename synth class to AciResult + breakdown --puv into --api-only and --no-cleanup --- tests/test_AciResult.py | 207 +++++++++++++++++++++++++++++++++++++++ tests/test_parse_args.py | 19 ++-- tests/test_prepare.py | 20 ++-- tests/test_run_checks.py | 2 +- 4 files changed, 228 insertions(+), 20 deletions(-) create mode 100644 tests/test_AciResult.py diff --git a/tests/test_AciResult.py b/tests/test_AciResult.py new file mode 100644 index 00000000..22651112 --- /dev/null +++ b/tests/test_AciResult.py @@ -0,0 +1,207 @@ +import pytest +import importlib +import json + +script = importlib.import_module("aci-preupgrade-validation-script") + + +@pytest.mark.parametrize( + "func_name, name, description, result, recommended_action, reason, doc_url, column, row, unformatted_column, unformatted_rows, expected_show, expected_criticality, expected_passed", + [ + # Check 1: NA + ( + "fake_func_name_NA_test", + "NA", + "", + script.NA, + "", + "", + "", + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + False, + "informational", + "passed" + ), + # Check 2: PASS + ( + "fake_func_name_PASS_test", + "PASS", + "", + script.PASS, + "", + "", + "", + [], + [], + [], + [], + True, + "informational", + "passed" + ), + # Check 3: POST + ( + "fake_func_name_POST_test", + "POST", + "", + script.POST, + "reboot", + "test reason", + "https://test_doc_url.html", + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + False, + "informational", + "failed" + ), + # Check 4: MANUAL + ( + "fake_func_name_MANUAL_test", + "MANUAL", + "", + script.MANUAL, + "reboot", + "test reason", + "https://test_doc_url.html", + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + True, + "warning", + "failed" + ), + # Check 5: ERROR + ( + "fake_func_name_ERROR_test", + "ERROR", + "", + script.ERROR, + "reboot", + "test reason", + "https://test_doc_url.html", + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + True, + "major", + "failed" + ), + # Check 6: FAIL_UF + ( + "fake_func_name_FAIL_UF_test", + "FAIL_UF", + "", + script.FAIL_UF, + "reboot", + "test reason", + "https://test_doc_url.html", + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + ["col1", "col2"], + [["row1", "row2"], ["row3", "row4"]], + True, + "critical", + "failed" + ), + # Check 7: FAIL_O + ( + "fake_func_name_FAIL_O_test", + "FAIL_O", + "", + script.FAIL_O, + "reboot", + "test reason", + "https://test_doc_url.html", + ["col1", "col2", "col3"], + [["row1", "row2", "row3"], ["row4", "row5", "row6"]], + ["col4", "col5"], + [["row1", "row2"], ["row3", "row4"]], + True, + "critical", + "failed" + ), + # Check 8: FAIL_O Formatted only + ( + "fake_func_name_FAIL_O_formatted_only_test", + "FAIL_O Formatted only", + "", + script.FAIL_O, + "reboot", + "test reason", + "https://test_doc_url.html", + ["col1", "col2", "col3"], + [["row1", "row2", "row3"], ["row4", "row5", "row6"]], + [], + [], + True, + "critical", + "failed" + ), + # Check 9: FAIL_O + ( + "fake_func_name_FAIL_O_unformatted_only_test", + "FAIL_O Unformatted only", + "", + script.FAIL_O, + "reboot", + "test reason", + "https://test_doc_url.html", + [], + [], + ["col1", "col2", "col3"], + [["row1", "row2", "row3"], ["row4", "row5", "row6"]], + True, + "critical", + "failed" + ), + ], +) +def test_AciResult( + func_name, + name, + description, + result, + recommended_action, + reason, + doc_url, + column, + row, + unformatted_column, + unformatted_rows, + expected_show, + expected_criticality, + expected_passed, +): + synth = script.AciResult(func_name, name, description) + synth.updateWithResults(result, recommended_action, reason, doc_url, column, row, unformatted_column, unformatted_rows) + file = synth.writeResult() + with open(file, "r") as f: + data = json.load(f) + assert data["ruleId"] == func_name + assert data["showValidation"] == expected_show + assert data["severity"] == expected_criticality + assert data["ruleStatus"] == expected_passed + + +@pytest.mark.parametrize( + "headers, data", + [ + ("", []), # invalid headers (columns) + ([], {}), # invalid data (rows) + ("", {}), # invalid headers and data + ] +) +def test_invalid_headers_or_data(headers, data): + with pytest.raises(TypeError): + synth = script.AciResult("func_name", "Check Title", "A Description") + synth.craftData( + column=headers, + rows=data, + ) \ No newline at end of file diff --git a/tests/test_parse_args.py b/tests/test_parse_args.py index 74bd37d1..5d3f55d8 100644 --- a/tests/test_parse_args.py +++ b/tests/test_parse_args.py @@ -14,23 +14,24 @@ def test_no_args(): # To simulate the script being run without any command-line arguments, # we set `sys.argv[1:]` to an empty list when `args` is `None`. sys.argv[1:] = [] - is_puv, tversion, cversion, debug_function = script.parse_args(args=None) - assert is_puv is False + api_only, tversion, cversion, debug_function, no_cleanup = script.parse_args(args=None) + assert api_only is False assert tversion is None assert cversion is None assert debug_function is None + assert no_cleanup is False @pytest.mark.parametrize( "args, expected_result", [ ([], False), - (["--puv"], True), + (["--api-only"], True), ], ) -def test_puv(args, expected_result): - is_puv, tversion, cversion, debug_function = script.parse_args(args) - assert is_puv == expected_result +def test_api_only(args, expected_result): + api_only, tversion, cversion, debug_function, no_cleanup = script.parse_args(args) + assert api_only == expected_result @pytest.mark.parametrize( @@ -45,7 +46,7 @@ def test_puv(args, expected_result): ], ) def test_tversion(args, expected_result): - is_puv, tversion, cversion, debug_function = script.parse_args(args) + api_only, tversion, cversion, debug_function, no_cleanup = script.parse_args(args) if tversion is not None: assert isinstance(tversion, str) assert str(tversion) == str(expected_result) @@ -63,7 +64,7 @@ def test_tversion(args, expected_result): ], ) def test_cversion(args, expected_result): - is_puv, tversion, cversion, debug_function = script.parse_args(args) + api_only, tversion, cversion, debug_function, no_cleanup = script.parse_args(args) if cversion is not None: assert isinstance(cversion, str) assert str(cversion) == str(expected_result) @@ -78,7 +79,7 @@ def test_cversion(args, expected_result): ], ) def test_debug_func(args, expected_result): - is_puv, tversion, cversion, debug_function = script.parse_args(args) + api_only, tversion, cversion, debug_function, no_cleanup = script.parse_args(args) if debug_function is not None: assert isinstance(debug_function, str) assert str(debug_function) == str(expected_result) diff --git a/tests/test_prepare.py b/tests/test_prepare.py index 00bcfc62..7589bec7 100644 --- a/tests/test_prepare.py +++ b/tests/test_prepare.py @@ -62,7 +62,7 @@ def _mock_get_target_version(arg_tversion): @pytest.mark.parametrize( - "icurl_outputs, is_puv, arg_tversion, arg_cversion, debug_function, expected_result", + "icurl_outputs, api_only, arg_tversion, arg_cversion, debug_function, expected_result", [ # Default, no argparse arguments ( @@ -77,7 +77,7 @@ def _mock_get_target_version(arg_tversion): None, {"username": "admin", "password": "mypassword", "cversion": AciVersion("6.1(1a)"), "tversion": AciVersion("6.2(1a)"), "sw_cversion": AciVersion("6.0(9d)"), "vpc_node_ids": ["101", "102"]}, ), - # `is_puv` is True (i.e. --puv) + # `api_only` is True (i.e. --puv) # No `get_credentials()`, no username nor password ( { @@ -163,9 +163,9 @@ def _mock_get_target_version(arg_tversion): ), ], ) -def test_prepare(mock_icurl, is_puv, arg_tversion, arg_cversion, debug_function, expected_result): - checks = script.get_checks(is_puv, debug_function) - inputs = script.prepare(is_puv, arg_tversion, arg_cversion, len(checks)) +def test_prepare(mock_icurl, api_only, arg_tversion, arg_cversion, debug_function, expected_result): + checks = script.get_checks(api_only, debug_function) + inputs = script.prepare(api_only, arg_tversion, arg_cversion, len(checks)) for key, value in expected_result.items(): if "version" in key: assert isinstance(inputs[key], AciVersion) @@ -182,7 +182,7 @@ def test_prepare(mock_icurl, is_puv, arg_tversion, arg_cversion, debug_function, assert meta["cversion"] == str(expected_result["cversion"]) assert meta["tversion"] == str(expected_result["tversion"]) assert meta["sw_cversion"] == str(expected_result["sw_cversion"]) - assert meta["is_puv"] == is_puv + assert meta["api_only"] == api_only assert meta["total_checks"] == len(checks) if debug_function: assert meta["total_checks"] == 1 @@ -201,7 +201,7 @@ def test_cversion_invald(): @pytest.mark.parametrize( - "icurl_outputs, is_puv, arg_tversion, arg_cversion, debug_function, expected_result", + "icurl_outputs, api_only, arg_tversion, arg_cversion, debug_function, expected_result", [ # `get_cversion()` failure ( @@ -259,12 +259,12 @@ def test_cversion_invald(): ), ], ) -def test_prepare_exception(capsys, caplog, mock_icurl, is_puv, arg_tversion, arg_cversion, debug_function, expected_result): +def test_prepare_exception(capsys, caplog, mock_icurl, api_only, arg_tversion, arg_cversion, debug_function, expected_result): caplog.set_level(logging.CRITICAL) with pytest.raises(SystemExit): with pytest.raises(Exception): - checks = script.get_checks(is_puv, debug_function) - script.prepare(is_puv, arg_tversion, arg_cversion, len(checks)) + checks = script.get_checks(api_only, debug_function) + script.prepare(api_only, arg_tversion, arg_cversion, len(checks)) captured = capsys.readouterr() print(captured.out) assert captured.out.endswith(expected_result) diff --git a/tests/test_run_checks.py b/tests/test_run_checks.py index 753d322e..708fadaa 100644 --- a/tests/test_run_checks.py +++ b/tests/test_run_checks.py @@ -6,7 +6,7 @@ script = importlib.import_module("aci-preupgrade-validation-script") AciVersion = script.AciVersion JSON_DIR = script.JSON_DIR -ApicResult = script.syntheticMaintPValidate +ApicResult = script.AciResult Result = script.Result check_wrapper = script.check_wrapper From 385fcb5f5b9d15c63046316c2d8f44d5a3314754 Mon Sep 17 00:00:00 2001 From: Gabriel <gmonroy@cisco.com> Date: Mon, 14 Jul 2025 15:54:11 -0400 Subject: [PATCH 25/32] same updates --- aci-preupgrade-validation-script.py | 38 ++--- tests/test_synthenticMaintPValidate.py | 207 ------------------------- 2 files changed, 20 insertions(+), 225 deletions(-) delete mode 100644 tests/test_synthenticMaintPValidate.py diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 39f6e5a8..e2fb0a65 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -919,9 +919,9 @@ def is_firstver_gt_secondver(first_ver, second_ver): return result -class syntheticMaintPValidate: +class AciResult: """ - APIC uses an object called `syntheticMaintPValidate` to store the results of + APIC uses an object called `AciResult` to store the results of each rule/check in the pre-upgrade validation which is run during the upgrade workflow in the APIC GUI. When this script is invoked during the workflow, it is expected to write the results of each rule/check to a JSON file (one per rule) @@ -1055,7 +1055,7 @@ def wrapper(index, total_checks, *args, **kwargs): # both show the original check func name and `wrapper.__name__` can # be dynamically changed inside each check func if needed. (mainly # for test or debugging) - synth = syntheticMaintPValidate(wrapper.__name__, check_title, "") + synth = AciResult(wrapper.__name__, check_title, "") synth.updateWithResults(**r.as_dict_for_json_result()) synth.writeResult() return r.result @@ -5216,22 +5216,24 @@ def parse_args(args): parser = ArgumentParser(description="ACI Pre-Upgrade Validation Script - %s" % SCRIPT_VERSION) parser.add_argument("-t", "--tversion", action="store", type=str, help="Upgrade Target Version. Ex. 6.2(1a)") parser.add_argument("-c", "--cversion", action="store", type=str, help="Override Current Version. Ex. 6.1(1a)") - parser.add_argument("-d", "--debug_function", action="store", type=str, help="Name of a single function to debug. Ex. 'apic_version_md5_check'") - parser.add_argument("--puv", action="store_true", help="For built-in PUV. API Checks only. Checks using SSH are skipped.") + parser.add_argument("-d", "--debug-function", action="store", type=str, help="Name of a single function to debug. Ex. 'apic_version_md5_check'") + parser.add_argument("--api-only", action="store_true", help="For built-in PUV. API Checks only. Checks using SSH are skipped.") + parser.add_argument("--no-cleanup", action="store_true", help="Skip all file cleanup after script execution.") parsed_args = parser.parse_args(args) - is_puv = parsed_args.puv + api_only = parsed_args.api_only tversion = parsed_args.tversion cversion = parsed_args.cversion debug_function = parsed_args.debug_function - return is_puv, tversion, cversion, debug_function + no_cleanup = parsed_args.no_cleanup + return api_only, tversion, cversion, debug_function, no_cleanup -def prepare(is_puv, arg_tversion, arg_cversion, total_checks): +def prepare(api_only, arg_tversion, arg_cversion, total_checks): prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') username = password = None - if not is_puv: + if not api_only: username, password = get_credentials() try: cversion = get_current_version(arg_cversion) @@ -5254,7 +5256,7 @@ def prepare(is_puv, arg_tversion, arg_cversion, total_checks): "cversion": str(cversion), "tversion": str(tversion), "sw_cversion": str(sw_cversion), - "is_puv": is_puv, + "api_only": api_only, "total_checks": total_checks, } with open(META_FILE, "w") as f: @@ -5262,7 +5264,7 @@ def prepare(is_puv, arg_tversion, arg_cversion, total_checks): return inputs -def get_checks(is_puv, debug_func): +def get_checks(api_only, debug_func): api_checks = [ # General Checks target_version_compatibility_check, @@ -5368,7 +5370,7 @@ def get_checks(is_puv, debug_func): ] if debug_func: return [check for check in api_checks + conn_checks if check.__name__ == debug_func] - if is_puv: + if api_only: return api_checks return conn_checks + api_checks @@ -5394,7 +5396,7 @@ def run_checks(checks, inputs): json.dump(summary, f, indent=2) -def wrapup(is_puv): +def wrapup(no_cleanup): subprocess.check_output(['tar', '-czf', BUNDLE_NAME, DIR]) bundle_loc = '/'.join([os.getcwd(), BUNDLE_NAME]) prints(""" @@ -5409,16 +5411,16 @@ def wrapup(is_puv): prints('==== Script Version %s FIN ====' % (SCRIPT_VERSION)) # puv integration needs to keep reading files from `JSON_DIR` under `DIR`. - if not is_puv and os.path.isdir(DIR): + if not no_cleanup and os.path.isdir(DIR): shutil.rmtree(DIR) def main(args=None): - is_puv, arg_tversion, arg_cversion, debug_func = parse_args(args) - checks = get_checks(is_puv, debug_func) - inputs = prepare(is_puv, arg_tversion, arg_cversion, len(checks)) + api_only, arg_tversion, arg_cversion, debug_func, no_cleanup = parse_args(args) + checks = get_checks(api_only, debug_func) + inputs = prepare(api_only, arg_tversion, arg_cversion, len(checks)) run_checks(checks, inputs) - wrapup(is_puv) + wrapup(no_cleanup) if __name__ == "__main__": diff --git a/tests/test_synthenticMaintPValidate.py b/tests/test_synthenticMaintPValidate.py deleted file mode 100644 index 329f56aa..00000000 --- a/tests/test_synthenticMaintPValidate.py +++ /dev/null @@ -1,207 +0,0 @@ -import pytest -import importlib -import json - -script = importlib.import_module("aci-preupgrade-validation-script") - - -@pytest.mark.parametrize( - "func_name, name, description, result, recommended_action, reason, doc_url, column, row, unformatted_column, unformatted_rows, expected_show, expected_criticality, expected_passed", - [ - # Check 1: NA - ( - "fake_func_name_NA_test", - "NA", - "", - script.NA, - "", - "", - "", - ["col1", "col2"], - [["row1", "row2"], ["row3", "row4"]], - ["col1", "col2"], - [["row1", "row2"], ["row3", "row4"]], - False, - "informational", - "passed" - ), - # Check 2: PASS - ( - "fake_func_name_PASS_test", - "PASS", - "", - script.PASS, - "", - "", - "", - [], - [], - [], - [], - True, - "informational", - "passed" - ), - # Check 3: POST - ( - "fake_func_name_POST_test", - "POST", - "", - script.POST, - "reboot", - "test reason", - "https://test_doc_url.html", - ["col1", "col2"], - [["row1", "row2"], ["row3", "row4"]], - ["col1", "col2"], - [["row1", "row2"], ["row3", "row4"]], - False, - "informational", - "failed" - ), - # Check 4: MANUAL - ( - "fake_func_name_MANUAL_test", - "MANUAL", - "", - script.MANUAL, - "reboot", - "test reason", - "https://test_doc_url.html", - ["col1", "col2"], - [["row1", "row2"], ["row3", "row4"]], - ["col1", "col2"], - [["row1", "row2"], ["row3", "row4"]], - True, - "warning", - "failed" - ), - # Check 5: ERROR - ( - "fake_func_name_ERROR_test", - "ERROR", - "", - script.ERROR, - "reboot", - "test reason", - "https://test_doc_url.html", - ["col1", "col2"], - [["row1", "row2"], ["row3", "row4"]], - ["col1", "col2"], - [["row1", "row2"], ["row3", "row4"]], - True, - "major", - "failed" - ), - # Check 6: FAIL_UF - ( - "fake_func_name_FAIL_UF_test", - "FAIL_UF", - "", - script.FAIL_UF, - "reboot", - "test reason", - "https://test_doc_url.html", - ["col1", "col2"], - [["row1", "row2"], ["row3", "row4"]], - ["col1", "col2"], - [["row1", "row2"], ["row3", "row4"]], - True, - "critical", - "failed" - ), - # Check 7: FAIL_O - ( - "fake_func_name_FAIL_O_test", - "FAIL_O", - "", - script.FAIL_O, - "reboot", - "test reason", - "https://test_doc_url.html", - ["col1", "col2", "col3"], - [["row1", "row2", "row3"], ["row4", "row5", "row6"]], - ["col4", "col5"], - [["row1", "row2"], ["row3", "row4"]], - True, - "critical", - "failed" - ), - # Check 8: FAIL_O Formatted only - ( - "fake_func_name_FAIL_O_formatted_only_test", - "FAIL_O Formatted only", - "", - script.FAIL_O, - "reboot", - "test reason", - "https://test_doc_url.html", - ["col1", "col2", "col3"], - [["row1", "row2", "row3"], ["row4", "row5", "row6"]], - [], - [], - True, - "critical", - "failed" - ), - # Check 9: FAIL_O - ( - "fake_func_name_FAIL_O_unformatted_only_test", - "FAIL_O Unformatted only", - "", - script.FAIL_O, - "reboot", - "test reason", - "https://test_doc_url.html", - [], - [], - ["col1", "col2", "col3"], - [["row1", "row2", "row3"], ["row4", "row5", "row6"]], - True, - "critical", - "failed" - ), - ], -) -def test_syntheticMaintPValidate( - func_name, - name, - description, - result, - recommended_action, - reason, - doc_url, - column, - row, - unformatted_column, - unformatted_rows, - expected_show, - expected_criticality, - expected_passed, -): - synth = script.syntheticMaintPValidate(func_name, name, description) - synth.updateWithResults(result, recommended_action, reason, doc_url, column, row, unformatted_column, unformatted_rows) - file = synth.writeResult() - with open(file, "r") as f: - data = json.load(f) - assert data["ruleId"] == func_name - assert data["showValidation"] == expected_show - assert data["severity"] == expected_criticality - assert data["ruleStatus"] == expected_passed - - -@pytest.mark.parametrize( - "headers, data", - [ - ("", []), # invalid headers (columns) - ([], {}), # invalid data (rows) - ("", {}), # invalid headers and data - ] -) -def test_invalid_headers_or_data(headers, data): - with pytest.raises(TypeError): - synth = script.syntheticMaintPValidate("func_name", "Check Title", "A Description") - synth.craftData( - column=headers, - rows=data, - ) \ No newline at end of file From c46c9c18d8c13106d9b51a00e3774affe0cacf0c Mon Sep 17 00:00:00 2001 From: tkishida <tkishida@cisco.com> Date: Mon, 14 Jul 2025 18:06:52 -0700 Subject: [PATCH 26/32] Add --version and --total-checks --- aci-preupgrade-validation-script.py | 35 ++++++------ tests/test_main.py | 23 ++++++++ tests/test_parse_args.py | 84 ++++++++++++++++++++++------- 3 files changed, 106 insertions(+), 36 deletions(-) create mode 100644 tests/test_main.py diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index e2fb0a65..537076b7 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -5217,15 +5217,12 @@ def parse_args(args): parser.add_argument("-t", "--tversion", action="store", type=str, help="Upgrade Target Version. Ex. 6.2(1a)") parser.add_argument("-c", "--cversion", action="store", type=str, help="Override Current Version. Ex. 6.1(1a)") parser.add_argument("-d", "--debug-function", action="store", type=str, help="Name of a single function to debug. Ex. 'apic_version_md5_check'") - parser.add_argument("--api-only", action="store_true", help="For built-in PUV. API Checks only. Checks using SSH are skipped.") - parser.add_argument("--no-cleanup", action="store_true", help="Skip all file cleanup after script execution.") + parser.add_argument("-a", "--api-only", action="store_true", help="For built-in PUV. API Checks only. Checks using SSH are skipped.") + parser.add_argument("-n", "--no-cleanup", action="store_true", help="Skip all file cleanup after script execution.") + parser.add_argument("-v", "--version", action="store_true", help="Show the script version.") + parser.add_argument("--total-checks", action="store_true", help="Show the total number of checks.") parsed_args = parser.parse_args(args) - api_only = parsed_args.api_only - tversion = parsed_args.tversion - cversion = parsed_args.cversion - debug_function = parsed_args.debug_function - no_cleanup = parsed_args.no_cleanup - return api_only, tversion, cversion, debug_function, no_cleanup + return parsed_args def prepare(api_only, arg_tversion, arg_cversion, total_checks): @@ -5264,7 +5261,7 @@ def prepare(api_only, arg_tversion, arg_cversion, total_checks): return inputs -def get_checks(api_only, debug_func): +def get_checks(api_only, debug_function): api_checks = [ # General Checks target_version_compatibility_check, @@ -5368,8 +5365,8 @@ def get_checks(api_only, debug_func): observer_db_size_check, ] - if debug_func: - return [check for check in api_checks + conn_checks if check.__name__ == debug_func] + if debug_function: + return [check for check in api_checks + conn_checks if check.__name__ == debug_function] if api_only: return api_checks return conn_checks + api_checks @@ -5415,12 +5412,18 @@ def wrapup(no_cleanup): shutil.rmtree(DIR) -def main(args=None): - api_only, arg_tversion, arg_cversion, debug_func, no_cleanup = parse_args(args) - checks = get_checks(api_only, debug_func) - inputs = prepare(api_only, arg_tversion, arg_cversion, len(checks)) +def main(_args=None): + args = parse_args(_args) + if args.version: + prints(SCRIPT_VERSION) + return + checks = get_checks(args.api_only, args.debug_function) + if args.total_checks: + prints("Total Number of Checks: {}".format(len(checks))) + return + inputs = prepare(args.api_only, args.tversion, args.cversion, len(checks)) run_checks(checks, inputs) - wrapup(no_cleanup) + wrapup(args.no_cleanup) if __name__ == "__main__": diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 00000000..d89a6741 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,23 @@ +import pytest +import importlib +import sys + +script = importlib.import_module("aci-preupgrade-validation-script") +AciVersion = script.AciVersion + + +def test_args_version(capsys): + script.main(["--version"]) + captured = capsys.readouterr() + print(captured.out) + assert "{}\n".format(script.SCRIPT_VERSION) == captured.out + + +@pytest.mark.parametrize("api_only", [False, True]) +def test_args_total_checks(capsys, api_only): + args = ["--total-checks", "--api-only"] if api_only else ["--total-checks"] + checks = script.get_checks(api_only=api_only, debug_function=None) + script.main(args) + captured = capsys.readouterr() + print(captured.out) + assert "Total Number of Checks: {}\n".format(len(checks)) == captured.out diff --git a/tests/test_parse_args.py b/tests/test_parse_args.py index 5d3f55d8..a460da9d 100644 --- a/tests/test_parse_args.py +++ b/tests/test_parse_args.py @@ -14,24 +14,27 @@ def test_no_args(): # To simulate the script being run without any command-line arguments, # we set `sys.argv[1:]` to an empty list when `args` is `None`. sys.argv[1:] = [] - api_only, tversion, cversion, debug_function, no_cleanup = script.parse_args(args=None) - assert api_only is False - assert tversion is None - assert cversion is None - assert debug_function is None - assert no_cleanup is False + args = script.parse_args(args=None) + assert args.api_only is False + assert args.tversion is None + assert args.cversion is None + assert args.debug_function is None + assert args.no_cleanup is False + assert args.version is False + assert args.total_checks is False @pytest.mark.parametrize( "args, expected_result", [ ([], False), + (["-a"], True), (["--api-only"], True), ], ) def test_api_only(args, expected_result): - api_only, tversion, cversion, debug_function, no_cleanup = script.parse_args(args) - assert api_only == expected_result + args = script.parse_args(args) + assert args.api_only == expected_result @pytest.mark.parametrize( @@ -43,13 +46,14 @@ def test_api_only(args, expected_result): (["-t", "n9000-16.2(1a).bin"], "n9000-16.2(1a).bin"), (["-t", "aci-apic-dk9.6.2.1a.bin"], "aci-apic-dk9.6.2.1a.bin"), (["-t", "invalid_version"], "invalid_version"), + (["--tversion", "6.2(1a)"], "6.2(1a)"), ], ) def test_tversion(args, expected_result): - api_only, tversion, cversion, debug_function, no_cleanup = script.parse_args(args) - if tversion is not None: - assert isinstance(tversion, str) - assert str(tversion) == str(expected_result) + args = script.parse_args(args) + if args.tversion is not None: + assert isinstance(args.tversion, str) + assert str(args.tversion) == str(expected_result) @pytest.mark.parametrize( @@ -61,13 +65,14 @@ def test_tversion(args, expected_result): (["-c", "n9000-16.2(1a).bin"], "n9000-16.2(1a).bin"), (["-c", "aci-apic-dk9.6.2.1a.bin"], "aci-apic-dk9.6.2.1a.bin"), (["-c", "invalid_version"], "invalid_version"), + (["--cversion", "6.2(1a)"], "6.2(1a)"), ], ) def test_cversion(args, expected_result): - api_only, tversion, cversion, debug_function, no_cleanup = script.parse_args(args) - if cversion is not None: - assert isinstance(cversion, str) - assert str(cversion) == str(expected_result) + args = script.parse_args(args) + if args.cversion is not None: + assert isinstance(args.cversion, str) + assert str(args.cversion) == str(expected_result) @pytest.mark.parametrize( @@ -76,10 +81,49 @@ def test_cversion(args, expected_result): ([], None), (["-d", "pbr_high_scale_check"], "pbr_high_scale_check"), (["-d", "made_up_func"], "made_up_func"), + (["--debug-func", "pbr_high_scale_check"], "pbr_high_scale_check"), ], ) def test_debug_func(args, expected_result): - api_only, tversion, cversion, debug_function, no_cleanup = script.parse_args(args) - if debug_function is not None: - assert isinstance(debug_function, str) - assert str(debug_function) == str(expected_result) + args = script.parse_args(args) + if args.debug_function is not None: + assert isinstance(args.debug_function, str) + assert str(args.debug_function) == str(expected_result) + + +@pytest.mark.parametrize( + "args, expected_result", + [ + ([], False), + (["-n"], True), + (["--no-cleanup"], True), + ], +) +def test_no_cleanup(args, expected_result): + args = script.parse_args(args) + assert args.no_cleanup == expected_result + + +@pytest.mark.parametrize( + "args, expected_result", + [ + ([], False), + (["-v"], True), + (["--version"], True), + ], +) +def test_version(args, expected_result): + args = script.parse_args(args) + assert args.version == expected_result + + +@pytest.mark.parametrize( + "args, expected_result", + [ + ([], False), + (["--total-checks"], True), + ], +) +def test_total_checks(args, expected_result): + args = script.parse_args(args) + assert args.total_checks == expected_result From 68be98aaf9cfae0443e4991e82a3d26cb92cb8f4 Mon Sep 17 00:00:00 2001 From: tkishida <tkishida@cisco.com> Date: Mon, 14 Jul 2025 19:31:09 -0700 Subject: [PATCH 27/32] Do not touch log folders with some options like --version --- aci-preupgrade-validation-script.py | 116 ++++++++++++++++------------ tests/conftest.py | 7 +- tests/test_main.py | 1 - 3 files changed, 71 insertions(+), 53 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 537076b7..c2dd7632 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -68,14 +68,10 @@ RESULT_FILE = DIR + 'preupgrade_validator_%s%s.txt' % (ts, tz) SUMMARY_FILE = DIR + 'summary.json' LOG_FILE = DIR + 'preupgrade_validator_debug.log' -fmt = '[%(asctime)s.%(msecs)03d{} %(levelname)-8s %(funcName)20s:%(lineno)-4d] %(message)s'.format(tz) -if os.path.isdir(DIR): - shutil.rmtree(DIR) -os.mkdir(DIR) -os.mkdir(JSON_DIR) -logging.basicConfig(level=logging.DEBUG, filename=LOG_FILE, format=fmt, datefmt='%Y-%m-%d %H:%M:%S') warnings.simplefilter(action='ignore', category=FutureWarning) +log = logging.getLogger() + class OldVerClassNotFound(Exception): """ Later versions of ACI can have class properties not found in older versions """ @@ -147,7 +143,7 @@ def __init__(self, hostname): def __connected(self): # determine if a connection is already open connected = (self.child is not None and self.child.isatty()) - logging.debug("check for valid connection: %r" % connected) + log.debug("check for valid connection: %r" % connected) return connected @property @@ -173,7 +169,7 @@ def start_log(self): self._log = open(self.log, "ab") else: self._log = self.log - logging.debug("setting logfile to %s" % self._log.name) + log.debug("setting logfile to %s" % self._log.name) if self.child is not None: self.child.logfile = self._log @@ -195,18 +191,18 @@ def connect(self): self.port = 23 # spawn new thread if self.protocol.lower() == "ssh": - logging.debug( + log.debug( "spawning new pexpect connection: ssh %s@%s -p %d" % (self.username, self.hostname, self.port)) no_verify = " -o StrictHostKeyChecking=no -o LogLevel=ERROR -o UserKnownHostsFile=/dev/null" if self.verify: no_verify = "" self.child = pexpect.spawn("ssh %s %s@%s -p %d" % (no_verify, self.username, self.hostname, self.port), searchwindowsize=self.searchwindowsize) elif self.protocol.lower() == "telnet": - logging.info("spawning new pexpect connection: telnet %s %d" % (self.hostname, self.port)) + log.info("spawning new pexpect connection: telnet %s %d" % (self.hostname, self.port)) self.child = pexpect.spawn("telnet %s %d" % (self.hostname, self.port), searchwindowsize=self.searchwindowsize) else: - logging.error("unknown protocol %s" % self.protocol) + log.error("unknown protocol %s" % self.protocol) raise Exception("Unsupported protocol: %s" % self.protocol) # start logging @@ -215,7 +211,7 @@ def connect(self): def close(self): # try to gracefully close the connection if opened if self.__connected(): - logging.info("closing current connection") + log.info("closing current connection") self.child.close() self.child = None self._login = False @@ -241,16 +237,16 @@ def __expect(self, matches, timeout=None): indexed.append(matches[i]) mapping.append(i) result = self.child.expect(indexed, timeout) - logging.debug("timeout: %d, matched: '%s'\npexpect output: '%s%s'" % ( + log.debug("timeout: %d, matched: '%s'\npexpect output: '%s%s'" % ( timeout, self.child.after, self.child.before, self.child.after)) if result <= len(mapping) and result >= 0: - logging.debug("expect matched result[%d] = %s" % (result, mapping[result])) + log.debug("expect matched result[%d] = %s" % (result, mapping[result])) return mapping[result] ds = '' - logging.error("unexpected pexpect return index: %s" % result) + log.error("unexpected pexpect return index: %s" % result) for i in range(0, len(mapping)): ds += '[%d] %s\n' % (i, mapping[i]) - logging.debug("mapping:\n%s" % ds) + log.debug("mapping:\n%s" % ds) raise Exception("Unexpected pexpect return index: %s" % result) def login(self, max_attempts=7, timeout=17): @@ -258,7 +254,7 @@ def login(self, max_attempts=7, timeout=17): returns true on successful login, else returns false """ - logging.debug("Logging into host") + log.debug("Logging into host") # successfully logged in at a different time if not self.__connected(): self.connect() @@ -277,35 +273,35 @@ def login(self, max_attempts=7, timeout=17): max_attempts -= 1 match = self.__expect(matches, timeout) if match == "console": # press return to get started - logging.debug("matched console, send enter") + log.debug("matched console, send enter") self.child.sendline("\r\n") elif match == "refuse": # connection refused - logging.error("connection refused by host") + log.error("connection refused by host") return False elif match == "yes/no": # yes/no for SSH key acceptance - logging.debug("received yes/no prompt, send yes") + log.debug("received yes/no prompt, send yes") self.child.sendline("yes") elif match == "username": # username/login prompt - logging.debug("received username prompt, send username") + log.debug("received username prompt, send username") self.child.sendline(self.username) elif match == "password": # don't log passwords to the logfile self.stop_log() - logging.debug("matched password prompt, send password") + log.debug("matched password prompt, send password") self.child.sendline(self.password) # restart logging self.start_log() elif match == "prompt": - logging.debug("successful login") + log.debug("successful login") self._login = True # force terminal length at login self.term_len = self._term_len return True elif match == "timeout": - logging.debug("timeout received but connection still opened, send enter") + log.debug("timeout received but connection still opened, send enter") self.child.sendline("\r\n") # did not find prompt within max attempts, failed login - logging.error("failed to login after multiple attempts") + log.error("failed to login after multiple attempts") return False def cmd(self, command, **kargs): @@ -348,7 +344,7 @@ def cmd(self, command, **kargs): self.output = "" # check if we've ever logged into device or currently connected if (not self.__connected()) or (not self._login): - logging.debug("no active connection, attempt to login") + log.debug("no active connection, attempt to login") if not self.login(): raise Exception("failed to login to host") @@ -357,7 +353,7 @@ def cmd(self, command, **kargs): if not echo_cmd: self.stop_log() # execute command - logging.debug("cmd command: %s" % command) + log.debug("cmd command: %s" % command) if sendline: self.child.sendline(command) else: @@ -373,7 +369,7 @@ def cmd(self, command, **kargs): result = self.__expect(matches, timeout) self.output = "%s%s" % (self.child.before.decode("utf-8"), self.child.after.decode("utf-8")) if result == "eof" or result == "timeout": - logging.warning("unexpected %s occurred" % result) + log.warning("unexpected %s occurred" % result) return result @@ -693,7 +689,7 @@ def get_node_ids_from_ifp(self, ifp_dn): def get_node_ids_from_ifsel(self, ifsel_dn): ifp = self.get_parent(ifsel_dn, self.IFP) if not ifp: - logging.warning("No I/F Profile for Selector (%s)", ifsel_dn) + log.warning("No I/F Profile for Selector (%s)", ifsel_dn) return [] node_ids = self.get_node_ids_from_ifp(ifp["dn"]) return node_ids @@ -765,7 +761,7 @@ def create_port_data(self): node2fexid[_node_id] = fex_id node_ids = node2fexid.keys() if len(node_ids) > 2: - logging.error( + log.error( "FEX HIF handling failed as it shows more than 2 nodes." ) break @@ -1045,7 +1041,7 @@ def wrapper(index, total_checks, *args, **kwargs): r = check_func(*args, **kwargs) except Exception as e: r = Result(result=ERROR, msg='Unexpected Error: {}'.format(e)) - logging.exception(e) + log.exception(e) # Print `[Check 1/81] <title>... <result> + <failure details>` print_result(title=check_title, **r.as_dict()) @@ -1241,9 +1237,9 @@ def _icurl(apitype, query, page=0, page_size=100000): query += '{}page={}&page-size={}'.format(pre, page, page_size) uri = 'http://127.0.0.1:7777/api/{}/{}'.format(apitype, query) cmd = ['icurl', '-gs', uri] - logging.info('cmd = ' + ' '.join(cmd)) + log.info('cmd = ' + ' '.join(cmd)) response = subprocess.check_output(cmd) - logging.debug('response: ' + str(response)) + log.debug('response: ' + str(response)) data = json.loads(response) _icurl_error_handler(data['imdata']) return data @@ -1569,7 +1565,7 @@ def switch_group_guideline_check(**kwargs): if nodes[key]['role'] == 'spine': dn = re.search(node_regex, key) if not dn: - logging.error('Failed to parse - %s', key) + log.error('Failed to parse - %s', key) continue f_spines[0][dn.group('pod')].append(int(dn.group('node'))) @@ -1586,7 +1582,7 @@ def switch_group_guideline_check(**kwargs): if nodes.get(tDn, {}).get('role') == 'spine': dn = re.search(node_regex, tDn) if not dn: - logging.error('Failed to parse - %s', tDn) + log.error('Failed to parse - %s', tDn) continue f_spines[2][dn.group('pod')].append(int(dn.group('node'))) @@ -1595,7 +1591,7 @@ def switch_group_guideline_check(**kwargs): for lldp in lldps: dn = re.search(node_regex, lldp['lldpCtrlrAdjEp']['attributes']['dn']) if not dn: - logging.error('Failed to parse - %s', lldp['lldpCtrlrAdjEp']['attributes']['dn']) + log.error('Failed to parse - %s', lldp['lldpCtrlrAdjEp']['attributes']['dn']) continue apic_id_pod = '-'.join([lldp['lldpCtrlrAdjEp']['attributes']['id'], dn.group('pod')]) apic_leafs[apic_id_pod].add(int(dn.group('node'))) @@ -2663,7 +2659,7 @@ def l3out_overlapping_loopback_check(**kwargs): node = np_child['l3extRsNodeL3OutAtt'] m = re.search(node_regex, node['attributes']['tDn']) if not m: - logging.error('Failed to parse tDn - %s', node['attributes']['tDn']) + log.error('Failed to parse tDn - %s', node['attributes']['tDn']) continue node_id = m.group('node') @@ -2694,7 +2690,7 @@ def l3out_overlapping_loopback_check(**kwargs): port = ifp_child['l3extRsPathL3OutAtt'] m = re.search(path_regex, port['attributes']['tDn']) if not m: - logging.error('Failed to parse tDn - %s', port['attributes']['tDn']) + log.error('Failed to parse tDn - %s', port['attributes']['tDn']) continue node1_id = m.group('node1') node2_id = m.group('node2') @@ -3178,7 +3174,7 @@ def intersight_upgrade_status_check(**kwargs): cmd = ['icurl', '-gks', 'https://127.0.0.1/connector/UpgradeStatus'] - logging.info('cmd = ' + ' '.join(cmd)) + log.info('cmd = ' + ' '.join(cmd)) response = subprocess.check_output(cmd) try: resp_json = json.loads(response) @@ -3550,16 +3546,16 @@ def apic_ca_cert_validation(**kwargs): ''' # Re-run cleanup for Issue #120 if os.path.exists(cert_gen_filename): - logging.debug('CA CHECK file found and removed: ' + ''.join(cert_gen_filename)) + log.debug('CA CHECK file found and removed: ' + ''.join(cert_gen_filename)) os.remove(cert_gen_filename) if os.path.exists(key_pem): - logging.debug('CA CHECK file found and removed: ' + ''.join(key_pem)) + log.debug('CA CHECK file found and removed: ' + ''.join(key_pem)) os.remove(key_pem) if os.path.exists(csr_pem): - logging.debug('CA CHECK file found and removed: ' + ''.join(csr_pem)) + log.debug('CA CHECK file found and removed: ' + ''.join(csr_pem)) os.remove(csr_pem) if os.path.exists(sign): - logging.debug('CA CHECK file found and removed: ' + ''.join(sign)) + log.debug('CA CHECK file found and removed: ' + ''.join(sign)) os.remove(sign) with open(cert_gen_filename, 'w') as f: @@ -3569,7 +3565,7 @@ def apic_ca_cert_validation(**kwargs): cmd = 'openssl genrsa -out ' + key_pem + ' 2048' cmd = cmd + ' && openssl req -config ' + cert_gen_filename + ' -new -key ' + key_pem + ' -out ' + csr_pem cmd = cmd + ' && openssl dgst -sha256 -hmac ' + passphrase + ' -out ' + sign + ' ' + csr_pem - logging.debug('cmd = '+''.join(cmd)) + log.debug('cmd = '+''.join(cmd)) genrsa_proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) genrsa_proc.communicate()[0].strip() if genrsa_proc.returncode != 0: @@ -3589,11 +3585,11 @@ def apic_ca_cert_validation(**kwargs): payload = '{"aaaCertGenReq":{"attributes":{"type":"csvc","hmac":"%s", "certreq": "%s", ' \ '"podip": "None", "podmac": "None", "podname": "None"}}}' % (hmac, certreq) cmd = 'icurl -kX POST %s -d \' %s \'' % (url, payload) - logging.debug('cmd = ' + ''.join(cmd)) + log.debug('cmd = ' + ''.join(cmd)) certreq_proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) certreq_out = certreq_proc.communicate()[0].strip() - logging.debug(certreq_out) + log.debug(certreq_out) if '"error":{"attributes"' in str(certreq_out): # Spines can crash on 5.2(6e)+, but APIC CA Certs should be fixed regardless of tver data.append([certreq_out]) @@ -4431,7 +4427,7 @@ def vzany_vzany_service_epg_check(cversion, tversion, **kwargs): elif "vzRtAnyToProv" in vzRtAny: rel_class = "vzRtAnyToProv" else: - logging.warning("Unexpected class - %s", vzRtAny.keys()) + log.warning("Unexpected class - %s", vzRtAny.keys()) continue vrf_tdn = vzRtAny[rel_class]["attributes"]["tDn"] vrf_match = re.search(vrf_regex, vrf_tdn) @@ -5010,7 +5006,7 @@ def service_bd_forceful_routing_check(cversion, tversion, **kwargs): for fvRtEPpInfoToBD in fvRtEPpInfoToBDs: m = re.search(dn_regex, fvRtEPpInfoToBD["fvRtEPpInfoToBD"]["attributes"]["dn"]) if not m: - logging.error("Failed to match %s", fvRtEPpInfoToBD["fvRtEPpInfoToBD"]["attributes"]["dn"]) + log.error("Failed to match %s", fvRtEPpInfoToBD["fvRtEPpInfoToBD"]["attributes"]["dn"]) unformatted_data.append([fvRtEPpInfoToBD["fvRtEPpInfoToBD"]["attributes"]["dn"]]) continue data.append([ @@ -5225,6 +5221,21 @@ def parse_args(args): return parsed_args +def initialize(): + """ + Initialize the script environment, create necessary directories and set up log. + Not required for some options such as `--version` or `--total-checks`. + """ + if os.path.isdir(DIR): + log.info("Cleaning up previous run files in %s", DIR) + shutil.rmtree(DIR) + log.info("Creating directories %s and %s", DIR, JSON_DIR) + os.mkdir(DIR) + os.mkdir(JSON_DIR) + fmt = '[%(asctime)s.%(msecs)03d{} %(levelname)-8s %(funcName)20s:%(lineno)-4d] %(message)s'.format(tz) + logging.basicConfig(level=logging.DEBUG, filename=LOG_FILE, format=fmt, datefmt='%Y-%m-%d %H:%M:%S') + + def prepare(api_only, arg_tversion, arg_cversion, total_checks): prints(' ==== %s%s, Script Version %s ====\n' % (ts, tz, SCRIPT_VERSION)) prints('!!!! Check https://github.com/datacenter/ACI-Pre-Upgrade-Validation-Script for Latest Release !!!!\n') @@ -5240,7 +5251,7 @@ def prepare(api_only, arg_tversion, arg_cversion, total_checks): except Exception as e: prints('\n\nError: %s' % e) prints("Initial query failed. Ensure APICs are healthy. Ending script run.") - logging.exception(e) + log.exception(e) sys.exit() inputs = {'username': username, 'password': password, 'cversion': cversion, 'tversion': tversion, @@ -5409,18 +5420,21 @@ def wrapup(no_cleanup): # puv integration needs to keep reading files from `JSON_DIR` under `DIR`. if not no_cleanup and os.path.isdir(DIR): + log.info('Cleaning up temporary files and directories...') shutil.rmtree(DIR) def main(_args=None): args = parse_args(_args) if args.version: - prints(SCRIPT_VERSION) + print(SCRIPT_VERSION) return checks = get_checks(args.api_only, args.debug_function) if args.total_checks: - prints("Total Number of Checks: {}".format(len(checks))) + print("Total Number of Checks: {}".format(len(checks))) return + + initialize() inputs = prepare(args.api_only, args.tversion, args.cversion, len(checks)) run_checks(checks, inputs) wrapup(args.no_cleanup) diff --git a/tests/conftest.py b/tests/conftest.py index 7972b208..2c837e0c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,12 @@ script = importlib.import_module("aci-preupgrade-validation-script") -log = logging.getLogger(__name__) +log = logging.getLogger() + + +@pytest.fixture(scope="session", autouse=True) +def init(): + script.initialize() @pytest.fixture diff --git a/tests/test_main.py b/tests/test_main.py index d89a6741..41c4543f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,6 +1,5 @@ import pytest import importlib -import sys script = importlib.import_module("aci-preupgrade-validation-script") AciVersion = script.AciVersion From d9efebb51a1cf410d68e413b749be1884680ca91 Mon Sep 17 00:00:00 2001 From: Gabriel <gmonroy@cisco.com> Date: Tue, 22 Jul 2025 09:49:47 -0400 Subject: [PATCH 28/32] arg verbiage update --- aci-preupgrade-validation-script.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index c2dd7632..fb82ee50 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -5215,8 +5215,8 @@ def parse_args(args): parser.add_argument("-d", "--debug-function", action="store", type=str, help="Name of a single function to debug. Ex. 'apic_version_md5_check'") parser.add_argument("-a", "--api-only", action="store_true", help="For built-in PUV. API Checks only. Checks using SSH are skipped.") parser.add_argument("-n", "--no-cleanup", action="store_true", help="Skip all file cleanup after script execution.") - parser.add_argument("-v", "--version", action="store_true", help="Show the script version.") - parser.add_argument("--total-checks", action="store_true", help="Show the total number of checks.") + parser.add_argument("-v", "--version", action="store_true", help="Only show the script version, then end.") + parser.add_argument("--total-checks", action="store_true", help="Only show the total number of checks, then end.") parsed_args = parser.parse_args(args) return parsed_args From fbf65e7280988a7bf85a8666dd6a9e1c030d3e4b Mon Sep 17 00:00:00 2001 From: Gabriel <gmonroy@cisco.com> Date: Tue, 22 Jul 2025 10:51:38 -0400 Subject: [PATCH 29/32] ValueError when col and row len do not match + pytest + offending check fixes --- aci-preupgrade-validation-script.py | 21 +++++++++++---------- tests/test_AciResult.py | 19 +++++++++++++++++-- tests/test_run_checks.py | 6 +++--- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index fb82ee50..d568a7ee 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -951,13 +951,14 @@ def craftData(column, rows): if not (isinstance(rows, list) and isinstance(column, list)): raise TypeError("Rows and column must be lists.") data = [] - for i in range(len(rows)): + c_len = len(column) + for row_entry in range(len(rows)): + r_len = len(rows[row_entry]) + if r_len != c_len: + raise ValueError("Row length ({}), data: {} does not match column length ({}).".format(r_len, rows[row_entry], c_len)) entry = {} - for j in range(len(column)): - if j < len(rows[i]): - entry[column[j]] = rows[i][j] - else: - entry[column[j]] = None + for col_pos in range(c_len): + entry[column[col_pos]] = rows[row_entry][col_pos] data.append(entry) return data @@ -1648,7 +1649,7 @@ def switch_group_guideline_check(**kwargs): def switch_bootflash_usage_check(tversion, **kwargs): result = FAIL_UF msg = '' - headers = ["Pod-ID", "Node-ID", "Utilization", "Alert"] + headers = ["Pod-ID", "Node-ID", "Utilization"] data = [] recommended_action = "Over 50% usage! Contact Cisco TAC for Support" doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#switch-node-bootflash-usage" @@ -2860,7 +2861,7 @@ def apic_version_md5_check(tversion, username, password, **kwargs): c.log = LOG_FILE c.connect() except Exception as e: - data.append([apic_name, '-', '-', str(e), '-']) + data.append([apic_name, '-', '-', str(e)]) print_result(node_title, ERROR) has_error = True continue @@ -2870,7 +2871,7 @@ def apic_version_md5_check(tversion, username, password, **kwargs): tversion.dot_version) except Exception as e: data.append([apic_name, '-', '-', - 'ls command via ssh failed due to:{}'.format(str(e)), '-']) + 'ls command via ssh failed due to:{}'.format(str(e))]) print_result(node_title, ERROR) has_error = True continue @@ -2884,7 +2885,7 @@ def apic_version_md5_check(tversion, username, password, **kwargs): tversion.dot_version) except Exception as e: data.append([apic_name, str(tversion), '-', - 'failed to check md5sum via ssh due to:{}'.format(str(e)), '-']) + 'failed to check md5sum via ssh due to:{}'.format(str(e))]) print_result(node_title, ERROR) has_error = True continue diff --git a/tests/test_AciResult.py b/tests/test_AciResult.py index 22651112..473e3552 100644 --- a/tests/test_AciResult.py +++ b/tests/test_AciResult.py @@ -144,7 +144,7 @@ "critical", "failed" ), - # Check 9: FAIL_O + # Check 9: FAIL_O unformatted only ( "fake_func_name_FAIL_O_unformatted_only_test", "FAIL_O Unformatted only", @@ -204,4 +204,19 @@ def test_invalid_headers_or_data(headers, data): synth.craftData( column=headers, rows=data, - ) \ No newline at end of file + ) + +@pytest.mark.parametrize( + "headers, data", + [ + (["col1", "col2"], [["row1"], ["row2"]]), # Rows are shorter + (["col1"], [["row1", "row2"], ["row3", "row4"]]), # columns are shorter + ] +) +def test_mismatched_lengths(headers, data): + with pytest.raises(ValueError): + synth = script.AciResult("func_name", "Check Title", "A Description") + synth.craftData( + column=headers, + rows=data, + ) diff --git a/tests/test_run_checks.py b/tests/test_run_checks.py index 708fadaa..9b9f796e 100644 --- a/tests/test_run_checks.py +++ b/tests/test_run_checks.py @@ -6,7 +6,7 @@ script = importlib.import_module("aci-preupgrade-validation-script") AciVersion = script.AciVersion JSON_DIR = script.JSON_DIR -ApicResult = script.AciResult +AciResult = script.AciResult Result = script.Result check_wrapper = script.check_wrapper @@ -162,10 +162,10 @@ def test_run_checks(capsys, caplog): else: assert data["failureDetails"]["failType"] == "" # failureDetails.data - assert data["failureDetails"]["data"] == ApicResult.craftData( + assert data["failureDetails"]["data"] == AciResult.craftData( others.get("headers", []), others.get("data", []) ) - assert data["failureDetails"]["unformatted_data"] == ApicResult.craftData( + assert data["failureDetails"]["unformatted_data"] == AciResult.craftData( others.get("unformatted_headers", []), others.get("unformatted_data", []) ) # other fields From 3c55922741ddc880b92d3c885123e46ec4437f60 Mon Sep 17 00:00:00 2001 From: Gabriel <gmonroy@cisco.com> Date: Tue, 22 Jul 2025 12:19:44 -0400 Subject: [PATCH 30/32] fix more checks with mismatched col row length --- aci-preupgrade-validation-script.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index d568a7ee..24283eef 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -1747,9 +1747,9 @@ def l3out_mtu_check(**kwargs): @check_wrapper(check_title="L3 Port Config (F0467 port-configured-as-l2)") def port_configured_as_l2_check(**kwargs): result = FAIL_O - headers = ['Fault', 'Tenant', 'L3Out', 'Node', 'Path', 'Recommended Action'] + headers = ['Fault', 'Tenant', 'L3Out', 'Node', 'Path'] data = [] - unformatted_headers = ['Fault', 'Fault DN', 'Recommended Action'] + unformatted_headers = ['Fault', 'Fault DN'] unformatted_data = [] recommended_action = 'Resolve the conflict by removing this config or other configs using this port as L2' doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#l2l3-port-config" @@ -2006,7 +2006,7 @@ def bd_subnet_overlap_check(**kwargs): result = FAIL_O headers = ["Fault", "Pod", "Node", "VRF", "Interface", "Address"] data = [] - unformatted_headers = ['Fault', 'Fault DN', 'Recommended Action'] + unformatted_headers = ['Fault', 'Fault DN'] unformatted_data = [] recommended_action = 'Resolve the conflict by removing BD subnets causing the overlap' doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#bd-subnets" @@ -2168,7 +2168,7 @@ def apic_ssd_check(cversion, username, password, **kwargs): result = FAIL_UF headers = ["Pod", "Node", "Storage Unit", "% lifetime remaining", "Recommended Action"] data = [] - unformatted_headers = ["Pod", "Node", "Storage Unit", "% lifetime remaining", "Recommended Action"] + unformatted_headers = ["Fault", "Fault DN", "% lifetime remaining", "Recommended Action"] unformatted_data = [] recommended_action = "Contact TAC for replacement" doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#apic-ssd-health" @@ -2201,7 +2201,7 @@ def apic_ssd_check(cversion, username, password, **kwargs): c.log = LOG_FILE c.connect() except Exception as e: - data.append([attr['id'], attr['name'], '-', '-', '-', str(e)]) + data.append([attr['id'], attr['name'], '-', '-', str(e)]) print_result(node_title, ERROR) has_error = True continue @@ -2209,7 +2209,7 @@ def apic_ssd_check(cversion, username, password, **kwargs): c.cmd( 'grep -oE "SSD Wearout Indicator is [0-9]+" /var/log/dme/log/svc_ifc_ae.bin.log | tail -1') except Exception as e: - data.append([attr['id'], attr['name'], '-', '-', '-', str(e)]) + data.append([attr['id'], attr['name'], '-', '-', str(e)]) print_result(node_title, ERROR) has_error = True continue @@ -2227,7 +2227,6 @@ def apic_ssd_check(cversion, username, password, **kwargs): print_result(node_title, DONE) else: headers = ["Fault", "Pod", "Node", "Storage Unit", "% lifetime remaining", "Recommended Action"] - unformatted_headers = ["Fault", "Fault DN", "% lifetime remaining", "Recommended Action"] for faultInst in faultInsts: dn_array = re.search(dn_regex, faultInst['faultInst']['attributes']['dn']) lifetime_remaining = "<5%" @@ -4906,7 +4905,7 @@ def standby_sup_sync_check(cversion, tversion, **kwargs): @check_wrapper(check_title='Equipment Disk Limits Exceeded') def equipment_disk_limits_exceeded(**kwargs): result = PASS - headers = ['Pod', 'Node', 'Code', '%', 'Description',] + headers = ['Pod', 'Node', 'Code', '%', 'Description'] data = [] unformatted_headers = ['Fault DN', '%', 'Recommended Action'] unformatted_data = [] From df5403e5d7e731079e87bac734d0cbcf6a70ea5b Mon Sep 17 00:00:00 2001 From: Gabriel <gmonroy@cisco.com> Date: Tue, 22 Jul 2025 13:23:01 -0400 Subject: [PATCH 31/32] header cleanup + cimc logic update for QA --- aci-preupgrade-validation-script.py | 7 +- aciVersion_test.py | 67 +++++++++++++++++++ .../compatRsSuppHw_empty.json | 1 + .../test_cimc_compatibilty_check.py | 10 ++- 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 aciVersion_test.py create mode 100644 tests/cimc_compatibilty_check/compatRsSuppHw_empty.json diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 24283eef..cbb8abda 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2923,7 +2923,7 @@ def apic_version_md5_check(tversion, username, password, **kwargs): def standby_apic_disk_space_check(**kwargs): result = FAIL_UF msg = '' - headers = ['SN', 'OOB', 'Mount Point', 'Current Usage %'] + headers = ['SN', 'OOB', 'Mount Point', 'Current Usage %', 'Details'] data = [] recommended_action = 'Contact Cisco TAC' doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#standby-apic-disk-space-usage" @@ -2962,7 +2962,7 @@ def standby_apic_disk_space_check(**kwargs): directory = fs.group(1) usage = fs.group(5) if int(usage) >= threshold: - data.append([stb['mbSn'], stb['oobIpAddr'], directory, usage]) + data.append([stb['mbSn'], stb['oobIpAddr'], directory, usage, '-']) if not infraSnNodes: result = NA msg = 'No standby APIC found' @@ -3143,6 +3143,9 @@ def cimc_compatibilty_check(tversion, **kwargs): compat_lookup_dn = "uni/fabric/compcat-default/ctlrfw-apic-" + tversion.simple_version + \ "/rssuppHw-[uni/fabric/compcat-default/ctlrhw-" + model + "].json" compatMo = icurl('mo', compat_lookup_dn) + if not compatMo: + msg = "No compatibility information found for {}/{}".format(model, tversion.simple_version) + return Result(result=MANUAL, msg=msg, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) recommended_cimc = compatMo[0]['compatRsSuppHw']['attributes']['cimcVersion'] warning = "" if compatMo and recommended_cimc: diff --git a/aciVersion_test.py b/aciVersion_test.py new file mode 100644 index 00000000..8cc1cb8e --- /dev/null +++ b/aciVersion_test.py @@ -0,0 +1,67 @@ +import re + +class AciVersion(): + """ + ACI Version parser class. Parses the version string and provides methods to compare versions. + Supported version formats: + - APIC: `5.2(7f)`, `5.2.7f`, `5.2(7.123a)`, `5.2.7.123a`, `5.2(7.123)`, `5.2.7.123`, `aci-apic-dk9.5.2.7f.iso/bin` + - Switch: `15.2(7f)`, `15.2.7f`, `15.2(7.123a)`, `15.2.7.123a`, `15.2(7.123)`, `15.2.7.123`, `aci-n9000-dk9.15.2.7f.bin` + """ + v_regex = r'(?:dk9\.)?[1]?(?P<major1>\d)\.(?P<major2>\d)(?:\.|\()(?P<maint>\d+)(?P<QAdot>\.?)(?P<patch1>(?:[a-z]|\d+))(?P<patch2>[a-z]?)\)?' + + def __init__(self, version): + self.original = version + v = re.search(self.v_regex, version) + if not v: + raise ValueError("Parsing failure of ACI version `%s`" % version) + self.version = "{major1}.{major2}({maint}{QAdot}{patch1}{patch2})".format(**v.groupdict()) + self.dot_version = "{major1}.{major2}.{maint}{QAdot}{patch1}{patch2}".format(**v.groupdict()) + self.simple_version = "{major1}.{major2}({maint})".format(**v.groupdict()) + self.major_version = "{major1}.{major2}".format(**v.groupdict()) + self.major1 = v.group("major1") + self.major2 = v.group("major2") + self.maint = v.group("maint") + self.patch1 = v.group("patch1") + self.patch2 = v.group("patch2") + self.regex = v + + def __str__(self): + return self.version + + def older_than(self, version): + v2 = version if isinstance(version, AciVersion) else AciVersion(version) + for key in ["major1", "major2", "maint"]: + if int(self.regex.group(key)) > int(v2.regex.group(key)): return False + elif int(self.regex.group(key)) < int(v2.regex.group(key)): return True + # Patch1 can be alphabet or number + if self.patch1.isalpha() and v2.patch1.isdigit(): + return True # e.g., 5.2(7f) is older than 5.2(7.123) + elif self.patch1.isdigit() and v2.patch1.isalpha(): + return False + elif self.patch1.isalpha() and v2.patch1.isalpha(): + if self.patch1 > v2.patch1: return False + elif self.patch1 < v2.patch1: return True + elif self.patch1.isdigit() and v2.patch1.isdigit(): + if int(self.patch1) > int(v2.patch1): return False + elif int(self.patch1) < int(v2.patch1): return True + # Patch2 (alphabet) is optional. + if not self.patch2 and v2.patch2: + return True # one without Patch2 is older. + elif self.patch2 and not v2.patch2: + return False + elif self.patch2 and v2.patch2: + if self.patch2 > v2.patch2: return False + elif self.patch2 < v2.patch2: return True + return False + + def newer_than(self, version): + return not self.older_than(version) and not self.same_as(version) + + def same_as(self, version): + v2 = version if isinstance(version, AciVersion) else AciVersion(version) + return self.version == v2.version + +if __name__ == "__main__": + # Example usage + v1 = AciVersion("6.1(3.248)") + print(v1.simple_version) \ No newline at end of file diff --git a/tests/cimc_compatibilty_check/compatRsSuppHw_empty.json b/tests/cimc_compatibilty_check/compatRsSuppHw_empty.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/tests/cimc_compatibilty_check/compatRsSuppHw_empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/cimc_compatibilty_check/test_cimc_compatibilty_check.py b/tests/cimc_compatibilty_check/test_cimc_compatibilty_check.py index dfd452db..9ce8af6f 100644 --- a/tests/cimc_compatibilty_check/test_cimc_compatibilty_check.py +++ b/tests/cimc_compatibilty_check/test_cimc_compatibilty_check.py @@ -11,7 +11,7 @@ # icurl queries -eqptCh_api = 'eqptCh.json?query-target-filter=wcard(eqptCh.descr,"APIC")' +eqptCh_api = 'eqptCh.json?query-target-filter=wcard(eqptCh.descr,"APIC")' compatRsSuppHwL2_api = 'uni/fabric/compcat-default/ctlrfw-apic-6.0(5)/rssuppHw-[uni/fabric/compcat-default/ctlrhw-apicl2].json' compatRsSuppHwM1_api = 'uni/fabric/compcat-default/ctlrfw-apic-6.0(5)/rssuppHw-[uni/fabric/compcat-default/ctlrhw-apicm1].json' @@ -40,6 +40,14 @@ "6.0(5a)", script.PASS, ), + # Seen in QA testing where version + model does not have catalog entry + ( + {eqptCh_api: read_data(dir, "eqptCh_newver.json"), + compatRsSuppHwL2_api: read_data(dir, "compatRsSuppHw_605_L2.json"), + compatRsSuppHwM1_api: read_data(dir, "compatRsSuppHw_empty.json")}, + "6.0(5a)", + script.MANUAL, + ), ], ) def test_logic(mock_icurl, tversion, expected_result): From b50908314a0f30fdcf0aa7f7b208969c80d7431c Mon Sep 17 00:00:00 2001 From: Gabriel <gmonroy@cisco.com> Date: Tue, 22 Jul 2025 13:38:29 -0400 Subject: [PATCH 32/32] cleanup my testing --- aciVersion_test.py | 67 ---------------------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 aciVersion_test.py diff --git a/aciVersion_test.py b/aciVersion_test.py deleted file mode 100644 index 8cc1cb8e..00000000 --- a/aciVersion_test.py +++ /dev/null @@ -1,67 +0,0 @@ -import re - -class AciVersion(): - """ - ACI Version parser class. Parses the version string and provides methods to compare versions. - Supported version formats: - - APIC: `5.2(7f)`, `5.2.7f`, `5.2(7.123a)`, `5.2.7.123a`, `5.2(7.123)`, `5.2.7.123`, `aci-apic-dk9.5.2.7f.iso/bin` - - Switch: `15.2(7f)`, `15.2.7f`, `15.2(7.123a)`, `15.2.7.123a`, `15.2(7.123)`, `15.2.7.123`, `aci-n9000-dk9.15.2.7f.bin` - """ - v_regex = r'(?:dk9\.)?[1]?(?P<major1>\d)\.(?P<major2>\d)(?:\.|\()(?P<maint>\d+)(?P<QAdot>\.?)(?P<patch1>(?:[a-z]|\d+))(?P<patch2>[a-z]?)\)?' - - def __init__(self, version): - self.original = version - v = re.search(self.v_regex, version) - if not v: - raise ValueError("Parsing failure of ACI version `%s`" % version) - self.version = "{major1}.{major2}({maint}{QAdot}{patch1}{patch2})".format(**v.groupdict()) - self.dot_version = "{major1}.{major2}.{maint}{QAdot}{patch1}{patch2}".format(**v.groupdict()) - self.simple_version = "{major1}.{major2}({maint})".format(**v.groupdict()) - self.major_version = "{major1}.{major2}".format(**v.groupdict()) - self.major1 = v.group("major1") - self.major2 = v.group("major2") - self.maint = v.group("maint") - self.patch1 = v.group("patch1") - self.patch2 = v.group("patch2") - self.regex = v - - def __str__(self): - return self.version - - def older_than(self, version): - v2 = version if isinstance(version, AciVersion) else AciVersion(version) - for key in ["major1", "major2", "maint"]: - if int(self.regex.group(key)) > int(v2.regex.group(key)): return False - elif int(self.regex.group(key)) < int(v2.regex.group(key)): return True - # Patch1 can be alphabet or number - if self.patch1.isalpha() and v2.patch1.isdigit(): - return True # e.g., 5.2(7f) is older than 5.2(7.123) - elif self.patch1.isdigit() and v2.patch1.isalpha(): - return False - elif self.patch1.isalpha() and v2.patch1.isalpha(): - if self.patch1 > v2.patch1: return False - elif self.patch1 < v2.patch1: return True - elif self.patch1.isdigit() and v2.patch1.isdigit(): - if int(self.patch1) > int(v2.patch1): return False - elif int(self.patch1) < int(v2.patch1): return True - # Patch2 (alphabet) is optional. - if not self.patch2 and v2.patch2: - return True # one without Patch2 is older. - elif self.patch2 and not v2.patch2: - return False - elif self.patch2 and v2.patch2: - if self.patch2 > v2.patch2: return False - elif self.patch2 < v2.patch2: return True - return False - - def newer_than(self, version): - return not self.older_than(version) and not self.same_as(version) - - def same_as(self, version): - v2 = version if isinstance(version, AciVersion) else AciVersion(version) - return self.version == v2.version - -if __name__ == "__main__": - # Example usage - v1 = AciVersion("6.1(3.248)") - print(v1.simple_version) \ No newline at end of file