def getHighestType(self, acquiredtype: str, limit=None): """getHighestType get Highest possible version for given type :param acquiredtype: Type available :param limit: Version string limit (full namespace or just version 'v1_x_x') """ typelist = list() if limit is not None: if getVersion(limit) is None: rst.traverseLogger.warning('Limiting namespace has no version, erasing: {}'.format(limit)) limit = None else: limit = getVersion(limit) for schema in self.soup.find_all('Schema'): newNamespace = schema.get('Namespace') if limit is not None: if getVersion(newNamespace) is None: continue if compareMinVersion(newNamespace, limit): continue if schema.find(['EntityType', 'ComplexType'], attrs={'Name': getType(acquiredtype)}, recursive=False): typelist.append(splitVersionString(newNamespace)) if len(typelist) > 1: for ns in reversed(sorted(typelist)): rst.traverseLogger.debug( "{} {}".format(ns, getType(acquiredtype))) acquiredtype = getNamespaceUnversioned(acquiredtype) + '.v{}_{}_{}'.format(*ns) + '.' + getType(acquiredtype) return acquiredtype return acquiredtype
def getTypeTagInSchema(self, currentType, tagType=['EntityType', 'ComplexType']): """getTypeTagInSchema Get type tag in schema :param currentType: type string :param tagType: Array or single string containing the xml tag name """ pnamespace, ptype = getNamespace(currentType), getType(currentType) soup = self.soup currentSchema = soup.find( 'Schema', attrs={'Namespace': pnamespace}) if currentSchema is None: return None currentEntity = currentSchema.find(tagType, attrs={'Name': ptype}, recursive=False) return currentEntity
def __init__(self, typename, schemaObj): # if we've generated this type, use it, else generate type self.initiated = False self.fulltype = typename self.snamespace, self.stype = getNamespace(self.fulltype), getType( self.fulltype) self.schemaObj = schemaObj self.parent = None self.propList = [] self.actionList = [] self.propPattern = None self.additional = False self.expectedURI = None # get all properties and actions in Type chain success, currentSchemaObj, baseType = True, self.schemaObj, self.fulltype try: newPropList, newActionList, self.additional, self.propPattern, self.expectedURI = getTypeDetails( currentSchemaObj, baseType) self.propList.extend(newPropList) self.actionList.extend(newActionList) success, currentSchemaObj, baseType = currentSchemaObj.getParentType( baseType) if success: self.parent = PropType(baseType, currentSchemaObj) if not self.additional: self.additional = self.parent.additional if self.expectedURI is None: self.expectedURI = self.parent.expectedURI except Exception as ex: rst.traverseLogger.debug( 'Exception caught while creating new PropType', exc_info=1) rst.traverseLogger.error('{}: Getting type failed for {}'.format( str(self.fulltype), str(baseType))) raise ex self.initiated = True
def getPropertyDetails(schemaObj, propertyOwner, propertyName, val, topVersion=None, customType=None, parent=None, top_of_resource=None): """ Get dictionary of tag attributes for properties given, including basetypes. param arg1: soup data param arg2: references ... """ propEntry = dict() propEntry['val'] = val if val == 'n/a': val = None OwnerNamespace, OwnerType = getNamespace(propertyOwner), getType( propertyOwner) rst.traverseLogger.debug('___') rst.traverseLogger.debug('{}, {}:{}'.format(OwnerNamespace, propertyOwner, propertyName)) soup, refs = schemaObj.soup, schemaObj.refs if customType is None: # Get Schema of the Owner that owns this prop ownerSchema = soup.find('Schema', attrs={'Namespace': OwnerNamespace}) if ownerSchema is None: rst.traverseLogger.warning( "getPropertyDetails: Schema could not be acquired, {}".format( OwnerNamespace)) return None # Get Entity of Owner, then the property of the Property we're targeting ownerEntity = ownerSchema.find(['EntityType', 'ComplexType'], attrs={'Name': OwnerType}, recursive=False) # check if this property is a nav property # Checks if this prop is an annotation success, propertySoup, propertyRefs, propertyFullType = True, soup, refs, OwnerType if '@' not in propertyName: propEntry['isTerm'] = False # not an @ annotation propertyTag = ownerEntity.find(['NavigationProperty', 'Property'], attrs={'Name': propertyName}, recursive=False) # start adding attrs and props together propertyInnerTags = propertyTag.find_all(recursive=False) for tag in propertyInnerTags: if (not tag.get('Term')): rst.traverseLogger.warn(tag, 'does not contain a Term name') elif (tag.get('Term') == 'Redfish.Revisions'): propEntry[tag['Term']] = tag.find_all('Record') else: propEntry[tag['Term']] = tag.attrs propertyFullType = propertyTag.get('Type') else: propEntry['isTerm'] = True ownerEntity = ownerSchema.find(['Term'], attrs={'Name': OwnerType}, recursive=False) if ownerEntity is None: ownerEntity = ownerSchema.find(['EntityType', 'ComplexType'], attrs={'Name': OwnerType}, recursive=False) propertyTag = ownerEntity propertyFullType = propertyTag.get('Type', propertyOwner) propEntry['isNav'] = propertyTag.name == 'NavigationProperty' propEntry['attrs'] = propertyTag.attrs rst.traverseLogger.debug(propEntry) propEntry['realtype'] = 'none' else: propertyFullType = customType propEntry['realtype'] = 'none' propEntry['attrs'] = dict() propEntry['attrs']['Type'] = customType serviceRefs = rst.currentService.metadata.get_service_refs() serviceSchemaSoup = rst.currentService.metadata.get_soup() success, propertySoup, propertyRefs, propertyFullType = True, serviceSchemaSoup, serviceRefs, customType # find the real type of this, by inheritance while propertyFullType is not None: rst.traverseLogger.debug("HASTYPE") PropertyNamespace, PropertyType = getNamespace( propertyFullType), getType(propertyFullType) rst.traverseLogger.debug('{}, {}'.format(PropertyNamespace, propertyFullType)) # Type='Collection(Edm.String)' # If collection, check its inside type if re.match('Collection\(.*\)', propertyFullType) is not None: if val is not None and not isinstance(val, list): raise TypeError( 'This collection is not a List: {}'.format(val)) propertyFullType = propertyFullType.replace('Collection(', "").replace(')', "") propEntry['isCollection'] = propertyFullType continue else: if val is not None and isinstance( val, list) and propEntry.get('isCollection') is None: raise TypeError('This item should not be a List') # If basic, just pass itself if 'Edm' in propertyFullType: propEntry['realtype'] = propertyFullType break # get proper soup, check if this Namespace is the same as its Owner, otherwise find its SchemaXml if PropertyNamespace.split('.')[0] != OwnerNamespace.split('.')[0]: schemaObj = schemaObj.getSchemaFromReference(PropertyNamespace) success = schemaObj is not None if success: uri = schemaObj.origin propertySoup = schemaObj.soup propertyRefs = schemaObj.refs else: success, propertySoup, uri = True, soup, 'of parent' if not success: rst.traverseLogger.warning( "getPropertyDetails: Could not acquire appropriate Schema for this item, {} {} {}" .format(propertyOwner, PropertyNamespace, propertyName)) return propEntry # traverse tags to find the type propertySchema = propertySoup.find( 'Schema', attrs={'Namespace': PropertyNamespace}) if propertySchema is None: rst.traverseLogger.warning( 'Schema element with Namespace attribute of {} not found in schema file {}' .format(PropertyNamespace, uri)) break propertyTypeTag = propertySchema.find( ['EnumType', 'ComplexType', 'EntityType', 'TypeDefinition'], attrs={'Name': PropertyType}, recursive=False) nameOfTag = propertyTypeTag.name if propertyTypeTag is not None else 'None' # perform more logic for each type if nameOfTag == 'TypeDefinition': # Basic type # This piece of code is rather simple UNLESS this is an "enumeration" # this is a unique deprecated enum, labeled as Edm.String propertyFullType = propertyTypeTag.get('UnderlyingType') isEnum = propertyTypeTag.find( 'Annotation', attrs={'Term': 'Redfish.Enumeration'}, recursive=False) if propertyFullType == 'Edm.String' and isEnum is not None: propEntry['realtype'] = 'deprecatedEnum' propEntry['typeprops'] = list() memberList = isEnum.find('Collection').find_all( 'PropertyValue') for member in memberList: propEntry['typeprops'].append(member.get('String')) rst.traverseLogger.debug("{}".format(propEntry['typeprops'])) break else: continue elif nameOfTag == 'ComplexType': # go deeper into this type rst.traverseLogger.debug("go deeper in type") # We need to find the highest existence of this type vs topVersion schema # not ideal, but works for this solution success, baseSoup, baseRefs, baseType = True, propertySoup, propertyRefs, propertyFullType # If we're outside of our normal Soup, then do something different, otherwise elif if PropertyNamespace.split('.')[0] != OwnerNamespace.split( '.')[0] and not customType: typelist = [] schlist = [] for schema in baseSoup.find_all('Schema'): if schema.find('ComplexType', attrs={'Name': PropertyType }) is None: continue newNamespace = schema.get('Namespace') typelist.append(newNamespace) schlist.append(schema) for item, schema in reversed(sorted(zip(typelist, schlist))): rst.traverseLogger.debug( "Working backwards: {} {}".format( item, getType(baseType))) baseType = item + '.' + getType(baseType) break elif topVersion is not None and (topVersion > OwnerNamespace): currentVersion = topVersion currentSchema = baseSoup.find( 'Schema', attrs={'Namespace': currentVersion}) # Working backwards from topVersion schematag, # created expectedType, check if currentTypeTag exists # if it does, use our new expectedType, else continue down parent types # until we exhaust all schematags in file while currentSchema is not None: expectedType = currentVersion + '.' + PropertyType currentTypeTag = currentSchema.find( 'ComplexType', attrs={'Name': PropertyType}) if currentTypeTag is not None: baseType = expectedType rst.traverseLogger.debug('new type: ' + baseType) break else: nextEntity = currentSchema.find( ['EntityType', 'ComplexType'], attrs={'Name': OwnerType}) if nextEntity is None: baseType = schemaObj.getHighestType( baseType, topVersion) break nextType = nextEntity.get('BaseType') currentVersion = getNamespace(nextType) currentSchema = baseSoup.find( 'Schema', attrs={'Namespace': currentVersion}) continue propEntry['realtype'] = 'complex' if propEntry.get('isCollection') is None: propEntry['typeprops'] = rst.createResourceObject( propertyName, 'complex', val, context=schemaObj.context, typename=baseType, isComplex=True, topVersion=topVersion, top_of_resource=top_of_resource) else: val = val if val is not None else [] propEntry['typeprops'] = [ rst.createResourceObject(propertyName, 'complex', item, context=schemaObj.context, typename=baseType, isComplex=True, topVersion=topVersion, top_of_resource=top_of_resource) for item in val ] break elif nameOfTag == 'EnumType': # If enum, get all members propEntry['realtype'] = 'enum' propEntry['typeprops'] = list() for MemberName in propertyTypeTag.find_all('Member'): propEntry['typeprops'].append(MemberName['Name']) break elif nameOfTag == 'EntityType': # If entity, do nothing special (it's a reference link) propEntry['realtype'] = 'entity' if val is not None: if propEntry.get('isCollection') is None: val = [val] val = val if val is not None else [] for innerVal in val: linkURI = innerVal.get('@odata.id') autoExpand = propEntry.get('OData.AutoExpand', None) is not None or\ propEntry.get('OData.AutoExpand'.lower(), None) is not None linkType = propertyFullType linkSchema = propertyFullType innerJson = innerVal propEntry[ 'typeprops'] = linkURI, autoExpand, linkType, linkSchema, innerJson else: propEntry['typeprops'] = None rst.traverseLogger.debug("typeEntityTag found {}".format( propertyTypeTag['Name'])) break else: rst.traverseLogger.error( 'Type {} not found under namespace {} in schema {}'.format( PropertyType, PropertyNamespace, uri)) break return propEntry
def getTypeDetails(schemaObj, SchemaAlias): """ Gets list of surface level properties for a given SchemaType, """ PropertyList = list() ActionList = list() PropertyPattern = None additional = False soup, refs = schemaObj.soup, schemaObj.refs SchemaNamespace, SchemaType = getNamespace(SchemaAlias), getType( SchemaAlias) rst.traverseLogger.debug("Generating type: {}".format(SchemaAlias)) rst.traverseLogger.debug("Schema is {}, {}".format(SchemaType, SchemaNamespace)) innerschema = soup.find('Schema', attrs={'Namespace': SchemaNamespace}) if innerschema is None: uri = schemaObj.origin rst.traverseLogger.error( 'getTypeDetails: Schema namespace {} not found in schema file {}. Will not be able to gather type details.' .format(SchemaNamespace, uri)) return PropertyList, ActionList, False, PropertyPattern, '.*' element = innerschema.find(['EntityType', 'ComplexType'], attrs={'Name': SchemaType}, recursive=False) if element is None: uri = schemaObj.origin rst.traverseLogger.error( 'getTypeDetails: Element {} not found in schema namespace {}. Will not be able to gather type details.' .format(SchemaType, SchemaNamespace)) return PropertyList, ActionList, False, PropertyPattern, '.*' rst.traverseLogger.debug("___") rst.traverseLogger.debug(element.get('Name')) rst.traverseLogger.debug(element.attrs) rst.traverseLogger.debug(element.get('BaseType')) additionalElement = element.find( 'Annotation', attrs={'Term': 'OData.AdditionalProperties'}) additionalElementOther = element.find( 'Annotation', attrs={'Term': 'Redfish.DynamicPropertyPatterns'}) uriElement = element.find('Annotation', attrs={'Term': 'Redfish.Uris'}) if additionalElement is not None: additional = additionalElement.get('Bool', False) if additional in ['false', 'False', False]: additional = False if additional in ['true', 'True']: additional = True else: additional = False if additionalElementOther is not None: # create PropertyPattern dict containing pattern and type for DynamicPropertyPatterns validation rst.traverseLogger.debug( 'getTypeDetails: Redfish.DynamicPropertyPatterns found, element = {}, SchemaAlias = {}' .format(element, SchemaAlias)) pattern_elem = additionalElementOther.find("PropertyValue", Property="Pattern") pattern = prop_type = None if pattern_elem is not None: pattern = pattern_elem.get("String") type_elem = additionalElementOther.find("PropertyValue", Property="Type") if type_elem is not None: prop_type = type_elem.get("String") rst.traverseLogger.debug( 'getTypeDetails: pattern = {}, type = {}'.format( pattern, prop_type)) if pattern is not None and prop_type is not None: PropertyPattern = dict() PropertyPattern['Pattern'] = pattern PropertyPattern['Type'] = prop_type additional = True expectedURI = None if uriElement is not None: try: all_strings = uriElement.find('Collection').find_all('String') expectedURI = [e.contents[0] for e in all_strings] except Exception as e: rst.traverseLogger.debug('Exception caught while checking URI', exc_info=1) rst.traverseLogger.warn( 'Could not gather info from Redfish.Uris annotation') expectedURI = None # get properties usableProperties = element.find_all(['NavigationProperty', 'Property'], recursive=False) for innerelement in usableProperties: rst.traverseLogger.debug(innerelement['Name']) rst.traverseLogger.debug(innerelement.get('Type')) rst.traverseLogger.debug(innerelement.attrs) newPropOwner = SchemaAlias if SchemaAlias is not None else 'SomeSchema' newProp = innerelement['Name'] rst.traverseLogger.debug("ADDING :::: {}:{}".format( newPropOwner, newProp)) PropertyList.append((schemaObj, newPropOwner, newProp)) # get actions usableActions = innerschema.find_all(['Action'], recursive=False) for act in usableActions: newPropOwner = getNamespace( SchemaAlias) if SchemaAlias is not None else 'SomeSchema' newProp = act['Name'] rst.traverseLogger.debug("ADDING ACTION :::: {}:{}".format( newPropOwner, newProp)) ActionList.append(PropAction(newPropOwner, newProp, act)) return PropertyList, ActionList, additional, PropertyPattern, expectedURI
def renderHtml(results, tool_version, startTick, nowTick, service, printCSV): # Render html config = service.config config_str = ', '.join(sorted(list(config.keys() - set(['systeminfo', 'targetip', 'password', 'description'])))) sysDescription, ConfigURI = (config['systeminfo'], config['targetip']) rsvLogger = rst.getLogger() logpath = config['logpath'] error_lines, finalCounts = count_errors(results) if service.metadata is not None: finalCounts.update(service.metadata.get_counter()) # wrap html htmlPage = '' htmlStrTop = '<head><title>Conformance Test Summary</title>\ <style>\ .pass {background-color:#99EE99}\ .column {\ float: left;\ width: 45%;\ }\ .fail {background-color:#EE9999}\ .warn {background-color:#EEEE99}\ .bluebg {background-color:#BDD6EE}\ .button {padding: 12px; display: inline-block}\ .center {text-align:center;}\ .log {text-align:left; white-space:pre-wrap; word-wrap:break-word; font-size:smaller; padding: 6px}\ .title {background-color:#DDDDDD; border: 1pt solid; font-height: 30px; padding: 8px}\ .titlesub {padding: 8px}\ .titlerow {border: 2pt solid}\ .results {transition: visibility 0s, opacity 0.5s linear; display: none; opacity: 0}\ .resultsShow {display: block; opacity: 1}\ body {background-color:lightgrey; border: 1pt solid; text-align:center; margin-left:auto; margin-right:auto}\ th {text-align:center; background-color:beige; border: 1pt solid}\ td {text-align:left; background-color:white; border: 1pt solid; word-wrap:break-word; overflow:hidden;}\ table {width:90%; margin: 0px auto; table-layout:fixed;}\ .titletable {width:100%}\ </style>\ </head>' htmlStrBodyHeader = '' # Logo and logname infos = [wrapTag('##### Redfish Conformance Test Report #####', 'h2')] infos.append(wrapTag('<img align="center" alt="DMTF Redfish Logo" height="203" width="288"' 'src="data:image/gif;base64,' + logo.logo + '">', 'h4')) infos.append('<h4><a href="https://github.com/DMTF/Redfish-Service-Validator">' 'https://github.com/DMTF/Redfish-Service-Validator</a></h4>') infos.append('Tool Version: {}'.format(tool_version)) infos.append(startTick.strftime('%c')) infos.append('(Run time: {})'.format( str(nowTick - startTick).rsplit('.', 1)[0])) infos.append('<h4>This tool is provided and maintained by the DMTF. ' 'For feedback, please open issues<br>in the tool\'s Github repository: ' '<a href="https://github.com/DMTF/Redfish-Service-Validator/issues">' 'https://github.com/DMTF/Redfish-Service-Validator/issues</a></h4>') htmlStrBodyHeader += tag.tr(tag.th(infoBlock(infos))) infos = {x: config[x] for x in config if x not in ['systeminfo', 'targetip', 'password', 'description']} infos_left, infos_right = dict(), dict() for key in sorted(infos.keys()): if len(infos_left) <= len(infos_right): infos_left[key] = infos[key] else: infos_right[key] = infos[key] block = tag.td(tag.div(infoBlock(infos_left), 'class=\'column log\'') \ + tag.div(infoBlock(infos_right), 'class=\'column log\''), 'id=\'resNumConfig\' class=\'results resultsShow\'') htmlButtons = '<div class="button warn" onClick="arr = document.getElementsByClassName(\'results\'); for (var i = 0; i < arr.length; i++){arr[i].classList.add(\'resultsShow\')};">Expand All</div>' htmlButtons += '<div class="button fail" onClick="arr = document.getElementsByClassName(\'results\'); for (var i = 0; i < arr.length; i++){arr[i].classList.remove(\'resultsShow\')};">Collapse All</div>' htmlButtons += tag.div('Toggle Config', attr='class="button pass" onClick="document.getElementById(\'resNumConfig\').classList.toggle(\'resultsShow\');"') htmlStrBodyHeader += tag.tr(tag.th(htmlButtons)) htmlStrBodyHeader += tag.tr(tag.th('Test Summary')) infos = {'System': ConfigURI, 'Description': sysDescription} htmlStrBodyHeader += tag.tr(tag.th(infoBlock(infos))) errors = error_lines if len(errors) == 0: errors = ['No errors located.'] errorTags = tag.td(infoBlock(errors), 'class="log"') infos_left, infos_right = dict(), dict() for key in sorted(finalCounts.keys()): if finalCounts.get(key) == 0: continue if len(infos_left) <= len(infos_right): infos_left[key] = finalCounts[key] else: infos_right[key] = finalCounts[key] htmlStrCounts = (tag.div(infoBlock(infos_left), 'class=\'column log\'') + tag.div(infoBlock(infos_right), 'class=\'column log\'')) htmlStrBodyHeader += tag.tr(tag.td(htmlStrCounts)) htmlStrBodyHeader += tag.tr(errorTags) htmlStrBodyHeader += tag.tr(block) if service.metadata is not None: htmlPage = service.metadata.to_html() for cnt, item in enumerate(results): entry = [] val = results[item] rtime = '(response time: {})'.format(val['rtime']) rcode = '({})'.format(val['rcode']) payload = val.get('payload', {}) # uri block prop_type = val['fulltype'] if prop_type is not None: namespace = getNamespace(prop_type) type_name = getType(prop_type) infos = [str(val.get(x)) for x in ['uri', 'samplemapped'] if val.get(x) not in ['',None]] infos.append(rtime) infos.append(type_name) uriTag = tag.tr(tag.th(infoBlock(infos, ' '), 'class="titlerow bluebg"')) entry.append(uriTag) # info block infos = [str(val.get(x)) for x in ['uri'] if val.get(x) not in ['',None]] infos.append(rtime) infos.append(tag.div('Show Results', attr='class="button warn"\ onClick="document.getElementById(\'payload{}\').classList.remove(\'resultsShow\');\ document.getElementById(\'resNum{}\').classList.toggle(\'resultsShow\');"'.format(cnt, cnt))) infos.append(tag.div('Show Payload', attr='class="button pass"\ onClick="document.getElementById(\'payload{}\').classList.toggle(\'resultsShow\');\ document.getElementById(\'resNum{}\').classList.add(\'resultsShow\');"'.format(cnt, cnt))) buttonTag = tag.td(infoBlock(infos), 'class="title" style="width:30%"') infos = [str(val.get(x)) for x in ['context', 'origin', 'fulltype']] infos = {y: x for x, y in zip(infos, ['Context', 'File Origin', 'Resource Type'])} infosTag = tag.td(infoBlock(infos), 'class="titlesub log" style="width:40%"') success = val['success'] if success: getTag = tag.td('GET Success HTTP Code {}'.format(rcode), 'class="pass"') else: getTag = tag.td('GET Failure HTTP Code {}'.format(rcode), 'class="fail"') countsTag = tag.td(infoBlock(val['counts'], split='', ffunc=applyInfoSuccessColor), 'class="log"') rhead = ''.join([buttonTag, infosTag, getTag, countsTag]) for x in [('tr',), ('table', 'class=titletable'), ('td', 'class=titlerow'), ('tr')]: rhead = wrapTag(''.join(rhead), *x) entry.append(rhead) # actual table rows = [[str(m)] + list([str(x) for x in val['messages'][m]]) for m in val['messages']] titles = ['Property Name', 'Value', 'Type', 'Exists', 'Result'] widths = ['15', '30', '30', '10', '15'] tableHeader = tableBlock(rows, titles, widths, ffunc=applySuccessColor) # lets wrap table and errors and warns into one single column table tableHeader = tag.tr(tag.td((tableHeader))) infos_a = [str(val.get(x)) for x in ['uri'] if val.get(x) not in ['',None]] infos_a.append(rtime) if(printCSV): rsvLogger.info(','.join(infos_a)) rsvLogger.info(','.join(infos)) rsvLogger.info(','.join(titles)) rsvLogger.info('\n'.join([','.join(x) for x in rows])) rsvLogger.info(',') # warns and errors errors = val['errors'] if len(errors) == 0: errors = 'No errors' infos = errors.split('\n') errorTags = tag.tr(tag.td(infoBlock(infos), 'class="fail log"')) warns = val['warns'] if len(warns) == 0: warns = 'No warns' infos = warns.split('\n') warnTags = tag.tr(tag.td(infoBlock(infos), 'class="warn log"')) payloadTag = tag.td(json.dumps(payload, indent=4, sort_keys=True), 'id=\'payload{}\' class=\'results log\''.format(cnt)) tableHeader += errorTags tableHeader += warnTags tableHeader += payloadTag tableHeader = tag.table(tableHeader) tableHeader = tag.td(tableHeader, 'class="results" id=\'resNum{}\''.format(cnt)) entry.append(tableHeader) # append htmlPage += ''.join([tag.tr(x) for x in entry]) return wrapTag(wrapTag(htmlStrTop + wrapTag(htmlStrBodyHeader + htmlPage, 'table'), 'body'), 'html')
def __init__(self, name: str, uri: str, jsondata: dict, typename: str, context: str, parent=None, isComplex=False, forceType=False): self.initiated = False self.parent = parent self.uri, self.name = uri, name self.rtime = 0 self.status = -1 self.isRegistry = False self.errorIndex = {} oem = config.get('oemcheck', True) # Check if this is a Registry resource parent_type = parent.typename if parent is not None and parent is not None else None if parent_type is not None and getType( parent_type) == 'MessageRegistryFile': traverseLogger.debug('{} is a Registry resource'.format(self.uri)) self.isRegistry = True self.context = None context = None # Check if we provide a valid json self.jsondata = jsondata traverseLogger.debug("payload: {}".format( json.dumps(self.jsondata, indent=4, sort_keys=True))) if not isinstance(self.jsondata, dict): traverseLogger.error("Resource no longer a dictionary...") raise ValueError('This Resource is no longer a Dictionary') # Check for @odata.id (todo: regex) odata_id = self.jsondata.get('@odata.id') if odata_id is None and not isComplex: if self.isRegistry: traverseLogger.debug( '{}: @odata.id missing, but not required for Registry resource' .format(self.uri)) else: traverseLogger.log( 'SERVICE', '{}: Json does not contain @odata.id'.format(self.uri)) # Get our real type (check for version) acquiredtype = typename if forceType else jsondata.get( '@odata.type', typename) if acquiredtype is None: traverseLogger.error( '{}: Json does not contain @odata.type or NavType'.format( uri)) raise ValueError if acquiredtype is not typename and isComplex: context = None if typename is not None: if not oem and 'OemObject' in typename: acquiredtype = typename if currentService: if not oem and 'OemObject' in acquiredtype: pass else: if jsondata.get('@odata.type') is not None: currentService.metadata.add_service_namespace( getNamespace(jsondata.get('@odata.type'))) if jsondata.get('@odata.context') is not None: # add the namespace to the set of namespaces referenced by this service ns = getNamespace( jsondata.get('@odata.context').split('#')[-1]) if '/' not in ns and not ns.endswith('$entity'): currentService.metadata.add_service_namespace(ns) # Provide a context for this (todo: regex) if context is None: context = self.jsondata.get('@odata.context') if context is None: context = createContext(acquiredtype) if self.isRegistry: # If this is a Registry resource, @odata.context is not required; do our best to construct one traverseLogger.debug( '{}: @odata.context missing from Registry resource; constructed context {}' .format(acquiredtype, context)) elif isComplex: pass else: traverseLogger.debug( '{}: Json does not contain @odata.context'.format( uri)) self.context = context # Get Schema object self.schemaObj = rfSchema.getSchemaObject(acquiredtype, self.context) if self.schemaObj is None: traverseLogger.error( "ResourceObject creation: No schema XML for {} {} {}".format( typename, acquiredtype, self.context)) raise ValueError # Use string comprehension to get highest type if acquiredtype is typename and not forceType: acquiredtype = self.schemaObj.getHighestType(typename, parent_type) if not isComplex: traverseLogger.debug( 'No @odata.type present, assuming highest type {} {}'. format(typename, acquiredtype)) # Check if we provide a valid type (todo: regex) self.typename = acquiredtype typename = self.typename self.initiated = True # get our metadata metadata = currentService.metadata if currentService else None self.typeobj = rfSchema.getTypeObject(typename, self.schemaObj) self.propertyList = self.typeobj.getProperties( self.jsondata, topVersion=getNamespace(typename)) propertyList = [prop.payloadName for prop in self.propertyList] # get additional self.additionalList = [] propTypeObj = self.typeobj if propTypeObj.propPattern is not None and len( propTypeObj.propPattern) > 0: prop_pattern = propTypeObj.propPattern.get('Pattern', '.*') prop_type = propTypeObj.propPattern.get('Type', 'Resource.OemObject') regex = re.compile(prop_pattern) for key in [ k for k in self.jsondata if k not in propertyList and regex.fullmatch(k) ]: val = self.jsondata.get(key) value_obj = rfSchema.PropItem(propTypeObj.schemaObj, propTypeObj.fulltype, key, val, customType=prop_type) self.additionalList.append(value_obj) if config['uricheck'] and self.typeobj.expectedURI is not None: my_id = self.jsondata.get('Id') self.errorIndex[ 'bad_uri_schema_uri'] = not self.typeobj.compareURI( uri, my_id) self.errorIndex[ 'bad_uri_schema_odata'] = not self.typeobj.compareURI( odata_id, my_id) if self.errorIndex['bad_uri_schema_uri']: traverseLogger.error('{}: URI not in Redfish.Uris: {}'.format( uri, self.typename)) if my_id != uri.rsplit('/', 1)[-1]: traverseLogger.error( 'Id {} in payload doesn\'t seem to match URI'.format( my_id)) else: traverseLogger.debug('{} in Redfish.Uris: {}'.format( uri, self.typename)) if self.errorIndex['bad_uri_schema_odata']: traverseLogger.error( '{}: odata_id not in Redfish.Uris: {}'.format( odata_id, self.typename)) if my_id != uri.rsplit('/', 1)[-1]: traverseLogger.error( 'Id {} in payload doesn\'t seem to match URI'.format( my_id)) else: traverseLogger.debug('{} in Redfish.Uris: {}'.format( odata_id, self.typename)) # get annotation successService, annotationProps = getAnnotations( metadata, self.jsondata) if successService: self.additionalList.extend(annotationProps) # list illegitimate properties together self.unknownProperties = [ k for k in self.jsondata if k not in propertyList + [prop.payloadName for prop in self.additionalList] and '@odata' not in k ] self.links = OrderedDict() sample = config.get('sample') linklimits = config.get('linklimits', {}) self.links.update( self.typeobj.getLinksFromType(self.jsondata, self.context, self.propertyList, oem, linklimits, sample)) self.links.update( getAllLinks(self.jsondata, self.additionalList, self.schemaObj, context=context, linklimits=linklimits, sample_size=sample, oemCheck=oem))
def getAllLinks(jsonData, propList, schemaObj, prefix='', context='', linklimits=None, sample_size=0, oemCheck=True): """ Function that returns all links provided in a given JSON response. This result will include a link to itself. :param arg1: json dict :param arg2: property dict :param arg3: reference dict :param prefix: default blank, for deeper links :param context: default blank, for AutoExpanded types :return: list of links """ linkList = OrderedDict() if linklimits is None: linklimits = {} # check keys in propertyDictionary # if it is a Nav property, check that it exists # if it is not a Nav Collection, add it to list # otherwise, add everything IN Nav collection # if it is a Complex property, check that it exists # if it is, recurse on collection or individual item if not isinstance(jsonData, dict): traverseLogger.error("Generating links requires a dict") refDict = schemaObj.refs try: for propx in propList: propDict = propx.propDict if propDict is None: continue isNav = propDict.get('isNav', False) key = propx.name item = getType(key).split(':')[-1] insideItem = propx.val if propx.exists else None autoExpand = propDict.get('OData.AutoExpand', None) is not None or\ propDict.get('OData.AutoExpand'.lower(), None) is not None cType = propDict.get('isCollection') ownerNS = propx.propOwner.split('.')[0] ownerType = propx.propOwner.split('.')[-1] if isNav: if insideItem is not None: if cType is not None: cTypeName = getType(cType) cSchema = refDict.get(getNamespace(cType), (None, None))[1] if cSchema is None: cSchema = context for cnt, listItem in enumerate_collection( insideItem, cTypeName, linklimits, sample_size): linkList[prefix + str(item) + '.' + cTypeName + '#' + str(cnt)] = (listItem.get('@odata.id'), autoExpand, cType, cSchema, listItem) else: cType = propDict['attrs'].get('Type') cSchema = refDict.get(getNamespace(cType), (None, None))[1] if cSchema is None: cSchema = context linkList[prefix + str(item) + '.' + getType(propDict['attrs']['Name'])] = ( insideItem.get('@odata.id'), autoExpand, cType, cSchema, insideItem) elif item == 'Uri' and ownerNS == 'MessageRegistryFile' and ownerType == 'Location': # special handling for MessageRegistryFile Location Uri if insideItem is not None and isinstance( insideItem, str) and len(insideItem) > 0: uriItem = {'@odata.id': insideItem} cType = ownerNS + '.' + ownerNS cSchema = refDict.get(getNamespace(cType), (None, None))[1] if cSchema is None: cSchema = context traverseLogger.debug( 'Registry Location Uri: resource = {}, type = {}, schema = {}' .format(insideItem, cType, cSchema)) linkList[prefix + str(item) + '.' + getType(propDict['attrs']['Name'])] = ( uriItem.get('@odata.id'), autoExpand, cType, cSchema, uriItem) elif item == 'Actions': # special handling for @Redfish.ActionInfo payload annotations if isinstance(insideItem, dict): cType = 'ActionInfo.ActionInfo' cSchema = refDict.get(getNamespace(cType), (None, None))[1] for k, v in insideItem.items(): if not isinstance(v, dict): continue uri = v.get('@Redfish.ActionInfo') if isinstance(uri, str): uriItem = {'@odata.id': uri} traverseLogger.debug( '{}{}: @Redfish.ActionInfo annotation uri = {}' .format(item, k, uri)) linkList[prefix + str(item) + k + '.' + cType] = (uriItem.get('@odata.id'), autoExpand, cType, cSchema, uriItem) for propx in propList: propDict = propx.propDict if propDict is None: continue propDict = propx.propDict key = propx.name item = getType(key).split(':')[-1] if 'Oem' in item and not oemCheck: continue cType = propDict.get('isCollection') if propDict is None: continue elif propDict['realtype'] == 'complex': tp = propDict['typeprops'] if jsonData.get(item) is not None and tp is not None: if cType is not None: cTypeName = getType(cType) for item in tp: linkList.update(item.links) else: linkList.update(tp.links) traverseLogger.debug(str(linkList)) except Exception as e: traverseLogger.debug('Exception caught while getting all links', exc_info=1) traverseLogger.error( 'Unexpected error while extracting links from payload: {}'.format( repr(e))) # contents of Registries may be needed to validate other resources (like Bios), so move to front of linkList if 'Registries.Registries' in linkList: linkList.move_to_end('Registries.Registries', last=False) traverseLogger.debug( 'getAllLinks: Moved Registries.Registries to front of list') return linkList
def renderHtml(results, finalCounts, tool_version, startTick, nowTick, printCSV): # Render html config = rst.config config_str = ', '.join( sorted( list(config.keys() - set(['systeminfo', 'targetip', 'password', 'description'])))) rsvLogger = rst.getLogger() sysDescription, ConfigURI = (config['systeminfo'], config['targetip']) logpath = config['logpath'] # wrap html htmlPage = '' htmlStrTop = '<head><title>Conformance Test Summary</title>\ <style>\ .pass {background-color:#99EE99}\ .fail {background-color:#EE9999}\ .warn {background-color:#EEEE99}\ .bluebg {background-color:#BDD6EE}\ .button {padding: 12px; display: inline-block}\ .center {text-align:center;}\ .log {text-align:left; white-space:pre-wrap; word-wrap:break-word; font-size:smaller}\ .title {background-color:#DDDDDD; border: 1pt solid; font-height: 30px; padding: 8px}\ .titlesub {padding: 8px}\ .titlerow {border: 2pt solid}\ .results {transition: visibility 0s, opacity 0.5s linear; display: none; opacity: 0}\ .resultsShow {display: block; opacity: 1}\ body {background-color:lightgrey; border: 1pt solid; text-align:center; margin-left:auto; margin-right:auto}\ th {text-align:center; background-color:beige; border: 1pt solid}\ td {text-align:left; background-color:white; border: 1pt solid; word-wrap:break-word;}\ table {width:90%; margin: 0px auto; table-layout:fixed;}\ .titletable {width:100%}\ </style>\ </head>' htmlStrBodyHeader = '' # Logo and logname infos = [wrapTag('##### Redfish Conformance Test Report #####', 'h2')] infos.append( wrapTag( '<img align="center" alt="DMTF Redfish Logo" height="203" width="288"' 'src="data:image/gif;base64,' + logo.logo + '">', 'h4')) infos.append( '<h4><a href="https://github.com/DMTF/Redfish-Interop-Validator">' 'https://github.com/DMTF/Redfish-Interop-Validator</a></h4>') infos.append('Tool Version: {}'.format(tool_version)) infos.append(startTick.strftime('%c')) infos.append('(Run time: {})'.format( str(nowTick - startTick).rsplit('.', 1)[0])) infos.append( '<h4>This tool is provided and maintained by the DMTF. ' 'For feedback, please open issues<br>in the tool\'s Github repository: ' '<a href="https://github.com/DMTF/Redfish-Interop-Validator/issues">' 'https://github.com/DMTF/Redfish-Interop-Validator/issues</a></h4>') htmlStrBodyHeader += tr(th(infoBlock(infos))) infos = {'System': ConfigURI, 'Description': sysDescription} htmlStrBodyHeader += tr(th(infoBlock(infos))) infos = {'Profile': config['profile'], 'Schema': config['schema']} htmlStrBodyHeader += tr(th(infoBlock(infos))) infos = { x: config[x] for x in config if x not in [ 'systeminfo', 'targetip', 'password', 'description', 'profile', 'schema' ] } block = tr(th(infoBlock(infos, '|||'))) for num, block in enumerate(block.split('|||'), 1): sep = '<br/>' if num % 4 == 0 else ', ' sep = '' if num == len(infos) else sep htmlStrBodyHeader += block + sep htmlStrTotal = '<div>Final counts: ' for countType in sorted(finalCounts.keys()): if finalCounts.get(countType) == 0: continue htmlStrTotal += '{p}: {q}, '.format(p=countType, q=finalCounts.get(countType, 0)) htmlStrTotal += '</div><div class="button warn" onClick="arr = document.getElementsByClassName(\'results\'); for (var i = 0; i < arr.length; i++){arr[i].className = \'results resultsShow\'};">Expand All</div>' htmlStrTotal += '</div><div class="button fail" onClick="arr = document.getElementsByClassName(\'results\'); for (var i = 0; i < arr.length; i++){arr[i].className = \'results\'};">Collapse All</div>' htmlStrBodyHeader += tr(td(htmlStrTotal)) htmlPage = rst.currentService.metadata.to_html() for cnt, item in enumerate(results): entry = [] val = results[item] rtime = '(response time: {})'.format(val['rtime']) if len(val['messages']) == 0 and len(val['errors']) == 0: continue # uri block prop_type = val['fulltype'] if prop_type is not None: namespace = getNamespace(prop_type) type_name = getType(prop_type) infos_a = [ str(val.get(x)) for x in ['uri', 'samplemapped'] if val.get(x) not in ['', None] ] infos_a.append(rtime) infos_a.append(type_name) uriTag = tr(th(infoBlock(infos_a, ' '), 'class="titlerow bluebg"')) entry.append(uriTag) # info block infos_b = [ str(val.get(x)) for x in ['uri'] if val.get(x) not in ['', None] ] infos_b.append(rtime) infos_b.append( div('Show Results', attr= 'class="button warn" onClick="document.getElementById(\'resNum{}\').classList.toggle(\'resultsShow\');"' .format(cnt))) buttonTag = td(infoBlock(infos_b), 'class="title" style="width:30%"') infos_content = [ str(val.get(x)) for x in ['context', 'origin', 'fulltype'] ] infos_c = { y: x for x, y in zip(infos_content, ['Context', 'File Origin', 'Resource Type']) } infosTag = td(infoBlock(infos_c), 'class="titlesub log" style="width:40%"') success = val['success'] if success: getTag = td('GET Success', 'class="pass"') else: getTag = td('GET Failure', 'class="fail"') countsTag = td( infoBlock(val['counts'], split='', ffunc=applyInfoSuccessColor), 'class="log"') rhead = ''.join([buttonTag, infosTag, getTag, countsTag]) for x in [('tr', ), ('table', 'class=titletable'), ('td', 'class=titlerow'), ('tr')]: rhead = wrapTag(''.join(rhead), *x) entry.append(rhead) # actual table rows = [(str(i.name), str(i.entry), str(i.expected), str(i.actual), str(i.success.value)) for i in val['messages']] titles = ['Property Name', 'Value', 'Expected', 'Actual', 'Result'] widths = ['15', '30', '30', '10', '15'] tableHeader = tableBlock(rows, titles, widths, ffunc=applySuccessColor) # lets wrap table and errors and warns into one single column table tableHeader = tr(td((tableHeader))) if (printCSV): rsvLogger.info(','.join(infos_a)) rsvLogger.info(','.join(infos_content)) rsvLogger.info(','.join(titles)) rsvLogger.info('\n'.join([','.join(x) for x in rows])) rsvLogger.info(',') # warns and errors errors = val['errors'] if len(errors) == 0: errors = 'No errors' infos = errors.split('\n') errorTags = tr(td(infoBlock(infos), 'class="fail log"')) warns = val['warns'] if len(warns) == 0: warns = 'No warns' infos = warns.split('\n') warnTags = tr(td(infoBlock(infos), 'class="warn log"')) tableHeader += errorTags tableHeader += warnTags tableHeader = table(tableHeader) tableHeader = td(tableHeader, 'class="results" id=\'resNum{}\''.format(cnt)) entry.append(tableHeader) # append htmlPage += ''.join([tr(x) for x in entry]) return wrapTag( wrapTag(htmlStrTop + wrapTag(htmlStrBodyHeader + htmlPage, 'table'), 'body'), 'html')