diff --git a/redfish_service_validator/RedfishServiceValidator.py b/redfish_service_validator/RedfishServiceValidator.py index 433de87..7f398ab 100755 --- a/redfish_service_validator/RedfishServiceValidator.py +++ b/redfish_service_validator/RedfishServiceValidator.py @@ -19,22 +19,25 @@ 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) @@ -42,6 +45,7 @@ def verbose2(self, msg, *args, **kwargs): standard_out.setLevel(logging.INFO) my_logger.addHandler(standard_out) + def validate(argslist=None, configfile=None): """Main command diff --git a/redfish_service_validator/catalog.py b/redfish_service_validator/catalog.py index 28df5ef..6a61f4e 100644 --- a/redfish_service_validator/catalog.py +++ b/redfish_service_validator/catalog.py @@ -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 """ @@ -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(): @@ -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: @@ -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 @@ -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 diff --git a/redfish_service_validator/validateRedfish.py b/redfish_service_validator/validateRedfish.py index dd4fd8e..b883405 100644 --- a/redfish_service_validator/validateRedfish.py +++ b/redfish_service_validator/validateRedfish.py @@ -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() @@ -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 == "": @@ -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: @@ -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 diff --git a/redfish_service_validator/validateResource.py b/redfish_service_validator/validateResource.py index 5a5256b..5279b33 100644 --- a/redfish_service_validator/validateResource.py +++ b/redfish_service_validator/validateResource.py @@ -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) @@ -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 @@ -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 @@ -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: @@ -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: @@ -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: @@ -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) @@ -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