diff --git a/aci-preupgrade-validation-script.py b/aci-preupgrade-validation-script.py index 0c6de21a..63730bde 100644 --- a/aci-preupgrade-validation-script.py +++ b/aci-preupgrade-validation-script.py @@ -2041,9 +2041,9 @@ def apic_upgrade_mixed_version_check(cversion, tversion, fabric_nodes, **kwargs) if not tversion: return Result(result=MANUAL, msg=TVER_MISSING, doc_url=doc_url) if not cversion: - return Result(result=MANUAL, msg='Current APIC version not found.', doc_url=doc_url) + return Result(result=MANUAL, msg='Current APIC version unavailable.', doc_url=doc_url) if not tversion.newer_than(cversion): - return Result(result=NA, msg='No APIC upgrade is planned.', doc_url=doc_url) + return Result(result=NA, msg='No APIC upgrade planned.', doc_url=doc_url) apic_data = [] switch_data = [] @@ -2088,34 +2088,33 @@ def apic_upgrade_mixed_version_check(cversion, tversion, fabric_nodes, **kwargs) if apic_data: return Result( - result=FAIL_UF, msg='APICs are running different versions.', + result=FAIL_UF, headers=headers, data=sorted(apic_data), recommended_action='Complete or resolve the APIC cluster upgrade before starting another upgrade.', doc_url=doc_url) if switch_data: if newer_switch_found: - msg = 'A switch is newer than the APIC, which violates mixed-version conditions.' + assessment = 'A switch is newer than the APIC, which violates mixed-version conditions.' elif (int(tversion.major1), int(tversion.major2), int(tversion.maint)) < (6, 2, 1): - msg = 'APIC upgrade is unsupported while the fabric is in mixed-version mode.' + assessment = 'APIC upgrade is unsupported while the fabric is in mixed-version mode.' else: - msg = ('Enhanced Mixed Version Support may permit the current state, ' + assessment = ('Enhanced Mixed Version Support may permit the current state, ' 'but another APIC upgrade can introduce a third version.') return Result( - result=FAIL_UF, msg=msg, headers=headers, + result=FAIL_UF, headers=headers, data=sorted(switch_data + unknown_data), - recommended_action=('Align all active switches with the current APIC release ' + recommended_action=(assessment + ' Align all active switches with the current APIC release ' 'before upgrading the APIC cluster.'), doc_url=doc_url) if not active_switches or unknown_data: - msg = ('No active leaf or spine found.' if not active_switches else - 'One or more active switch versions could not be determined.') + msg = 'No active switches found.' if not active_switches else '' return Result(result=MANUAL, msg=msg, headers=headers, data=sorted(unknown_data), recommended_action='Verify active switch versions before upgrading the APIC cluster.', doc_url=doc_url) - return Result(result=PASS, msg='All active switches match the APIC release.', doc_url=doc_url) + return Result(result=PASS, msg='Switch versions match APIC.', doc_url=doc_url) @check_wrapper(check_title="Firmware/Maintenance Groups when crossing 4.0 Release") @@ -2350,11 +2349,23 @@ def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): ) doc_url = "https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#switch-node-bootflash-usage" + def missing_images(image_rows): + return Result( + result=MANUAL, + headers=["Image Role", "Architecture", "Filename", "Status"], + data=[row + ["Missing"] for row in image_rows], + recommended_action=( + "Make the listed images available in the Firmware Repository, " + "then rerun this check." + ), + doc_url=doc_url, + ) + if not tversion: return Result(result=MANUAL, msg=TVER_MISSING, doc_url=doc_url) if not sw_cversion: - return Result(result=MANUAL, msg="Current switch version not found. Check switch health.", doc_url=doc_url) + return Result(result=MANUAL, msg="Current switch version unavailable.", doc_url=doc_url) partitions_api = 'eqptcapacityFSPartition.json' partitions_api += '?query-target-filter=eq(eqptcapacityFSPartition.path,"/bootflash")' @@ -2365,7 +2376,7 @@ def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): partitions = icurl('class', partitions_api) if not partitions: - return Result(result=MANUAL, msg='/bootflash directory not found. Check switch health.', doc_url=doc_url) + return Result(result=MANUAL, msg='Bootflash inventory unavailable.', doc_url=doc_url) predownloaded_nodes = [] try: @@ -2404,8 +2415,7 @@ def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): if target_is_pre_split: # Only the 32-bit image is ever used for a pre-6.0(2a) target. if target_size_32 is None: - msg = 'Target switch image ({}) not found in Firmware Repository.'.format(switch_target_version) - return Result(result=MANUAL, msg=msg, doc_url=doc_url) + return missing_images([['Target', '32-bit', switch_target_version]]) required_space = 2 * target_size_32 downloaded_required_space = target_size_32 @@ -2413,14 +2423,14 @@ def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): # The larger image is used as a conservative estimate, so both sizes must be # known; a missing one can't be assumed to be the smaller (or zero-byte) one. if target_size_32 is None and target_size_64 is None: - msg = 'Target switch images ({}, {}) not found in Firmware Repository.'.format(switch_target_version, switch_target_version_64) - return Result(result=MANUAL, msg=msg, doc_url=doc_url) + return missing_images([ + ['Target', '32-bit', switch_target_version], + ['Target', '64-bit', switch_target_version_64], + ]) elif target_size_32 is None: - msg = '32-bit target switch image ({}) not found in Firmware Repository.'.format(switch_target_version) - return Result(result=MANUAL, msg=msg, doc_url=doc_url) + return missing_images([['Target', '32-bit', switch_target_version]]) elif target_size_64 is None: - msg = '64-bit target switch image ({}) not found in Firmware Repository.'.format(switch_target_version_64) - return Result(result=MANUAL, msg=msg, doc_url=doc_url) + return missing_images([['Target', '64-bit', switch_target_version_64]]) if current_is_pre_split: # Crossing the 32/64-bit boundary: the pre-6.0(2a) switch only ever had a @@ -2428,8 +2438,7 @@ def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): switch_current_version = "aci-n9000-dk9.1{}.bin".format(sw_cversion.dot_version) current_size = fw_sizes.get(switch_current_version) if current_size is None: - msg = 'Current switch image ({}) not found in Firmware Repository.'.format(switch_current_version) - return Result(result=MANUAL, msg=msg, doc_url=doc_url) + return missing_images([['Current', '32-bit', switch_current_version]]) if target_size_32 > current_size: required_space = 2 * (target_size_32 + target_size_64 - current_size) else: @@ -2459,7 +2468,7 @@ def switch_bootflash_usage_check(sw_cversion, tversion, **kwargs): if not data: result = PASS - msg = 'All nodes have sufficient bootflash space' + msg = 'Sufficient bootflash space.' return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) @@ -3089,7 +3098,7 @@ def apic_ssd_check(cversion, username, password, fabric_nodes, **kwargs): ): apics = [node for node in fabric_nodes if node["fabricNode"]["attributes"]["role"] == "controller"] if not apics: - return Result(result=ERROR, msg="No fabricNode of APIC. Is the cluster healthy?", doc_url=doc_url) + return Result(result=ERROR, msg="No APIC records found.", doc_url=doc_url) # `fabricNode` in pre-4.0 does not have `address` if not apics[0]["fabricNode"]["attributes"].get("address"): apic1 = [apic for apic in apics if apic["fabricNode"]["attributes"]["id"] == "1"][0] @@ -3252,7 +3261,7 @@ def overlapping_vlan_pools_check(**kwargs): infraSetPols = icurl('mo', 'uni/infra/settings.json') if infraSetPols[0]['infraSetPol']['attributes'].get('validateOverlappingVlans') in ['true', 'yes']: - return Result(result=PASS, msg="`Enforce EPG VLAN Validation` is enabled. No need to check overlapping VLANs") + return Result(result=PASS, msg="EPG VLAN validation enabled.") # Get VLAN pools and ports from access policy mo_classes = AciAccessPolicyParser.get_classes() @@ -3818,7 +3827,7 @@ def apic_version_md5_check(tversion, username, password, fabric_nodes, **kwargs) apics = [node for node in fabric_nodes if node["fabricNode"]["attributes"]["role"] == "controller"] if not apics: - return Result(result=ERROR, msg="No fabricNode of APIC. Is the cluster healthy?", doc_url=doc_url) + return Result(result=ERROR, msg="No APIC records found.", doc_url=doc_url) # `fabricNode` in pre-4.0 does not have `address` if not apics[0]["fabricNode"]["attributes"].get("address"): apic1 = [apic for apic in apics if apic["fabricNode"]["attributes"]["id"] == "1"][0] @@ -4602,7 +4611,7 @@ def telemetryStatsServerP_object_check(sw_cversion, tversion, **kwargs): doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#telemetrystatserverp-object' if not sw_cversion: - return Result(result=MANUAL, msg="Current switch version not found. Check switch health.") + return Result(result=MANUAL, msg="Current switch version unavailable.") if not tversion: return Result(result=MANUAL, msg=TVER_MISSING) @@ -4728,7 +4737,15 @@ def apic_ca_cert_validation(**kwargs): genrsa_proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) genrsa_proc.communicate()[0].strip() if genrsa_proc.returncode != 0: - return Result(result=ERROR, msg="openssl cmd issue, send logs to TAC.") + return Result( + result=ERROR, + headers=headers, + data=[['ERR']], + recommended_action=( + 'OpenSSL command failed (exit code {}).\n ' + 'Send the validation logs to Cisco TAC for investigation.' + ).format(genrsa_proc.returncode), + ) # Prep certreq with open(sign) as f: @@ -4772,12 +4789,12 @@ def fabricdomain_name_check(cversion, tversion, fabric_nodes, **kwargs): if tversion.same_as("6.0(2h)"): apic1 = [node for node in fabric_nodes if node["fabricNode"]["attributes"]["id"] == "1"] if not apic1: - return Result(result=ERROR, msg='No fabricNode of APIC 1. Is the cluster healthy?') + return Result(result=ERROR, msg='APIC 1 fabricNode not found.') # Using topSystem because fabricTopology.fabricDomain is not yet available prior to 5.2(6e). apic1_topsys = icurl("mo", "/".join([apic1[0]["fabricNode"]["attributes"]["dn"], "sys.json"])) if not apic1_topsys: - return Result(result=ERROR, msg='No topSystem of APIC 1. Is the cluster healthy?') + return Result(result=ERROR, msg='APIC 1 topSystem not found.') fabricDomain = apic1_topsys[0]['topSystem']['attributes']['fabricDomain'] if re.search(r'#|;', fabricDomain): data.append([fabricDomain, "Contains a special character"]) @@ -4815,7 +4832,7 @@ def sup_hwrev_check(cversion, tversion, **kwargs): sup_re = r'/.+(?Psupslot-\d+)' sups = icurl('class', 'eqptSpCmnBlk.json?&query-target-filter=wcard(eqptSpromSupBlk.dn,"sup")') if not sups: - return Result(result=MANUAL, msg='No sups found. This is unlikely. Check switch health.') + return Result(result=MANUAL, msg='No supervisor records returned.') for sup in sups: prtNum = sup['eqptSpCmnBlk']['attributes']['prtNum'] @@ -4939,7 +4956,7 @@ def mini_aci_6_0_2_check(cversion, tversion, fabric_nodes, **kwargs): apics = [node for node in fabric_nodes if node["fabricNode"]["attributes"]["role"] == "controller"] if not apics: - return Result(result=ERROR, msg="No fabricNode of APIC. Is the cluster healthy?", doc_url=doc_url) + return Result(result=ERROR, msg="No APIC records found.", doc_url=doc_url) for apic in apics: if apic['fabricNode']['attributes']['nodeType'] == "virtual": @@ -5070,8 +5087,10 @@ def post_upgrade_cb_check(cversion, tversion, **kwargs): "Impact": "Unexpected 64/32 bit image can deploy to switches", }, } - if not tversion or (tversion and cversion.older_than(str(tversion))): - return Result(result=POST, msg="Re-run script after APICs are upgraded and back to Fully-Fit") + if not tversion: + return Result(result=POST, msg=TVER_MISSING) + if cversion.older_than(str(tversion)): + return Result(result=POST, msg="Upgrade APICs first.") for new_mo in new_mo_dict: skip_current_mo = False @@ -5469,7 +5488,7 @@ def unsupported_fec_configuration_ex_check(sw_cversion, tversion, **kwargs): doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#unsupported-fec-configuration-for-n9k-c93180yc-ex' if not sw_cversion: - return Result(result=MANUAL, msg="Current switch version not found. Check switch health.") + return Result(result=MANUAL, msg="Current switch version unavailable.") if not tversion: return Result(result=MANUAL, msg=TVER_MISSING) @@ -5864,7 +5883,7 @@ def validate_32_64_bit_image_check(cversion, tversion, **kwargs): return Result(result=MANUAL, msg=TVER_MISSING) if cversion.older_than("6.0(2a)") and tversion.newer_than("6.0(2a)"): - return Result(result=POST, msg="Re-run after APICs are upgraded to 6.0(2) or later") + return Result(result=POST, msg="Upgrade APICs to 6.0(2)+ first.") if cversion.newer_than("6.0(2a)") and tversion.newer_than("6.0(2a)"): result_32 = result_64 = "Not Found" @@ -5891,7 +5910,7 @@ def validate_32_64_bit_image_check(cversion, tversion, **kwargs): result = FAIL_UF data.append([target_sw_ver, result_32, result_64]) else: - return Result(result=NA, msg="Target version below 6.0(2)") + return Result(result=NA, msg="Target version below 6.0(2).") return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) @@ -6092,7 +6111,6 @@ def count_mos(query): data.append(['Deprecated TEP paths', path_count, 'dbgAcPathA']) return Result( result=MANUAL, - msg='Atomic counters are no longer supported in APIC 6.1(2) and later. Cleanup is mandatory before upgrade.', headers=headers, data=data, recommended_action='Remove configured atomic counter policies before upgrading.', @@ -6118,7 +6136,7 @@ def count_mos(query): msg = '' if not tversion: - msg = 'Target version not supplied; atomic counter deprecation could not be assessed.' + msg = 'Tver not supplied, deprecation not assessed' if result in (PASS, NA): result = MANUAL @@ -6502,7 +6520,7 @@ def observer_db_size_check(username, password, fabric_nodes, **kwargs): apics = [node for node in fabric_nodes if node["fabricNode"]["attributes"]["role"] == "controller"] if not apics: - return Result(result=ERROR, msg="No fabricNode of APIC. Is the cluster healthy?", doc_url=doc_url) + return Result(result=ERROR, msg="No APIC records found.", doc_url=doc_url) # `fabricNode` in pre-4.0 does not have `address` if not apics[0]["fabricNode"]["attributes"].get("address"): apic1 = [apic for apic in apics if apic["fabricNode"]["attributes"]["id"] == "1"][0] @@ -7021,7 +7039,7 @@ def apic_database_size_check(cversion, **kwargs): try: collect_shard_stats_data = run_cmd(collect_stats_cmd, splitlines=False) except subprocess.CalledProcessError: - return Result(result=MANUAL, msg="acidiag command not available to current user") + return Result(result=MANUAL, msg="acidiag dbsize failed.") top_db_stats = json.loads(collect_shard_stats_data) for db_stats in top_db_stats['dbs']: @@ -7043,7 +7061,6 @@ def apic_database_size_check(cversion, **kwargs): if collection_errors: return Result( result=ERROR, - msg='Unable to collect APIC database object counters', headers=['APIC ID', 'DME', 'Collection Error'], data=collection_errors, unformatted_headers=headers, @@ -7305,7 +7322,6 @@ def tenant_dn(dn): if relationship_errors: return Result( result=ERROR, - msg="Unable to resolve context for one or more derived contract relationships", headers=["Derived Relationship", "Context Resolution Error"], data=relationship_errors, unformatted_headers=headers, @@ -7471,7 +7487,7 @@ def apic_storage_inode_check(**kwargs): faultInsts = icurl('class', 'faultInst.json?query-target-filter=or(eq(faultInst.code,"F4388"),eq(faultInst.code,"F4389"),eq(faultInst.code,"F4390"))') except OldVerPropNotFound: # Pre 5.2.6 does not have these fault codes. - return Result(result=NA, msg="cversion does not have fault code F4388, F4389 or F4390.", doc_url=doc_url) + return Result(result=NA, msg="Fault codes unavailable.", doc_url=doc_url) for faultInst in faultInsts: lc = faultInst['faultInst']['attributes']['lc'] if lc not in ["raised", "soaking"]: @@ -7653,7 +7669,16 @@ def svccore_excessive_data_check(**kwargs): return Result(result=result,headers=headers,data=data,recommended_action=recommended_action,doc_url=doc_url) except Exception as e: - return Result(result=ERROR, msg="Error occurred while fetching svccore object counts: {}".format(str(e)), doc_url=doc_url) + return Result( + result=ERROR, + headers=headers, + data=[['svccoreCtrlr/svccoreNode', 'ERR']], + recommended_action=( + 'Unable to collect svccore object counts: {}\n ' + 'Retry this check. Contact Cisco TAC if collection continues to fail.' + ).format(e), + doc_url=doc_url, + ) @check_wrapper(check_title='BGP Timer Policy Already Existing (F0467 bgpProt-policy-already-existing)') @@ -7667,7 +7692,7 @@ def bgpProto_timer_policy_already_existing_check(tversion, cversion, **kwargs): doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#bgpProto-timer-policy-already-existing' if not tversion: - return Result(result=MANUAL, msg=TVER_MISSING) + return Result(result=MANUAL, msg="Tver not supplied.") cversion_check = (cversion.newer_than("6.2(1g)") or ( cversion.major1 == "6" and cversion.major2 == "1" and cversion.newer_than("6.1(5e)")) and cversion.same_as(tversion)) @@ -7696,7 +7721,7 @@ def bgpProto_timer_policy_already_existing_check(tversion, cversion, **kwargs): if not data and not unformatted_data: result = PASS elif result == MANUAL: - return Result(result=result, msg="Clear the fault code F0467 for bgp timer policy", headers=headers, data=data, unformatted_headers=unformatted_headers, unformatted_data=unformatted_data, recommended_action=recommended_action, doc_url=doc_url) + return Result(result=result, headers=headers, data=data, unformatted_headers=unformatted_headers, unformatted_data=unformatted_data, recommended_action=recommended_action, doc_url=doc_url) return Result(result=result, headers=headers, data=data, unformatted_headers=unformatted_headers, unformatted_data=unformatted_data, recommended_action=recommended_action, doc_url=doc_url) @@ -7771,7 +7796,7 @@ def n9k_c93180yc_fx3_switch_memory_check(fabric_nodes, **kwargs): if not affected_nodes: result = NA - msg = 'No N9K-C93180YC-FX3 switches found. Skipping.' + msg = 'No applicable switches.' else: query = 'procMemUsage.json?query-target-filter=and(wcard(procMemUsage.dn,"memusage-sup"),lt(procMemUsage.Total,"{}"))'.format( min_memory_kb @@ -7800,13 +7825,6 @@ def n9k_c93180yc_fx3_switch_memory_check(fabric_nodes, **kwargs): memory_in_gb, ]) - if data: - msg = ( - 'N9K-C93180YC-FX3 requires a minimum of 32GB RAM for proper operation in ACI mode. ' - 'One or more switches with less than 32GB of memory may experience service instability. ' - 'Upgrade the switch memory to at least 32GB.' - ) - return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) @@ -7881,7 +7899,7 @@ def infravlan_overlap_access_policy_check(tversion, **kwargs): break if infra_vlan is None: - return Result(result=ERROR, msg="Unable to determine InfraVLAN from lldpInst.") + return Result(result=ERROR, msg="Cannot determine InfraVLAN.") encap_blocks = icurl('class', 'fvnsEncapBlk.json?query-target-filter=eq(fvnsEncapBlk.role,"external")') for obj in encap_blocks: @@ -7912,7 +7930,7 @@ def infravlan_overlap_access_policy_check(tversion, **kwargs): result = PASS if has_error: result = ERROR - msg = "Overlap check for InfraVLAN {} could not be determined because one or more VLAN pool blocks contain improper data or Error while fetching data.".format(infra_vlan) + msg = "VLAN pool data invalid." 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) @@ -7939,13 +7957,13 @@ def port_tracking_active_fabric_port_check(tversion, vpc_node_ids, **kwargs): return Result(result=NA, msg=VER_NOT_AFFECTED, doc_url=doc_url) if not vpc_node_ids: - return Result(result=NA, msg="No vPC nodes found. Not susceptible.", doc_url=doc_url) + return Result(result=NA, msg="No vPC nodes found.", doc_url=doc_url) port_tracking_api = 'uni/infra/trackEqptFabP-default.json' port_tracking_mo = icurl('mo', port_tracking_api) if not isinstance(port_tracking_mo, list) or len(port_tracking_mo) != 1: object_count = len(port_tracking_mo) if isinstance(port_tracking_mo, list) else 0 - msg = "Expected exactly one infraPortTrackPol object, but found {}.".format(object_count) + msg = "Expected one policy; found {}.".format(object_count) return Result(result=ERROR, msg=msg, recommended_action=recommended_action, doc_url=doc_url) try: @@ -7953,11 +7971,11 @@ def port_tracking_active_fabric_port_check(tversion, vpc_node_ids, **kwargs): admin_st = attributes['adminSt'] minimal_uplink = attributes['minlinks'] except (KeyError, TypeError): - msg = "The infraPortTrackPol response is missing required adminSt or minlinks attributes." + msg = "Missing adminSt or minlinks." return Result(result=ERROR, msg=msg, recommended_action=recommended_action, doc_url=doc_url) if admin_st not in ("on", "off") or not str(minimal_uplink).isdigit(): - msg = "The infraPortTrackPol response contains an invalid adminSt or minlinks value." + msg = "Invalid adminSt or minlinks." return Result(result=ERROR, msg=msg, recommended_action=recommended_action, doc_url=doc_url) result = PASS @@ -8261,7 +8279,7 @@ def fx3_breakout_port_check(cversion, tversion, fabric_nodes, **kwargs): or 'TC-FX3' in node['fabricNode']['attributes']['model'] } if not fx3_nodes: - return Result(result=NA, msg='No YC-FX3/TC-FX3 switches found. Skipping.', doc_url=doc_url) + return Result(result=NA, msg='No applicable switches.', doc_url=doc_url) brkout_ports_of_interest = {'49', '50', '51', '52'} # Capture the line-card slot too so the brkoutport-1 child interface @@ -8281,7 +8299,7 @@ def fx3_breakout_port_check(cversion, tversion, fabric_nodes, **kwargs): brkout_leg_per_port.setdefault(key, {"pod": m.group('pod'), "card": m.group('card')}) if not brkout_leg_per_port: - return Result(result=PASS, msg='No breakout configuration found on ports 49-52 of YC-FX3/TC-FX3 switches.', doc_url=doc_url) + return Result(result=PASS, msg='No applicable breakout ports.', doc_url=doc_url) fcot_api = 'ethpmFcot.json' fcot_api += '?query-target-filter=and(wcard(ethpmFcot.guiName,"CISCO-INNOLIGHT"),eq(ethpmFcot.guiCiscoEID,"QSFP-100G-SR4"))' @@ -8318,7 +8336,7 @@ def l1physif_dn(pod, node_id, intf): } if not first_leg_per_port: - return Result(result=PASS, msg='No affected breakout transceivers found on ports 49-52 of YC-FX3/TC-FX3 switches.', doc_url=doc_url) + return Result(result=PASS, msg='No affected transceivers.', doc_url=doc_url) # Only flag interfaces that are admin up with FEC not disabled; use the exact # instead of pulling every l1PhysIf in the fabric. APIC enforces a max of 20 filter expressions per query-target-filter @@ -8354,7 +8372,6 @@ def l1physif_dn(pod, node_id, intf): if data: result = FAIL_O - msg = 'Affected breakout transceivers with FEC not disabled found. This may cause an outage during the leaf upgrade.' return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) @@ -8520,13 +8537,22 @@ def get_apic_oob_connectivity(apic_id_ip, ports_by_node): apic_id_ip = icurl('class', 'topSystem.json?query-target-filter=eq(topSystem.role,"controller")') if not apic_id_ip: - return Result(result=ERROR, msg="APIC controller inventory could not be retrieved; cannot validate OOB connectivity.", headers=headers, recommended_action=recommended_action, doc_url=doc_url) + return Result(result=ERROR, msg="No APIC inventory returned.", headers=headers, recommended_action=recommended_action, doc_url=doc_url) try: ports_by_node = get_effective_ports(apic_id_ip) except ValueError as e: log.warning("Could not resolve effective HTTPS policy: %s", e) - return Result(result=ERROR, msg=str(e), headers=headers, recommended_action=recommended_action, doc_url=doc_url) + return Result( + result=ERROR, + headers=headers, + data=[['Executing APIC', '-', '-', '-', 'ERR', '-']], + recommended_action=( + '{}\n Verify the Management Access Policy and HTTPS port settings, ' + 'then rerun this check.' + ).format(e), + doc_url=doc_url, + ) data, has_error, has_failure, has_manual, manual_commands = get_apic_oob_connectivity(apic_id_ip, ports_by_node) @@ -8536,6 +8562,10 @@ def get_apic_oob_connectivity(apic_id_ip, ports_by_node): result = FAIL_UF elif has_manual: result = MANUAL + recommended_action = ( + 'Check OOB management connectivity between all APICs and verify that ' + 'the required HTTPS ports are reachable across the OOB network.' + ) if manual_commands: data.extend(manual_commands) @@ -8928,7 +8958,7 @@ def nxos_ipn_multicast_rpf_defect_check(fabric_nodes, **kwargs): spines[(match.group('pod'), match.group('node'))] = attr.get('name') or match.group('node') if not spines: - return Result(result=MANUAL, msg="No active spine nodes found; IPN exposure cannot be assessed.", + return Result(result=MANUAL, msg="No active spines found.", doc_url=doc_url) if len(set(pod for pod, node in spines)) < 2: return Result(result=NA, msg="Active spines are in one pod.", doc_url=doc_url) @@ -8954,7 +8984,7 @@ def nxos_ipn_multicast_rpf_defect_check(fabric_nodes, **kwargs): 'Cannot map OSPF interface to a physical LLDP/CDP port']) if not ospf_ports and not data: - return Result(result=MANUAL, msg="No spine overlay OSPF adjacency found; IPN exposure cannot be assessed.", + return Result(result=MANUAL, msg="No overlay OSPF adjacencies found.", recommended_action=recommended_action, doc_url=doc_url) neighbor_dn_regex = re.compile( @@ -9025,8 +9055,7 @@ def nxos_ipn_multicast_rpf_defect_check(fabric_nodes, **kwargs): 'LLDP and CDP advertise conflicting NX-OS versions']) result = MANUAL if data else PASS - msg = ("Possible NX-OS IPN exposure or incomplete neighbor data; verify on the IPN devices." if data else - "No listed affected NX-OS release advertised by the discovered IPN neighbors.") + msg = ("" if data else "No affected releases advertised.") return Result(result=result, msg=msg, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url) @@ -9042,11 +9071,22 @@ def vzany_svcgraph_stretched_vrf_check(cversion, tversion, **kwargs): recommended_action = 'Migrate vzAny service graph configuration to NDO before upgrade using brownfield import. See documentation for detailed migration steps.' doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#vzany-service-graph-on-stretched-vrf' + def query_error_result(assessment, error): + return Result( + result=ERROR, + headers=headers, + data=[['-', '-', '-', '-', 'ERR']], + recommended_action=( + '{}: {}\n Retry this check. Contact Cisco TAC if the query continues to fail.' + ).format(assessment, error), + doc_url=doc_url, + ) + if not tversion: return Result(result=MANUAL, msg=TVER_MISSING, doc_url=doc_url) if not tversion.newer_than("6.1(3g)"): - return Result(result=PASS, msg="Target version does not trigger CSCwn95571 validation", doc_url=doc_url) + return Result(result=PASS, msg="Version not affected.", doc_url=doc_url) has_error = False @@ -9061,10 +9101,10 @@ def vzany_svcgraph_stretched_vrf_check(cversion, tversion, **kwargs): try: graph_insts = icurl('class', graph_query) except Exception as e: - return Result(result=ERROR, msg='Error querying service graphs: {}'.format(str(e)), doc_url=doc_url) + return query_error_result('Error querying service graphs', e) if not graph_insts: - return Result(result=PASS, msg="No service graphs found", doc_url=doc_url) + return Result(result=PASS, msg="No service graphs found.", doc_url=doc_url) # Key by contract with a list of instances so multiple VRFs sharing one contract are all retained. sg_by_contract = defaultdict(list) @@ -9114,7 +9154,7 @@ def vzany_svcgraph_stretched_vrf_check(cversion, tversion, **kwargs): return Result(result=ERROR, headers=headers, data=data, unformatted_headers=unformatted_headers, unformatted_data=unformatted_data, recommended_action=recommended_action, doc_url=doc_url) - return Result(result=PASS, msg="No locally-managed service graphs with contracts found", doc_url=doc_url) + return Result(result=PASS, msg="No applicable service graphs.", doc_url=doc_url) stretched_vrf_dns = set() try: @@ -9129,14 +9169,14 @@ def vzany_svcgraph_stretched_vrf_check(cversion, tversion, **kwargs): stretched_vrf_dns.add(ctx_dn) break except Exception as e: - return Result(result=ERROR, msg='Error querying stretched VRFs: {}'.format(str(e)), doc_url=doc_url) + return query_error_result('Error querying stretched VRFs', e) if not stretched_vrf_dns: if has_error: return Result(result=ERROR, headers=headers, data=data, unformatted_headers=unformatted_headers, unformatted_data=unformatted_data, recommended_action=recommended_action, doc_url=doc_url) - return Result(result=PASS, msg="No stretched VRFs found", doc_url=doc_url) + return Result(result=PASS, msg="No stretched VRFs found.", doc_url=doc_url) # Key by (contract, VRF) so each scoped vzAny relationship is preserved. vzany_on_stretched = {} @@ -9170,14 +9210,14 @@ def vzany_svcgraph_stretched_vrf_check(cversion, tversion, **kwargs): return Result(result=ERROR, headers=headers, data=data, unformatted_headers=unformatted_headers, unformatted_data=unformatted_data, recommended_action=recommended_action, doc_url=doc_url) - return Result(result=PASS, msg="No vzAny service graph contracts on stretched VRFs", doc_url=doc_url) + return Result(result=PASS, msg="No applicable vzAny contracts.", doc_url=doc_url) # Use APIC's instantiated EPgDef DNs rather than inferring a node from terminal # objects. TermNodeInst has no consumer type, and ConnectionInst is its sibling. try: epg_defs = icurl('class', 'vnsEPgDef.json?query-target-filter=eq(vnsEPgDef.name,"consumer")') except Exception as e: - return Result(result=ERROR, msg='Error querying service graph EPg definitions: {}'.format(str(e)), doc_url=doc_url) + return query_error_result('Error querying service graph EPg definitions', e) consumer_epg_defs = defaultdict(list) for epg_def_mo in epg_defs: diff --git a/tests/checks/apic_database_size_check/test_apic_database_size_check.py b/tests/checks/apic_database_size_check/test_apic_database_size_check.py index d7f9801b..3ff5680e 100644 --- a/tests/checks/apic_database_size_check/test_apic_database_size_check.py +++ b/tests/checks/apic_database_size_check/test_apic_database_size_check.py @@ -386,7 +386,7 @@ def test_missing_mitmocounters_returns_error( result = run_check(cversion=script.AciVersion("6.0(8f)")) assert result.result == script.ERROR - assert result.msg == "Unable to collect APIC database object counters" + assert result.msg == "" assert result.headers == ["APIC ID", "DME", "Collection Error"] assert len(result.data) == 4 assert all(row[2] == expected_error for row in result.data) diff --git a/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py b/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py index 4ce4eab9..70e1b317 100644 --- a/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py +++ b/tests/checks/apic_oob_connectivity_check/test_apic_oob_connectivity_check.py @@ -19,7 +19,7 @@ podProfiles = 'fabricPodP.json?rsp-subtree=full' defaultHttps = 'uni/fabric/comm-default/https.json' -# headers returned only when the check reaches its final return (early MANUAL/NA/ERROR returns have no headers) +# Policy errors and final results share the existing connectivity columns. HEADERS = ["Source APIC", "Destination APIC", "OOB IP", "Port", "Status", "Command"] DEFAULT_POLICY_OUTPUTS = { @@ -236,7 +236,7 @@ def custom_policy_outputs(port="8443"): [], script.ERROR, HEADERS, - [], + [["-", "-", "-", "ERR"]], ), # IPv6 OOB local probes pass, with manual mesh commands required. ( @@ -312,6 +312,19 @@ def mock_subprocess_call(cmd, stderr=None): probe_rows = [row for row in result.data if row[4] != "Manual check required"] assert probe_rows == [["Executing APIC"] + row + ["-"] for row in expected_data] assert idx[0] == len(curl_exit_codes) + if expected_data == [["-", "-", "-", "ERR"]]: + assert result.msg == "" + assert "Verify the Management Access Policy and HTTPS port settings" in result.recommended_action + aci_result = script.AciResult(test_function, "APIC OOB Connectivity check", result) + assert aci_result.failureDetails["data"] == [dict(zip(HEADERS, probe_rows[0]))] + assert aci_result.recommended_action == result.recommended_action + output = [] + monkeypatch.setattr(script, "prints", output.append) + script.print_result(116, 117, "APIC OOB Connectivity check", **result.as_dict()) + status_line, table_and_action = output[0].split("\n", 1) + assert "ERR" in table_and_action + assert result.recommended_action in table_and_action + assert "curl --max-time" not in output[0] @pytest.mark.parametrize("inventory, ips, port", [ @@ -367,6 +380,11 @@ def probe(*args, **kwargs): assert result.headers == HEADERS assert result.data == expected_probes + expected_commands assert "Run each command on its Source APIC" in result.recommended_action + if expected_result == script.MANUAL: + assert result.recommended_action.startswith("Check OOB management connectivity") + assert "Restore" not in result.recommended_action + elif expected_result == script.FAIL_UF: + assert result.recommended_action.startswith("Restore OOB management connectivity") aci_result = script.AciResult(test_function, "APIC OOB Connectivity check", result) assert aci_result.failureDetails["header"] == HEADERS diff --git a/tests/checks/apic_upgrade_mixed_version_check/test_apic_upgrade_mixed_version_check.py b/tests/checks/apic_upgrade_mixed_version_check/test_apic_upgrade_mixed_version_check.py index b9d965fa..6acd158e 100644 --- a/tests/checks/apic_upgrade_mixed_version_check/test_apic_upgrade_mixed_version_check.py +++ b/tests/checks/apic_upgrade_mixed_version_check/test_apic_upgrade_mixed_version_check.py @@ -61,7 +61,8 @@ def test_mismatch_fails_at_every_target(run_check, target, message): fabric_node(201, 'spine', 'n9000-16.1(4h)')] result = run_mixed_check(run_check, nodes, target=target) assert result.result == script.FAIL_UF - assert message in result.msg + assert result.msg == "" + assert message in result.recommended_action assert result.data == [ ['1', '101', 'leaf101', 'leaf', 'n9000-15.2(8f)', '6.1(4h)', 'Switch differs from APIC']] @@ -77,7 +78,8 @@ def test_every_mismatched_node_is_reported(run_check): assert result.result == script.FAIL_UF assert [row[1] for row in result.data] == ['101', '102', '202'] assert result.data[1][6] == 'Switch newer than APIC' - assert 'newer than the APIC' in result.msg + assert result.msg == '' + assert 'newer than the APIC' in result.recommended_action def test_apic_versions_must_agree(run_check): @@ -86,7 +88,8 @@ def test_apic_versions_must_agree(run_check): fabric_node(101, 'leaf', 'n9000-16.1(4h)')] result = run_mixed_check(run_check, nodes) assert result.result == script.FAIL_UF - assert 'APICs' in result.msg + assert result.msg == '' + assert 'APIC cluster upgrade' in result.recommended_action assert result.data[0][1] == '1' diff --git a/tests/checks/atomic_counter_check/test_atomic_counter_check.py b/tests/checks/atomic_counter_check/test_atomic_counter_check.py index d7d715dd..cde83776 100644 --- a/tests/checks/atomic_counter_check/test_atomic_counter_check.py +++ b/tests/checks/atomic_counter_check/test_atomic_counter_check.py @@ -32,6 +32,7 @@ def count_response(count): ('6.1(1a)', None, None, 0, 1, script.MANUAL, ['Configuration rollback review']), ('6.1(1a)', None, None, 1601, 1, script.FAIL_UF, ['TEP-to-TEP scalability', 'Configuration rollback review']), (None, None, None, 0, 0, script.MANUAL, []), + (None, None, None, 1601, 1, script.FAIL_UF, ['TEP-to-TEP scalability', 'Configuration rollback review']), ], ) def test_atomic_counter_order_and_results( @@ -51,6 +52,7 @@ def test_atomic_counter_order_and_results( assert result.result == expected_result assert [row[0] for row in result.data] == expected_concerns if expected_concerns and expected_concerns[0].startswith('Deprecated'): - assert 'Cleanup is mandatory' in result.msg + assert result.msg == '' + assert result.recommended_action == 'Remove configured atomic counter policies before upgrading.' if tversion is None: - assert 'deprecation could not be assessed' in result.msg + assert result.msg == 'Tver not supplied, deprecation not assessed' diff --git a/tests/checks/bgp_timer_policy_already_existing_check/test_bgp_timer_policy_already_existing_check.py b/tests/checks/bgp_timer_policy_already_existing_check/test_bgp_timer_policy_already_existing_check.py index ee6997f8..978d4cc3 100644 --- a/tests/checks/bgp_timer_policy_already_existing_check/test_bgp_timer_policy_already_existing_check.py +++ b/tests/checks/bgp_timer_policy_already_existing_check/test_bgp_timer_policy_already_existing_check.py @@ -34,7 +34,7 @@ "configQual:bgpProt-policy-already-existing, configSt:failed-to-apply, temporaryError:no", ], ], - "Clear the fault code F0467 for bgp timer policy", + "", ), # target release beyond affected range on 6.1 train with existing fault: manual clearance required ( @@ -56,7 +56,7 @@ "configQual:bgpProt-policy-already-existing, configSt:failed-to-apply, temporaryError:no", ], ], - "Clear the fault code F0467 for bgp timer policy", + "", ), # boundary version is still affected for strict newer_than check ( @@ -144,7 +144,7 @@ "configQual:bgpProt-policy-already-existing, configSt:failed-to-apply, temporaryError:no", ], ], - "Clear the fault code F0467 for bgp timer policy", + "", ), ( {faultDelegates: read_data(dir, "faultDelegate_NEG.json")}, @@ -170,4 +170,4 @@ def test_logic(run_check, mock_icurl, tversion, cversion, expected_result, expec assert result.result == expected_result assert result.data == expected_data if expected_msg is not None: - assert result.msg == expected_msg \ No newline at end of file + assert result.msg == expected_msg diff --git a/tests/checks/fx3_breakout_port_check/test_fx3_breakout_port_check.py b/tests/checks/fx3_breakout_port_check/test_fx3_breakout_port_check.py index af1afaab..95734cab 100644 --- a/tests/checks/fx3_breakout_port_check/test_fx3_breakout_port_check.py +++ b/tests/checks/fx3_breakout_port_check/test_fx3_breakout_port_check.py @@ -25,7 +25,7 @@ # ethpmFcot/l1PhysIf fixtures control which of these ports actually get flagged. ALL_BRKOUT_PORTS = read_data(dir, "eqptBrkoutP_all_breakout_ports.json") -FAIL_MSG = "Affected breakout transceivers with FEC not disabled found. This may cause an outage during the leaf upgrade." +FAIL_MSG = "" def l1physif_query(legs): @@ -126,26 +126,26 @@ def l1physif_entries(batch): "5.2(8g)", "6.2(1g)", FX3_NODES, {BRKOUT_QUERY: read_data(dir, "eqptBrkoutP_none.json")}, - script.PASS, "No breakout configuration found on ports 49-52 of YC-FX3/TC-FX3 switches.", [], + script.PASS, "No applicable breakout ports.", [], ), # Affected versions, no YC-FX3/TC-FX3 switches in the fabric ( "5.2(8g)", "5.3(2a)", NON_FX3_NODES, {}, - script.NA, "No YC-FX3/TC-FX3 switches found. Skipping.", [], + script.NA, "No applicable switches.", [], ), # Affected versions, FX3 switches present, but no breakout config on ports 49-52 ( "5.2(8g)", "5.3(2a)", FX3_NODES, {BRKOUT_QUERY: read_data(dir, "eqptBrkoutP_none.json")}, - script.PASS, "No breakout configuration found on ports 49-52 of YC-FX3/TC-FX3 switches.", [], + script.PASS, "No applicable breakout ports.", [], ), # Breakout configured, but not on ports 49-52 ( "5.2(8g)", "5.3(2a)", FX3_NODES, {BRKOUT_QUERY: read_data(dir, "eqptBrkoutP_port_not_in_range.json")}, - script.PASS, "No breakout configuration found on ports 49-52 of YC-FX3/TC-FX3 switches.", [], + script.PASS, "No applicable breakout ports.", [], ), # Breakout on port 49, but no matching Innolight SR4 transceiver found ( @@ -155,7 +155,7 @@ def l1physif_entries(batch): BRKOUT_QUERY: ALL_BRKOUT_PORTS, FCOT_QUERY: read_data(dir, "ethpmFcot_none.json"), }, - script.PASS, "No affected breakout transceivers found on ports 49-52 of YC-FX3/TC-FX3 switches.", [], + script.PASS, "No affected transceivers.", [], ), # Matching transceiver on brkoutport-1, but FEC is already disabled ( diff --git a/tests/checks/n9k_c93180yc_fx3_switch_memory_check/test_n9k_c93180yc_fx3_switch_memory_check.py b/tests/checks/n9k_c93180yc_fx3_switch_memory_check/test_n9k_c93180yc_fx3_switch_memory_check.py index 6458b218..0ff61fc2 100644 --- a/tests/checks/n9k_c93180yc_fx3_switch_memory_check/test_n9k_c93180yc_fx3_switch_memory_check.py +++ b/tests/checks/n9k_c93180yc_fx3_switch_memory_check/test_n9k_c93180yc_fx3_switch_memory_check.py @@ -40,7 +40,7 @@ def make_fx3_nodes(count): [], {}, script.NA, - 'No N9K-C93180YC-FX3 switches found. Skipping.', + 'No applicable switches.', [], ), # Non-N9K-C93180YC-FX3 node (N9K-C9508) @@ -48,7 +48,7 @@ def make_fx3_nodes(count): read_data(dir, "fabricNode_N9K-C9508.json"), {}, script.NA, - 'No N9K-C93180YC-FX3 switches found. Skipping.', + 'No applicable switches.', [], ), # N9K-C93180YC-FX3 node with >=32GB memory - API returns empty (filtered by lt) @@ -98,11 +98,7 @@ def make_fx3_nodes(count): proc_mem_query: read_data(dir, "procMemUsage_lt32gb.json"), }, script.FAIL_O, - ( - 'N9K-C93180YC-FX3 requires a minimum of 32GB RAM for proper operation in ACI mode. ' - 'One or more switches with less than 32GB of memory may experience service instability. ' - 'Upgrade the switch memory to at least 32GB.' - ), + '', [["101", "leaf101", "N9K-C93180YC-FX3", "16.0"]], ), ], @@ -116,4 +112,4 @@ def test_logic(run_check, mock_icurl, fabric_nodes, expected_result, expected_ms if result.data: assert result.data == expected_data else: - assert result.unformatted_data == expected_data \ No newline at end of file + assert result.unformatted_data == expected_data diff --git a/tests/checks/nxos_ipn_multicast_rpf_defect_check/test_nxos_ipn_multicast_rpf_defect_check.py b/tests/checks/nxos_ipn_multicast_rpf_defect_check/test_nxos_ipn_multicast_rpf_defect_check.py index cb177cf4..51b1ab47 100644 --- a/tests/checks/nxos_ipn_multicast_rpf_defect_check/test_nxos_ipn_multicast_rpf_defect_check.py +++ b/tests/checks/nxos_ipn_multicast_rpf_defect_check/test_nxos_ipn_multicast_rpf_defect_check.py @@ -160,7 +160,7 @@ def test_no_ospf_adj_is_inconclusive(run_check, mock_icurl, fabric_nodes, icurl_ result = run_check(fabric_nodes=fabric_nodes) assert result.result == script.MANUAL - assert "No spine overlay OSPF adjacency" in result.msg + assert result.msg == "No overlay OSPF adjacencies found." def test_single_pod_is_not_applicable(run_check, mock_icurl, icurl_outputs): diff --git a/tests/checks/pg_and_shared_svc_contract_check/test_pg_and_shared_svc_contract_check.py b/tests/checks/pg_and_shared_svc_contract_check/test_pg_and_shared_svc_contract_check.py index ae93129b..ea366381 100644 --- a/tests/checks/pg_and_shared_svc_contract_check/test_pg_and_shared_svc_contract_check.py +++ b/tests/checks/pg_and_shared_svc_contract_check/test_pg_and_shared_svc_contract_check.py @@ -687,9 +687,7 @@ def test_missing_provider_context_is_an_error(run_check, mock_icurl): ) assert result.result == script.ERROR - assert result.msg == ( - "Unable to resolve context for one or more derived contract relationships" - ) + assert result.msg == "" assert result.data == [[ ( "cdef-[uni/tn-common/brc-AD_C]/" @@ -726,9 +724,7 @@ def test_missing_consumer_context_is_an_error(run_check, mock_icurl): ) assert result.result == script.ERROR - assert result.msg == ( - "Unable to resolve context for one or more derived contract relationships" - ) + assert result.msg == "" assert result.data == [[ ( "cdef-[uni/tn-common/brc-AD_C]/" diff --git a/tests/checks/test_result_details.py b/tests/checks/test_result_details.py new file mode 100644 index 00000000..828a8c3a --- /dev/null +++ b/tests/checks/test_result_details.py @@ -0,0 +1,138 @@ +"""Check result data and recommended actions in console and JSON output.""" +import importlib +import json +import os + +import pytest + + +script = importlib.import_module('aci-preupgrade-validation-script') + + +def run_check(name, **kwargs): + return getattr(script, name)(finalize_check=lambda *_: None, **kwargs) + + +def assert_outputs(monkeypatch, name, result, detail): + titles = [] + getattr(script, name)(initialize_check=lambda _, title: titles.append(title)) + output = [] + monkeypatch.setattr(script, 'prints', output.append) + script.print_result(117, 117, titles[0], **result.as_dict()) + status_line, table_and_action = output[0].split('\n', 1) + assert status_line.endswith(' ' + result.result) + assert detail not in status_line + assert detail in table_and_action + assert result.recommended_action in table_and_action + + payload = json.loads(json.dumps(script.AciResult(name, titles[0], result).as_dict())) + assert payload['recommended_action'] == result.recommended_action + assert payload['failureDetails']['header'] == result.headers + assert payload['failureDetails']['data'] == [dict(zip(result.headers, row)) for row in result.data] + + +@pytest.mark.parametrize('failing_class', ['svccoreCtrlr', 'svccoreNode']) +def test_svccore_collection_error_uses_existing_table(monkeypatch, failing_class): + detail = 'APIC query timed out while collecting core counts' + + def icurl(_, query): + if query.startswith(failing_class + '.'): + raise RuntimeError(detail) + return [{'moCount': {'attributes': {'count': '0'}}}] + + monkeypatch.setattr(script, 'icurl', icurl) + name = 'svccore_excessive_data_check' + result = run_check(name) + assert result.result == script.ERROR + assert result.msg == '' + assert result.headers == ['Class Name', 'Count'] + assert result.data == [['svccoreCtrlr/svccoreNode', 'ERR']] + assert detail in result.recommended_action + assert_outputs(monkeypatch, name, result, detail) + + +@pytest.mark.parametrize('failing_class', ['vnsGraphInst', 'fvCtx', 'vnsEPgDef']) +def test_service_graph_query_errors_keep_details_in_action(monkeypatch, failing_class): + fixture_dir = os.path.join(os.path.dirname(__file__), 'vzany_svcgraph_stretched_vrf_check') + fixtures = { + 'vnsGraphInst': 'vnsGraphInst_with_consumer.json', + 'fvCtx': 'fvCtx_stretched_vrf.json', + 'vzRsAnyToCons': 'vzRsAnyToCons_consumer.json', + } + detail = 'API collection failed: ' + 'long diagnostic text ' * 12 + + def icurl(_, query): + mo_class = query.split('.')[0] + if mo_class == failing_class: + raise RuntimeError(detail) + if mo_class == 'vzRsAnyToProv': + return [] + with open(os.path.join(fixture_dir, fixtures[mo_class])) as fixture_file: + return json.load(fixture_file) + + monkeypatch.setattr(script, 'icurl', icurl) + name = 'vzany_svcgraph_stretched_vrf_check' + result = run_check(name, cversion=script.AciVersion('6.0(1a)'), tversion=script.AciVersion('6.1(4a)')) + assert result.result == script.ERROR + assert result.msg == '' + assert result.headers == ['Tenant', 'VRF', 'Contract', 'Graph', 'Issue'] + assert result.data == [['-', '-', '-', '-', 'ERR']] + assert detail in result.recommended_action + assert_outputs(monkeypatch, name, result, detail) + + +def test_openssl_failure_uses_certificate_response_column(monkeypatch, tmpdir): + monkeypatch.chdir(tmpdir) + monkeypatch.setattr(script, 'icurl', lambda *_: [ + {'pkiFabricSelfCAEp': {'attributes': {'currCertReqPassphrase': 'test-passphrase'}}} + ]) + class FailedProcess(object): + returncode = 7 + + def communicate(self): + return b'error', None + + monkeypatch.setattr(script.subprocess, 'Popen', lambda *args, **kwargs: FailedProcess()) + name = 'apic_ca_cert_validation' + result = run_check(name) + assert result.result == script.ERROR + assert result.msg == '' + assert result.headers == ['Certreq Response'] + assert result.data == [['ERR']] + assert str(FailedProcess.returncode) in result.recommended_action + assert_outputs(monkeypatch, name, result, 'OpenSSL command failed (exit code 7).') + + +IMAGE_32 = 'aci-n9000-dk9.16.0.2h.bin' +IMAGE_64 = 'aci-n9000-dk9.16.0.2h-cs_64.bin' +OLD_IMAGE = 'aci-n9000-dk9.15.2.8h.bin' + + +@pytest.mark.parametrize('current, target, available, missing', [ + ('6.0(3a)', '5.2(8h)', [], [['Target', '32-bit', OLD_IMAGE, 'Missing']]), + ('6.0(3a)', '6.0(2h)', [], [ + ['Target', '32-bit', IMAGE_32, 'Missing'], ['Target', '64-bit', IMAGE_64, 'Missing']]), + ('6.0(3a)', '6.0(2h)', [IMAGE_32], [['Target', '64-bit', IMAGE_64, 'Missing']]), + ('6.0(3a)', '6.0(2h)', [IMAGE_64], [['Target', '32-bit', IMAGE_32, 'Missing']]), + ('5.2(8h)', '6.0(2h)', [IMAGE_32, IMAGE_64], [['Current', '32-bit', OLD_IMAGE, 'Missing']]), +]) +def test_missing_images_are_results_with_repository_action(monkeypatch, current, target, available, missing): + def icurl(_, query): + if query.startswith('eqptcapacityFSPartition.'): + return [{'eqptcapacityFSPartition': {'attributes': { + 'dn': 'topology/pod-1/node-101/sys/eqptcapacity/fspartition-bootflash', 'avail': '1'}}}] + if query.startswith('maintUpgJob.'): + return [] + assert query.startswith('firmwareFirmware.') + return [{'firmwareFirmware': {'attributes': {'isoname': name, 'size': '2000000000'}}} + for name in available] + + monkeypatch.setattr(script, 'icurl', icurl) + name = 'switch_bootflash_usage_check' + result = run_check(name, sw_cversion=script.AciVersion(current), tversion=script.AciVersion(target)) + assert result.result == script.MANUAL + assert result.msg == '' + assert result.headers == ['Image Role', 'Architecture', 'Filename', 'Status'] + assert result.data == missing + assert result.recommended_action + assert_outputs(monkeypatch, name, result, missing[0][2])