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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
preupgrade_validator*.tgz

# Translations
*.mo
Expand Down
106 changes: 98 additions & 8 deletions aci-preupgrade-validation-script.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
import os
import re

SCRIPT_VERSION = "v4.0.1"
SCRIPT_VERSION = "v4.1.0-dev"
DEFAULT_TIMEOUT = 600 # sec
# result constants
DONE = 'DONE'
Expand Down Expand Up @@ -1023,13 +1023,17 @@ def __init__(
common_kwargs,
monitor_interval=0.5, # sec
monitor_timeout=600, # sec
max_threads=None,
callback_on_monitoring=None,
callback_on_start_failure=None,
callback_on_timeout=None,
):
self.funcs = funcs
self.threads = None
self.common_kwargs = common_kwargs
# Semaphore to cap the number of concurrently running check threads.
# None means unlimited.
self.semaphore = threading.Semaphore(max_threads) if max_threads and max_threads > 0 else None

# Not using `thread.join(timeout)` because it waits for each thread sequentially,
# which means the program may wait for "timeout * num of threads" at worst case.
Expand All @@ -1053,7 +1057,7 @@ def start(self):
raise RuntimeError("Threading on going. Cannot start again.")

self.threads = [
self._generate_thread(target=func, kwargs=self.common_kwargs)
self._generate_thread(target=func, kwargs=self.common_kwargs, use_semaphore=True)
for func in self.funcs
]

Expand All @@ -1080,9 +1084,19 @@ def join(self):
def is_timeout(self):
return self.timeout_event.is_set()

def _generate_thread(self, target, args=(), kwargs=None):
def _generate_thread(self, target, args=(), kwargs=None, use_semaphore=False):
if kwargs is None:
kwargs = {}
if use_semaphore and self.semaphore is not None:
semaphore = self.semaphore
original_target = target
def _wrapped_target(*a, **kw):
try:
original_target(*a, **kw)
finally:
semaphore.release()
_wrapped_target.__name__ = target.__name__
target = _wrapped_target
thread = CustomThread(
target=target, name=target.__name__, args=args, kwargs=kwargs
)
Expand All @@ -1102,11 +1116,16 @@ def _start_thread(self, thread):
thread_started = False
while not self.is_timeout():
try:
if self.semaphore is not None:
log.info("({}) Waiting for an available thread slot.".format(thread.name))
self.semaphore.acquire()
log.info("({}) Starting thread.".format(thread.name))
thread.start()
thread_started = True
break
except RuntimeError as e:
if self.semaphore is not None:
self.semaphore.release()
if str(e) != "can't start new thread":
log.error("({}) Unexpected error to start a thread.".format(thread.name), exc_info=True)
break
Expand All @@ -1121,6 +1140,8 @@ def _start_thread(self, thread):
time_elapsed += queue_interval
continue
except Exception:
if self.semaphore is not None:
self.semaphore.release()
log.error("({}) Unexpected error to start a thread.".format(thread.name), exc_info=True)
break

Expand Down Expand Up @@ -1493,11 +1514,14 @@ def get_row(widths, values, spad=" ", lpad=""):

def prints(objects, sep=' ', end='\n'):
with open(RESULT_FILE, 'a') as f:
print(objects, sep=sep, end=end, file=sys.stdout)
try:
print(objects, sep=sep, end=end, file=sys.stdout)
sys.stdout.flush()
except OSError:
pass
if end == "\r":
end = "\n" # easier to read with \n in a log file
print(objects, sep=sep, end=end, file=f)
sys.stdout.flush()
f.flush()


Expand Down Expand Up @@ -4748,7 +4772,13 @@ def fabricPathEp_target_check(**kwargs):
elif int(third) > 16:
data.append([dn, "eth port {} is invalid (1-16 expected) for breakout ports".format(third)])
else:
data.append([dn, "PathEp 'eth' syntax is invalid"])
# CHECK eth1//0 malform scenario (double slashes)
if "//" in path:
data.append([dn, "PathEp 'eth' syntax is invalid"])
# CHECK Ethx/y malform scenario (should not be caps)
elif path.startswith("Eth"):
data.append([dn, "PathEp 'eth' should be lowercase 'eth'"])

else:
data.append([dn, "target is not a valid fabricPathEp DN"])

Expand Down Expand Up @@ -6026,6 +6056,62 @@ def apic_downgrade_compat_warning_check(cversion, tversion, **kwargs):
return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url)


@check_wrapper(check_title='Shared Services Providers with Preferred Group enabled')
def pg_and_shared_svc_contract_check(cversion, tversion, **kwargs):
result= PASS
headers = ["Shared Service Contract", "Provider in Preferred Group", "PcTag"]
data = []
recommended_action = 'an EPG in a Contract Preferred Group can consume a shared service contract, but cannot be a provider for a shared service contract.'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] This recommendation restates the restriction but does not tell an operator how to make the upgrade safe. Please provide an actionable remediation, such as removing the EPG from the preferred group or stopping it from providing the shared-service contract, and verify the recommendation in the tests.

doc_url = 'https://datacenter.github.io/ACI-Pre-Upgrade-Validation-Script/validations/#preferred_group_shared_service_provider'

if not tversion:
return Result(result=MANUAL, msg=TVER_MISSING)
# Configuration becomes faulted after 4.2-6d and 5.1-1h
if tversion.older_than("4.2(6d)"):
return Result(result=NA)
elif tversion.older_than("5.1(1h)"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Gate these thresholds by release train. With the current sequential comparisons, every 4.2 target from 4.2(6d) onward passes the first check but is still older than 5.1(1h), so this returns N/A for an affected upgrade. I reproduced that behavior with target 4.2(6d). Please model the 4.2(6d)+ and 5.1(1h)+ branches explicitly and add boundary tests for affected 4.2 releases.

return Result(result=NA)
shrd_contracts_api = 'vzBrCP.json'
shrd_contracts_api += '?query-target-filter=and(eq(vzBrCP.scope,"global"))'
shrd_contracts = icurl('class', shrd_contracts_api)
if not shrd_contracts:
return Result(result=NA)
list_of_shrd_contracts =[]
for shrd_contract in shrd_contracts:
list_of_shrd_contracts.append(shrd_contract["vzBrCP"]["attributes"]["dn"])
# Configuration only becomes faulted for extEPGs after 6.0(1g), normal epgs permitted.
if tversion.older_than("6.0(1g)"):
glbl_epgs_api = 'fvAEPg.json'
glbl_epgs_api += '?query-target-filter=and(le(fvAEPg.pcTag,"16385"),ge(fvAEPg.pcTag,"16"),eq(fvAEPg.prefGrMemb,"include"))'
glbl_epgs_api += '&rsp-subtree=children&rsp-subtree-class=fvRsProv'
glbl_epgs = icurl('class', glbl_epgs_api)
if glbl_epgs:
for glbl_epg in glbl_epgs:
for prov_contract in glbl_epg["fvAEPg"]["children"]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Treat children as optional here and in the l3extInstP loop below. APIC omits the key when a preferred-group object has no provider relations; direct indexing then raises KeyError, converts this check to ERROR, and can prevent later affected objects from being evaluated. I reproduced ERROR !! Unexpected Error: children with a valid childless EPG response. Iterating over .get("children", []) or [] avoids that failure, and a mixed childless/affected fixture should cover it.

if prov_contract["fvRsProv"]["attributes"]["tDn"] in list_of_shrd_contracts:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Correlating only the provider with a global-scope contract is broader than the documented defect condition. CSCvm63145 and the new documentation require an L3Out EPG consumer in another VRF, but this condition flags any preferred-group provider of a global contract, including contracts without an L3Out consumer. Please query/correlate the consumer relation before reporting FAIL_O and add PASS cases for no consumer or a non-L3Out consumer.

contract = prov_contract["fvRsProv"]["attributes"]["tDn"]
pctag = glbl_epg["fvAEPg"]["attributes"]["pcTag"]
provider = glbl_epg["fvAEPg"]["attributes"]["dn"]
data.append([contract, provider, pctag])
# Regardless of version, check extEPGs
glbl_ext_epgs_api = 'l3extInstP.json'
glbl_ext_epgs_api += '?query-target-filter=and(le(l3extInstP.pcTag,"16385"),ge(l3extInstP.pcTag,"16"),eq(l3extInstP.prefGrMemb,"include"))'
glbl_ext_epgs_api += '&rsp-subtree=children&rsp-subtree-class=fvRsProv'
glbl_ext_epgs = icurl('class', glbl_ext_epgs_api)
if glbl_ext_epgs:
for glbl_ext_epg in glbl_ext_epgs:
for prov_ext_contract in glbl_ext_epg["l3extInstP"]["children"]:
if prov_ext_contract["fvRsProv"]["attributes"]["tDn"] in list_of_shrd_contracts:
contract = prov_ext_contract["fvRsProv"]["attributes"]["tDn"]
pctag = glbl_ext_epg["l3extInstP"]["attributes"]["pcTag"]
provider = glbl_ext_epg["l3extInstP"]["attributes"]["dn"]
data.append([contract, provider, pctag])

if data:
result = FAIL_O

return Result(result=result, headers=headers, data=data, recommended_action=recommended_action, doc_url=doc_url)

# ---- Script Execution ----


Expand All @@ -6039,6 +6125,7 @@ def parse_args(args):
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.")
parser.add_argument("--timeout", action="store", nargs="?", type=int, const=-1, default=DEFAULT_TIMEOUT, help="Show default script timeout (sec) or overwrite it when a number is provided (e.g. --timeout 1200).")
parser.add_argument("--max-threads", action="store", type=int, default=None, help="Maximum number of check threads to run concurrently. Defaults to unlimited.")
parsed_args = parser.parse_args(args)
return parsed_args

Expand Down Expand Up @@ -6161,6 +6248,7 @@ class CheckManager:
service_bd_forceful_routing_check,
ave_eol_check,
consumer_vzany_shared_services_check,
pg_and_shared_svc_contract_check,

# Bugs
ep_announce_check,
Expand Down Expand Up @@ -6209,11 +6297,12 @@ class CheckManager:
apic_ca_cert_validation,
]

def __init__(self, api_only=False, debug_function="", timeout=600, monitor_interval=0.5):
def __init__(self, api_only=False, debug_function="", timeout=600, monitor_interval=0.5, max_threads=None):
self.api_only = api_only
self.debug_function = debug_function
self.monitor_interval = monitor_interval # sec
self.monitor_timeout = timeout # sec
self.max_threads = max_threads
self.timeout_event = None

self.check_funcs = self.get_check_funcs()
Expand Down Expand Up @@ -6284,6 +6373,7 @@ def run_checks(self, common_data):
common_kwargs=dict({"finalize_check": self.finalize_check}, **common_data),
monitor_interval=self.monitor_interval,
monitor_timeout=self.monitor_timeout,
max_threads=self.max_threads,
callback_on_monitoring=print_progress,
callback_on_start_failure=self.finalize_check_on_thread_failure,
callback_on_timeout=self.finalize_check_on_thread_timeout,
Expand All @@ -6303,7 +6393,7 @@ def main(_args=None):
print("Timeout(sec): {}".format(DEFAULT_TIMEOUT))
return

cm = CheckManager(args.api_only, args.debug_function, args.timeout)
cm = CheckManager(args.api_only, args.debug_function, args.timeout, max_threads=args.max_threads)

if args.total_checks:
print("Total Number of Checks: {}".format(cm.total_checks))
Expand Down
15 changes: 14 additions & 1 deletion docs/docs/validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Items | Faults | This Script
[Service Graph BD Forceful Routing][c22] | :white_check_mark: | :no_entry_sign:
[AVE End-of-life][c23] | :white_check_mark: | :no_entry_sign:
[Shared Service with vzAny Consumer][c24] | :white_check_mark: | :no_entry_sign:

[Preferred Group Shared Service Provider][c25] | :white_check_mark: | :no_entry_sign:

[c1]: #vpc-paired-leaf-switches
[c2]: #overlapping-vlan-pool
Expand All @@ -160,6 +160,7 @@ Items | Faults | This Script
[c22]: #service-graph-bd-forceful-routing
[c23]: #ave-end-of-life
[c24]: #shared-service-with-vzany-consumer
[c25]: #preferred_group_shared_service_provider

### Defect Condition Checks

Expand Down Expand Up @@ -2265,6 +2266,16 @@ See [Inter-VRF contract with vzAny as the consumer][60] in Cisco ACI Contract Gu

See [Enable Policy Compression in Cisco ACI Contract Guide][61] for details about Policy Compression.

### Preferred Group Shared Service Provider

As detailed in the[ACI Policy Model][62] Starting in 4.2(6d) and later, or 5.1(1h) and later an EPG in a Contract Preferred Group can consume a shared service contract, but cannot be a provider for a shared service contract with an L3Out EPG as consumer.

Due to the behavior change contracts will no longer be pushed post-upgrade causing a significant datacenter outage.

The configuration will be faulted: e.g. F0467 Configuration failed for uni/tn-common/brc-shared/contract/epgCont-1/instp-ext-epg due to Invalid Contract Configuration, debug message: invalid-contract-config: Shared service provider cannot be in a Preferred Group.


Starting version 6.0(1g), this validation is relaxed for normal EPGs but is still not allowed for External EPGs.

## Defect Check Details

Expand Down Expand Up @@ -2710,3 +2721,5 @@ If any instances of `configpushShardCont` are flagged by this script, Cisco TAC
[59]: https://bst.cloudapps.cisco.com/bugsearch/bug/CSCwp95515
[60]: https://www.cisco.com/c/en/us/solutions/collateral/data-center-virtualization/application-centric-infrastructure/white-paper-c11-743951.html#Inter
[61]: https://www.cisco.com/c/en/us/solutions/collateral/data-center-virtualization/application-centric-infrastructure/white-paper-c11-743951.html#EnablePolicyCompression
[62]: https://www.cisco.com/c/en/us/td/docs/switches/datacenter/aci/apic/sw/5-x/aci-fundamentals/cisco-aci-fundamentals-50x/m_policy-model.html#concept_tds_vcc_fy

Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,13 @@
"tDn": "topology/pod-1/paths-101/pathep-[eth1/1]"
}
}
}, {
"infraRsHPathAtt": {
"attributes": {
"dn": "uni/infra/hpaths-__ui_xxx_201-202_Eth49-50/rsHPathAtt-[topology/pod-1/paths-201/pathep-[xxx_201-202_Eth49-50]]",
"tCl": "fabricPathEp",
"tDn": "topology/pod-1/paths-201/pathep-[xxx_201-202_Eth49-50]"
}
}
}
]
Loading