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
4 changes: 2 additions & 2 deletions redfish_interop_validator/RedfishInteropValidator.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ def main(argslist=None, configfile=None):

import redfish_interop_validator.tohtml as tohtml

html_str = tohtml.renderHtml(results, tool_version, start_tick, now_tick, currentService)
html_str = tohtml.renderHtml(results, final_counts, tool_version, start_tick, now_tick, currentService)

lastResultsPage = datetime.strftime(start_tick, os.path.join(logpath, "InteropHtmlLog_%m_%d_%Y_%H%M%S.html"))

Expand All @@ -295,7 +295,7 @@ def main(argslist=None, configfile=None):
'Pass: {}'.format(final_counts['pass']),
'Fail: {}'.format(final_counts['error']),
'Warning: {}'.format(final_counts['warning']),
'Not Tested: {}'.format(final_counts['nottested']),
'Not Tested: {}'.format(final_counts['not_tested']),
]))

success = final_counts['error'] == 0
Expand Down
7 changes: 3 additions & 4 deletions redfish_interop_validator/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def parseProfileInclude(target_name, target_profile_info, directories, online):

def getProfiles(profile, directories, chain=None, online=False):
profile_includes, required_by_resource = [], []

# Prevent cyclical imports when possible
profile_name = profile.get('ProfileName')
if chain is None:
Expand All @@ -174,12 +174,11 @@ def getProfiles(profile, directories, chain=None, online=False):
inner_includes, inner_reqs = getProfiles(profile_data, directories, chain)
profile_includes.extend(inner_includes)
required_by_resource.extend(inner_reqs)

# Process all RequiredResourceProfile by modifying profiles
profile_resources = profile.get('Resources', {})

for resource_name, resource in profile_resources.items():
# Modify just the resource or its UseCases. Should not have concurrent UseCases and RequiredResourceProfile in Resource
# Modify just the resource or its UseCases. Should not have concurrent UseCases and RequiredResourceProfile in Resource
if 'UseCases' not in resource:
modifying_objects = [resource]
else:
Expand Down
35 changes: 27 additions & 8 deletions redfish_interop_validator/tohtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def applyInfoSuccessColor(num, entry):
return tag.div(entry, attr=style)


def renderHtml(results, tool_version, startTick, nowTick, service):
def renderHtml(results, finalCounts, tool_version, startTick, nowTick, service):
# Render html
config = service.config
config_str = ', '.join(sorted(list(config.keys() - set(['systeminfo', 'targetip', 'password', 'description']))))
Expand Down Expand Up @@ -142,15 +142,18 @@ def renderHtml(results, tool_version, startTick, nowTick, service):

for k, my_result in results.items():
for msg in my_result['messages']:
if msg.result in [testResultEnum.PASS]:
if msg.result == testResultEnum.PASS:
summary['pass'] += 1
if msg.result in [testResultEnum.NOT_TESTED]:
elif msg.result == testResultEnum.FAIL:
summary['error'] += 1
elif msg.result == testResultEnum.WARN:
summary['warning'] += 1
elif msg.result == testResultEnum.NOT_TESTED:
summary['not_tested'] += 1
# to avoid double counting, only count if record doesn't have a 'result' attribute
for record in my_result['records']:
if record.levelname.lower() in ['error', 'warning']:
if not record.result and record.levelname.lower() in ['error', 'warning']:
summary[record.levelname.lower()] += 1
if record.result:
summary[record.result] += 1

important_block = tag.div('<b>Results Summary</b>')
important_block += tag.div(", ".join([
Expand All @@ -161,6 +164,16 @@ def renderHtml(results, tool_version, startTick, nowTick, service):
]))
htmlStrBodyHeader += tag.tr(tag.td(important_block, 'class="center"'))

# Filter non-zero counts and split into two columns
non_zero_items = [(k, v) for k, v in sorted(finalCounts.items()) if v != 0]
infos_left = dict(non_zero_items[::2]) # every other item starting at 0
infos_right = dict(non_zero_items[1::2]) # every other item starting at 1

htmlStrCounts = (tag.div(infoBlock(infos_left), 'class=\'column log\'') +
tag.div(infoBlock(infos_right), 'class=\'column log\''))

htmlStrBodyHeader += tag.tr(tag.td(htmlStrCounts))

infos = {x: config[x] for x in config if x not in ['systeminfo', 'ip', 'password', 'description']}
infos_left, infos_right = dict(), dict()
for key in sorted(infos.keys()):
Expand Down Expand Up @@ -232,10 +245,16 @@ def renderHtml(results, tool_version, startTick, nowTick, service):
my_summary = Counter()

for msg in my_result['messages']:
if msg.result in [testResultEnum.PASS]:
if msg.result == testResultEnum.PASS:
my_summary['pass'] += 1
if msg.result in [testResultEnum.NOT_TESTED]:
elif msg.result == testResultEnum.NOT_TESTED:
my_summary['not_tested'] += 1
elif msg.result == testResultEnum.FAIL:
my_summary['fail'] += 1
elif msg.result == testResultEnum.WARN:
my_summary['warn'] += 1
elif msg.result in [testResultEnum.NOPASS, testResultEnum.OK, testResultEnum.NA]:
my_summary[msg.result.value.lower()] += 1

for record in my_result['records']:
if record.levelname.lower() in ['error', 'warning']:
Expand Down
142 changes: 98 additions & 44 deletions redfish_interop_validator/validateResource.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import logging
import re
from io import StringIO
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading

import redfish_interop_validator.traverseInterop as traverseInterop
import redfish_interop_validator.interop as interop
Expand Down Expand Up @@ -208,7 +210,7 @@ def validateURITree(URI, profile, uriName, expectedType=None, expectedSchema=Non

links, limited_links = links if links else ({}, {})
for skipped_link in limited_links:
allLinks.add(limited_links[skipped_link])
allLinks.add(limited_links[skipped_link].rstrip('/'))

if resource_obj:
SchemaType = getType(resource_obj.jsondata.get('@odata.type', 'NoType'))
Expand All @@ -231,14 +233,20 @@ def validateURITree(URI, profile, uriName, expectedType=None, expectedSchema=Non
message_list.append(msg)

currentLinks = [(link, links[link], resource_obj) for link in links]
# Get max_workers from config with default of 100
max_workers = traverseInterop.config.get('max_workers', 50)
results_lock = threading.Lock()

# todo : churning a lot of links, causing possible slowdown even with set checks
while len(currentLinks) > 0:
newLinks = list()
for linkName, link, parent in currentLinks:

# Filter links to process (skip already visited, fragments, etc.)
links_to_process = []
for linkName, link, parent in currentLinks:
if link is None or link.rstrip('/') in allLinks:
continue

if '#' in link:
# NOTE: Skips referenced Links (using pound signs), this program currently only works with direct links
continue
Expand All @@ -247,51 +255,97 @@ def validateURITree(URI, profile, uriName, expectedType=None, expectedSchema=Non
refLinks.append((linkName, link, parent))
continue

# NOTE: unable to determine autoexpanded resources without Schema
else:
linkSuccess, linkResults, inner_links, linkobj = \
validateSingleURI(link, profile, linkName, parent=parent)

allLinks.add(link.rstrip('/'))
links_to_process.append((linkName, link, parent))

results.update(linkResults)

if not linkSuccess:
continue

inner_links, inner_limited_links = inner_links

for skipped_link in inner_limited_links:
allLinks.add(inner_limited_links[skipped_link])

innerLinksTuple = [(link, inner_links[link], linkobj) for link in inner_links]
newLinks.extend(innerLinksTuple)
SchemaType = getType(linkobj.jsondata.get('@odata.type', 'NoType'))

subordinate_tree = []

current_parent = linkobj.parent
while current_parent:
parentType = getType(current_parent.jsondata.get('@odata.type', 'NoType'))
subordinate_tree.append(parentType)
current_parent = current_parent.parent

# Search for UseCase.USECASENAME
usecases_found = [msg.name.split('.')[-1] for msg in linkResults[linkName]['messages'] if 'UseCase' == msg.name.split('.')[0]]
# Skip parallel processing if no links to process
if not links_to_process:
if refLinks is not currentLinks and len(newLinks) == 0 and len(refLinks) > 0:
currentLinks = refLinks
else:
currentLinks = newLinks
continue

if resource_stats.get(SchemaType) is None:
resource_stats[SchemaType] = {
"Exists": True,
"Writeable": False,
"URIsFound": [link.rstrip('/')],
"SubordinateTo": set([tuple(reversed(subordinate_tree))]),
"UseCasesFound": set(usecases_found),
# Process links in parallel using ThreadPoolExecutor
try:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# Submit all validation tasks
future_to_link = {
executor.submit(validateSingleURI, link, profile, linkName, parent=parent): (linkName, link, parent)
for linkName, link, parent in links_to_process
}
else:
resource_stats[SchemaType]['Exists'] = True
resource_stats[SchemaType]['URIsFound'].append(link.rstrip('/'))
resource_stats[SchemaType]['SubordinateTo'].add(tuple(reversed(subordinate_tree)))
resource_stats[SchemaType]['UseCasesFound'] = resource_stats[SchemaType]['UseCasesFound'].union(usecases_found)

# Process results as they complete
for future in as_completed(future_to_link):
linkName, link, parent = future_to_link[future]

try:
linkSuccess, linkResults, inner_links, linkobj = future.result()
except KeyboardInterrupt:
# Re-raise keyboard interrupt for graceful shutdown
raise
except traverseInterop.AuthenticationError as e:
my_logger.warning(f'Authentication error for {link}: {repr(e)}')
# Mark link as visited even on failure to avoid retry loops
with results_lock:
allLinks.add(link.rstrip('/'))
continue
except Exception as e:
my_logger.error(f'Exception during parallel validation of {link}: {repr(e)}')
# Mark link as visited even on failure to avoid retry loops
with results_lock:
allLinks.add(link.rstrip('/'))
continue

# Thread-safe updates to shared state
with results_lock:
allLinks.add(link.rstrip('/'))
results.update(linkResults)

if not linkSuccess:
continue

inner_links, inner_limited_links = inner_links

with results_lock:
for skipped_link in inner_limited_links:
allLinks.add(inner_limited_links[skipped_link].rstrip('/'))

innerLinksTuple = [(link, inner_links[link], linkobj) for link in inner_links]

# Thread-safe update to newLinks
with results_lock:
newLinks.extend(innerLinksTuple)

SchemaType = getType(linkobj.jsondata.get('@odata.type', 'NoType'))

subordinate_tree = []

current_parent = linkobj.parent
while current_parent:
parentType = getType(current_parent.jsondata.get('@odata.type', 'NoType'))
subordinate_tree.append(parentType)
current_parent = current_parent.parent

usecases_found = [msg.name.split('.')[-1] for msg in linkResults[linkName]['messages'] if 'UseCase' == msg.name.split('.')[0]]

with results_lock:
if resource_stats.get(SchemaType) is None:
resource_stats[SchemaType] = {
"Exists": True,
"Writeable": False,
"URIsFound": [link.rstrip('/')],
"SubordinateTo": set([tuple(reversed(subordinate_tree))]),
"UseCasesFound": set(usecases_found),
}
else:
resource_stats[SchemaType]['Exists'] = True
resource_stats[SchemaType]['URIsFound'].append(link.rstrip('/'))
resource_stats[SchemaType]['SubordinateTo'].add(tuple(reversed(subordinate_tree)))
resource_stats[SchemaType]['UseCasesFound'] = resource_stats[SchemaType]['UseCasesFound'].union(usecases_found)
except KeyboardInterrupt:
# Re-raise keyboard interrupt for graceful shutdown
raise

if refLinks is not currentLinks and len(newLinks) == 0 and len(refLinks) > 0:
currentLinks = refLinks
Expand Down