Skip to content

Commit

Permalink
Merge pull request #580 from DMTF/validator-oem-check
Browse files Browse the repository at this point in the history
Validator OEM Check link fix and OEM Action fix
  • Loading branch information
mraineri authored Mar 22, 2024
2 parents 4be47bc + a70e9db commit 3dba781
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 44 deletions.
16 changes: 10 additions & 6 deletions redfish_service_validator/RedfishServiceValidator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,33 @@
tool_version = '2.4.1'

# Set up the custom debug levels
VERBOSE1=logging.INFO-1
VERBOSE2=logging.INFO-2
VERBOSE1 = logging.INFO-1
VERBOSE2 = logging.INFO-2

logging.addLevelName(VERBOSE1, "VERBOSE1")
logging.addLevelName(VERBOSE2, "VERBOSE2")

def verbose1(self, msg, *args, **kwargs):

def print_verbose_1(self, msg, *args, **kwargs):
if self.isEnabledFor(VERBOSE1):
self._log(VERBOSE1, msg, args, **kwargs)

def verbose2(self, msg, *args, **kwargs):

def print_verbose_2(self, msg, *args, **kwargs):
if self.isEnabledFor(VERBOSE2):
self._log(VERBOSE2, msg, args, **kwargs)

logging.Logger.verbose1 = verbose1
logging.Logger.verbose2 = verbose2

logging.Logger.verbose1 = print_verbose_1
logging.Logger.verbose2 = print_verbose_2

my_logger = logging.getLogger()
my_logger.setLevel(logging.DEBUG)
standard_out = logging.StreamHandler(sys.stdout)
standard_out.setLevel(logging.INFO)
my_logger.addHandler(standard_out)


def validate(argslist=None, configfile=None):
"""Main command
Expand Down
12 changes: 7 additions & 5 deletions redfish_service_validator/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ def __init__(self, redfish_type: RedfishType, name="Object", parent=None):
self.properties[prop] = RedfishProperty(REDFISH_ABSENT, prop, self)
my_logger.warning('Schema not found for {}'.format(typ))

def populate(self, payload, check=False, casted=False):
def populate(self, payload, uri_check=False, casted=False):
"""
Return a populated object, or list of objects
"""
Expand All @@ -875,7 +875,7 @@ def populate(self, payload, check=False, casted=False):

new_rf_object = RedfishObject(new_type_obj, populated_object.Name, populated_object.parent)

populated_object.Value = [new_rf_object.populate(sub_item, check, casted) for sub_item in payload]
populated_object.Value = [new_rf_object.populate(sub_item, uri_check, casted) for sub_item in payload]
return populated_object
else:
if populated_object.Type.IsCollection():
Expand Down Expand Up @@ -924,7 +924,7 @@ def populate(self, payload, check=False, casted=False):
my_odata_type = my_odata_type.strip('#')
try:
type_obj = populated_object.Type.catalog.getSchemaDocByClass(my_odata_type).getTypeInSchemaDoc(my_odata_type)
populated_object = RedfishObject(type_obj, populated_object.Name, populated_object.parent).populate(sub_payload, check=check, casted=True)
populated_object = RedfishObject(type_obj, populated_object.Name, populated_object.parent).populate(sub_payload, uri_check=uri_check, casted=True)
except MissingSchemaError:
my_logger.warning("Couldn't get schema for object, skipping OemObject {}".format(populated_object.Name))
except Exception as e:
Expand Down Expand Up @@ -964,7 +964,7 @@ def populate(self, payload, check=False, casted=False):
# NOTE: This returns a Type object without IsPropertyType
my_logger.verbose1(('Morphing Complex', my_ns, my_type, my_limit))
new_type_obj = populated_object.Type.catalog.getSchemaDocByClass(my_ns).getTypeInSchemaDoc('.'.join([my_ns, my_type]))
populated_object = RedfishObject(new_type_obj, populated_object.Name, populated_object.parent).populate(sub_payload, check=check, casted=True)
populated_object = RedfishObject(new_type_obj, populated_object.Name, populated_object.parent).populate(sub_payload, uri_check=uri_check, casted=True)
return populated_object

# Validate our Uri
Expand All @@ -977,7 +977,9 @@ def populate(self, payload, check=False, casted=False):
# Strip our URI and warn if that's the case
my_odata_id = sub_payload['@odata.id']
if my_odata_id != '/redfish/v1/' and my_odata_id.endswith('/'):
if check: my_logger.warning('Stripping end of URI... {}'.format(my_odata_id))
# NOTE: uri_check is only used to suppress this message, look into better message suppression
if uri_check:
my_logger.warning('Stripping end of URI... {}'.format(my_odata_id))
my_odata_id = my_odata_id.rstrip('/')

# Initial check if our URI matches our format at all
Expand Down
20 changes: 13 additions & 7 deletions redfish_service_validator/validateRedfish.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
my_logger = logging.getLogger()
my_logger.setLevel(logging.DEBUG)


def validateExcerpt(prop, val):
# check Navprop if it's NEUTRAL or CONTAINS
base = prop.Type.getBaseType()
Expand Down Expand Up @@ -109,8 +110,11 @@ def validateEntity(service, prop, val, parentURI=""):
return False
uri = val.get('@odata.id')
if '@odata.id' not in val:
if autoExpand: uri = parentURI + '#/{}'.format(name.replace('[', '/').strip(']'))
else: uri = parentURI + '/{}'.format(name)
if autoExpand:
uri = parentURI + '#/{}'.format(name.replace('[', '/').strip(']'))
else:
uri = parentURI + '/{}'.format(name)

if excerptType == ExcerptTypes.NEUTRAL:
my_logger.error("{}: EntityType resource does not contain required @odata.id property, attempting default {}".format(name, uri))
if parentURI == "":
Expand Down Expand Up @@ -240,7 +244,7 @@ def validateComplex(service, sub_obj, prop_name, oem_check=True):
# Get our actions from the object itself to test
# Action Namespace.Type, Action Object
my_actions = [(x.strip('#'), y) for x, y in sub_obj.Value.items() if x != 'Oem']
if 'Oem' in sub_obj.Value.items():
if 'Oem' in sub_obj.Value:
if oem_check:
my_actions.extend([(x, y) for x, y in sub_obj.Value['Oem'].items()])
else:
Expand Down Expand Up @@ -394,10 +398,12 @@ def checkPropertyConformance(service, prop_name, prop, parent_name=None, parent_
# check oem
# rs-assertion: 7.4.7.2
oem_check = service.config.get('oemcheck', True)
if 'Oem' in prop_name and not oem_check:
my_logger.verbose1('\tOem is skipped')
counts['skipOem'] += 1
return {prop_name: ('-', '-', 'Yes' if prop.Exists else 'No', 'OEM')}, counts

if not oem_check:
if 'Oem' in prop_name or 'Resource.OemObject' in prop.Type.getTypeTree():
my_logger.verbose1('\tOem is skipped')
counts['skipOem'] += 1
return {prop_name: ('-', '-', 'Yes' if prop.Exists else 'No', 'OEM')}, counts

# Parameter Passes
paramPass = propMandatoryPass = propNullablePass = deprecatedPassOrSinceVersion = nullValid = True
Expand Down
62 changes: 36 additions & 26 deletions redfish_service_validator/validateResource.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ def validateSingleURI(service, URI, uriName='', expectedType=None, expectedJson=
if any(x in key for x in ['problem', 'fail', 'bad', 'exception']):
pass_val = False
break
my_logger.info("\t {}".format('PASS' if pass_val else' FAIL...'))
my_logger.info("\t {}".format('PASS' if pass_val else ' FAIL...'))

my_logger.verbose1('%s, %s', SchemaFullType, counts)

Expand All @@ -292,30 +292,35 @@ def validateSingleURI(service, URI, uriName='', expectedType=None, expectedJson=

# Count of occurrences of fail, warn, invalid and deprecated in result of tests to FAILS / WARNINGS
for value in messages.values():
if "FAIL" in value.result: counts['fails'] += 1
if "WARN" in value.result or "INVALID" in value.result or "Deprecated" in value.result: counts['warnings'] += 1
if "FAIL" in value.result:
counts['fails'] += 1
if "WARN" in value.result or "INVALID" in value.result or "Deprecated" in value.result:
counts['warnings'] += 1

# Additional analysis of whether failMandatoryExist occurred in the scheme and adding the number of failMandatoryExist to FAILS
if 'failMandatoryExist' in counts.keys(): counts['fails'] += counts['failMandatoryExist']
if 'failMandatoryExist' in counts.keys():
counts['fails'] += counts['failMandatoryExist']

return True, counts, results, redfish_obj.getLinks(), redfish_obj


def validateURITree(service, URI, uriName, expectedType=None, expectedJson=None, parent=None, allLinks=None, inAnnotation=False):
def validateURITree(service, URI, uriName, expectedType=None, expectedJson=None, parent=None, all_links_traversed=None, inAnnotation=False):
# from given URI, validate it, then follow its links like nodes
# Other than expecting a valid URI, on success (real URI) expects valid links
# valid links come from getAllLinks, includes info such as expected values, etc
# as long as it is able to pass that info, should not crash
# If this is our first called URI
top = allLinks is None
if top: allLinks = set()
allLinks.add(URI)
top_of_tree = all_links_traversed is None
if top_of_tree:
all_links_traversed = set()
all_links_traversed.add(URI)

refLinks = []
# Links that are not direct, usually "Redundancy"
referenced_links = []

if inAnnotation and service.config['uricheck']:
service.catalog.flags['ignore_uri_checks'] = True
validateSuccess, counts, results, links, thisobj = validateSingleURI(service, URI, uriName, expectedType, expectedJson, parent)
validateSuccess, counts, results, gathered_links, thisobj = validateSingleURI(service, URI, uriName, expectedType, expectedJson, parent)
if inAnnotation and service.config['uricheck']:
service.catalog.flags['ignore_uri_checks'] = False

Expand All @@ -329,21 +334,21 @@ def validateURITree(service, URI, uriName, expectedType=None, expectedJson=None,
val_list = [thisobj['Location'].Value]
for sub_obj in val_list:
if 'Uri' in sub_obj:
links.append(sub_obj)
gathered_links.append(sub_obj)

# If successful...
if validateSuccess:
# Bring Registries to Front if possible
for link_type in service.config['collectionlimit']:
link_limit = service.config['collectionlimit'][link_type]
applicable_links = [x for x in links if link_type in x.Type.TypeName]
applicable_links = [link for link in gathered_links if link_type in link.Type.TypeName]
trimmed_links = applicable_links[link_limit:]
for link in trimmed_links:
link_destination = link.Value.get('@odata.id', link.Value.get('Uri'))
my_logger.verbose1('Removing link via limit: {} {}'.format(link_type, link_destination))
allLinks.add(link_destination)
all_links_traversed.add(link_destination)

for link in sorted(links, key=lambda x: (x.Type.fulltype != 'Registries.Registries')):
for link in sorted(gathered_links, key=lambda link: (link.Type.fulltype != 'Registries.Registries')):
if link is None or link.Value is None:
my_logger.warning('Link is None, does it exist?')
continue
Expand All @@ -359,10 +364,15 @@ def validateURITree(service, URI, uriName, expectedType=None, expectedJson=None,

if link.IsExcerpt or link.Type.Excerpt:
continue
if not service.config['oemcheck']:
if link_destination and '/Oem/' in link_destination or link and 'Resource.OemObject' in link.Type.getTypeTree():
my_logger.info('Oem link skipped: {}'.format(link_destination))
counts['skipOemLink'] += 1
continue
if any(x in str(link.parent.Type) or x in link.Name for x in ['RelatedItem', 'Redundancy', 'Links', 'OriginOfCondition']) and not link.IsAutoExpanded:
refLinks.append((link, thisobj))
referenced_links.append((link, thisobj))
continue
if link_destination in allLinks:
if link_destination in all_links_traversed:
counts['repeat'] += 1
continue
elif link_destination is None:
Expand All @@ -378,26 +388,26 @@ def validateURITree(service, URI, uriName, expectedType=None, expectedJson=None,
results[uriName]['warns'] += '\n' + warnmsg
counts['warnTrailingSlashLink'] += 1
newLink = ''.join(link_destination.split('/')[:-1])
if newLink in allLinks:
if newLink in all_links_traversed:
counts['repeat'] += 1
continue

if link.Type is not None and link.IsAutoExpanded:
returnVal = validateURITree(service, link_destination, uriName + ' -> ' + link.Name, link.Type, link.Value, thisobj, allLinks, link.InAnnotation)
returnVal = validateURITree(service, link_destination, uriName + ' -> ' + link.Name, link.Type, link.Value, thisobj, all_links_traversed, link.InAnnotation)
else:
returnVal = validateURITree(service, link_destination, uriName + ' -> ' + link.Name, parent=parent, allLinks=allLinks, inAnnotation=link.InAnnotation)
returnVal = validateURITree(service, link_destination, uriName + ' -> ' + link.Name, parent=parent, all_links_traversed=all_links_traversed, inAnnotation=link.InAnnotation)
success, linkCounts, linkResults, xlinks, xobj = returnVal

my_logger.verbose1('%s, %s', link.Name, linkCounts)

refLinks.extend(xlinks)
referenced_links.extend(xlinks)
if not success:
counts['unvalidated'] += 1
results.update(linkResults)

if top:
if top_of_tree:
# TODO: consolidate above code block with this
for link in refLinks:
for link in referenced_links:
link, refparent = link
# get Uri or @odata.id
if link is None or link.Value is None:
Expand Down Expand Up @@ -425,11 +435,11 @@ def validateURITree(service, URI, uriName, expectedType=None, expectedJson=None,
results[uriName]['warns'] += '\n' + warnmsg
counts['warnTrailingSlashRefLink'] += 1
newLink = ''.join(link_destination.split('/')[:-1])
if newLink in allLinks:
if newLink in all_links_traversed:
counts['repeat'] += 1
continue

if link_destination not in allLinks:
if link_destination not in all_links_traversed:
my_logger.verbose1('{}, {}'.format(link.Name, link))
counts['reflink'] += 1
else:
Expand All @@ -438,7 +448,7 @@ def validateURITree(service, URI, uriName, expectedType=None, expectedJson=None,
my_link_type = link.Type.fulltype
success, my_data, _, _ = service.callResourceURI(link_destination)
# Using None instead of refparent simply because the parent is not where the link comes from
returnVal = validateURITree(service, link_destination, uriName + ' -> ' + link.Name, my_link_type, my_data, None, allLinks)
returnVal = validateURITree(service, link_destination, uriName + ' -> ' + link.Name, my_link_type, my_data, None, all_links_traversed)
success, linkCounts, linkResults, xlinks, xobj = returnVal
# refLinks.update(xlinks)

Expand All @@ -451,4 +461,4 @@ def validateURITree(service, URI, uriName, expectedType=None, expectedJson=None,
else:
results.update(linkResults)

return validateSuccess, counts, results, refLinks, thisobj
return validateSuccess, counts, results, referenced_links, thisobj

0 comments on commit 3dba781

Please sign in to comment.