Skip to content

Commit

Permalink
Merge pull request #102 from DMTF/interop-service-update
Browse files Browse the repository at this point in the history
Update to similar functionality as Service-Validator
  • Loading branch information
mraineri authored Mar 19, 2020
2 parents bb9dacf + 74655d2 commit 8c660af
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 97 deletions.
16 changes: 9 additions & 7 deletions RedfishInteropValidator.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ def validateSingleURI(URI, profile, uriName='', expectedType=None, expectedSchem
else:
successGet, jsondata = True, expectedJson

if jsondata is not None:
successPayload, odataMessages = rst.ResourceObj.checkPayloadConformance(jsondata, URI)

if not successPayload:
counts['failPayloadWarn'] += 1
rsvLogger.verboseout(str(URI) + ': payload error, @odata property non-conformant',)

# Generate dictionary of property info
try:
propResourceObj = rst.createResourceObject(
Expand All @@ -121,11 +128,6 @@ def validateSingleURI(URI, profile, uriName='', expectedType=None, expectedSchem
results[uriName]['warns'], results[uriName]['errors'] = next(lc)
return False, counts, results, None, None

successPayload, odataMessages = propResourceObj.checkPayloadConformance()

if not successPayload:
counts['failPayloadWarn'] += 1
rsvLogger.verboseout(str(URI) + ': payload error, @odata property non-conformant',)
counts['passGet'] += 1

# if URI was sampled, get the notation text from rst.uri_sample_map
Expand Down Expand Up @@ -338,6 +340,7 @@ def main(arglist=None, direct_parser=None):
argget.add_argument('--verbose_checks', action="store_const", const=VERBO_NUM, default=logging.INFO,
help='Show all checks in logging (parameter-only)')
argget.add_argument('--nooemcheck', action='store_const', const=True, default=None, help='Don\'t check OEM items')
argget.add_argument('--csv_report', action='store_true', help='print a csv report at the end of the log')

# service
argget.add_argument('-i', '--ip', type=str, help='ip to test on [host:port]')
Expand Down Expand Up @@ -369,7 +372,6 @@ def main(arglist=None, direct_parser=None):
# Config information unique to Interop Validator
argget.add_argument('profile', type=str, default='sample.json', help='interop profile with which to validate service against')
argget.add_argument('--schema', type=str, default=None, help='schema with which to validate interop profile against')
argget.add_argument('--csv_report', action='store_true', help='print a csv report at the end of the log')
argget.add_argument('--warnrecommended', action='store_true', help='warn on recommended instead of pass')
# todo: write patches
argget.add_argument('--writecheck', action='store_true', help='(unimplemented) specify to allow WriteRequirement checks')
Expand Down Expand Up @@ -490,7 +492,7 @@ def main(arglist=None, direct_parser=None):
elif 'Tree' in rst.config.get('payloadmode'):
success, counts, resultsNew, xlinks, topobj = validateURITree(rst.config.get('payloadfilepath'), 'Target', profile, expectedJson=jsonData)
else:
success, counts, resultsNew, xlinks, topobj = validateURITree('/redfish/v1', 'ServiceRoot', profile, expectedJson=jsonData)
success, counts, resultsNew, xlinks, topobj = validateURITree('/redfish/v1/', 'ServiceRoot', profile, expectedJson=jsonData)

if results is None:
results = resultsNew
Expand Down
19 changes: 18 additions & 1 deletion commonInterop.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import re
import traverseService as rst
from commonRedfish import compareRedfishURI
from enum import Enum
from collections import Counter

Expand Down Expand Up @@ -518,6 +517,24 @@ def validateActionRequirement(propResourceObj, profile_entry, rf_payload_tuple,
# if it doesn't exist, what should not be checked for action
return msgs, counts

def compareRedfishURI(expected_uris, uri, my_id):
if expected_uris is not None:
regex = re.compile(r"{.*?}")
for e in expected_uris:
e_left, e_right = tuple(e.rsplit('/', 1))
_uri_left, uri_right = tuple(uri.rsplit('/', 1))
e_left = regex.sub('[a-zA-Z0-9_.-]+', e_left)
if regex.match(e_right):
if my_id is None:
rst.traverseLogger.warn('No Id provided by payload')
e_right = str(my_id)
e_compare_to = '/'.join([e_left, e_right])
success = re.fullmatch(e_compare_to, uri) is not None
if success:
break
else:
success = True
return success

def validateInteropURI(r_obj, profile_entry):
"""
Expand Down
66 changes: 39 additions & 27 deletions commonRedfish.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Copyright Notice:
# Copyright 2016-2018 DMTF. All rights reserved.
# Copyright 2016-2020 DMTF. All rights reserved.
# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Interop-Validator/blob/master/LICENSE.md

import re
Expand All @@ -10,32 +10,41 @@
Power.1.1.1.Power , Power.v1_0_0.Power
"""

versionpattern = 'v[0-9]_[0-9]_[0-9]'

def compareRedfishURI(expected_uris, uri, my_id):
if expected_uris is not None:
regex = re.compile(r"{.*?}")
for e in expected_uris:
e_left, e_right = tuple(e.rsplit('/', 1))
_uri_left, uri_right = tuple(uri.rsplit('/', 1))
e_left = regex.sub('[a-zA-Z0-9_.-]+', e_left)
if regex.match(e_right):
if my_id is None:
rst.traverseLogger.warn('No Id provided by payload')
e_right = str(my_id)
e_compare_to = '/'.join([e_left, e_right])
success = re.fullmatch(e_compare_to, uri) is not None
if success:
break
versionpattern = 'v[0-9]+_[0-9]+_[0-9]+'


def splitVersionString(version):
v_payload = version
if(re.match('([a-zA-Z0-9_.-]*\.)+[a-zA-Z0-9_.-]*', version) is not None):
new_payload = getVersion(version)
if new_payload is not None:
v_payload = new_payload
if ('_' in v_payload):
v_payload = v_payload.replace('v', '')
payload_split = v_payload.split('_')
else:
success = True
return success
payload_split = v_payload.split('.')
if len(payload_split) != 3:
return [0, 0, 0]
return [int(v) for v in payload_split]


def compareMinVersion(version, min_version):
"""
Checks for the minimum version of a resource's type
"""
# If version doesn't contain version as is, try it as v#_#_#
# get version from payload
min_split = splitVersionString(min_version)
payload_split = splitVersionString(version)

# use array comparison, which compares each sequential number
return min_split < payload_split

def navigateJsonFragment(decoded, URILink):
traverseLogger = rst.getLogger()
if '#' in URILink:
URILink, frag = tuple(URILink.rsplit('#', 1))
URIfragless, frag = tuple(URILink.rsplit('#', 1))
fragNavigate = frag.split('/')
for item in fragNavigate:
if item == '':
Expand All @@ -44,16 +53,19 @@ def navigateJsonFragment(decoded, URILink):
decoded = decoded.get(item)
elif isinstance(decoded, list):
if not item.isdigit():
traverseLogger.error("This is an Array, but this is not an index, aborting: {} {}".format(URILink, item))
traverseLogger.error("This URI ({}) is accessing an array, but this is not an index: {}".format(URILink, item))
return None
decoded = decoded[int(item)] if int(item) < len(decoded) else None
if not isinstance(decoded, dict):
traverseLogger.error(
"Decoded object no longer a dictionary {}".format(URILink))
return None
if int(item) >= len(decoded):
traverseLogger.error("This URI ({}) is accessing an array, but the index is too large for an array of size {}: {}".format(URILink, len(decoded), item))
return None
decoded = decoded[int(item)]
else:
traverseLogger.error("This URI ({}) has resolved to an invalid object that is neither an array or dictionary".format(URILink))
return None
return decoded



def getNamespace(string: str):
"""getNamespace
Expand Down
8 changes: 4 additions & 4 deletions metadata.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright Notice:
# Copyright 2018 DMTF. All rights reserved.
# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Service-Validator/blob/master/LICENSE.md
# Copyright 2018-2020 DMTF. All rights reserved.
# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Interop-Validator/blob/master/LICENSE.md

import os
import time
Expand All @@ -19,13 +19,13 @@
EDMX_TAGS = ['DataServices', 'Edmx', 'Include', 'Reference']


live_zip_uri = 'http://redfish.dmtf.org/schemas/DSP8010_2019.1.zip'
live_zip_uri = 'http://redfish.dmtf.org/schemas/DSP8010_2019.4.zip'


def setup_schema_pack(uri, local_dir, proxies, timeout):
rst.traverseLogger.info('Unpacking schema pack...')
if uri == 'latest':
uri = live_zip_uri
rst.traverseLogger.info('Unpacking schema pack... {}'.format(uri))
try:
if not os.path.isdir(local_dir):
os.makedirs(local_dir)
Expand Down
79 changes: 51 additions & 28 deletions rfSchema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Copyright Notice:
# Copyright 2016-2018 DMTF. All rights reserved.
# Copyright 2016-2020 DMTF. All rights reserved.
# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Interop-Validator/blob/master/LICENSE.md

from collections import namedtuple
Expand All @@ -10,7 +10,7 @@
import difflib
import os.path

from commonRedfish import getType, getNamespace, getNamespaceUnversioned, getVersion, compareRedfishURI
from commonRedfish import getType, getNamespace, getNamespaceUnversioned, getVersion, compareMinVersion, splitVersionString
import traverseService as rst
from urllib.parse import urlparse, urlunparse

Expand Down Expand Up @@ -350,16 +350,17 @@ def getHighestType(self, acquiredtype: str, limit=None):
if limit is not None:
if getVersion(newNamespace) is None:
continue
if getVersion(newNamespace) > limit:
if compareMinVersion(newNamespace, limit):
continue
if schema.find(['EntityType', 'ComplexType'], attrs={'Name': getType(acquiredtype)}, recursive=False):
typelist.append(newNamespace)

for ns in reversed(sorted(typelist)):
rst.traverseLogger.debug(
"{} {}".format(ns, getType(acquiredtype)))
acquiredtype = ns + '.' + getType(acquiredtype)
return acquiredtype
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


Expand All @@ -378,6 +379,17 @@ def getSchemaObject(typename, uri, metadata=None):
return rfSchema(soup, uri, origin, metadata=metadata, name=typename) if success else None


def get_fuzzy_property(newProp, jsondata, allPropList=[]):
pname = newProp
possibleMatch = difflib.get_close_matches(newProp, [s for s in jsondata], 1, 0.70)
if len(possibleMatch) > 0 and possibleMatch[0] not in [s[2] for s in allPropList if s[2] != newProp]:
val = jsondata.get(possibleMatch[0], 'n/a')
if val != 'n/a':
pname = possibleMatch[0]
rst.traverseLogger.error('{} was not found in payload, attempting closest match: {}'.format(newProp, pname))
return pname


class PropType:
robjcache = {}

Expand Down Expand Up @@ -422,16 +434,14 @@ def __init__(self, typename, schemaObj):
self.initiated = True

def getTypeChain(self):
if self.fulltype is None:
raise StopIteration
else:
if self.fulltype is not None:
node = self
tlist = []
while node is not None:
tlist.append(node.fulltype)
yield node.fulltype
node = node.parent
raise StopIteration
return

def getLinksFromType(self, jsondata, context, propList=None, oemCheck=True, linklimits={}, sample=None):
node = self
Expand Down Expand Up @@ -459,13 +469,11 @@ def getProperties(self, jsondata, topVersion=None):
pname = newProp
# if our val is empty, do fuzzy check for property that exists in payload but not in all properties
if val == 'n/a':
possibleMatch = difflib.get_close_matches(newProp, [s for s in jsondata], 1, 0.70)
if len(possibleMatch) > 0 and possibleMatch[0] not in [s[2] for s in allPropList if s[2] != newProp]:
val = jsondata.get(possibleMatch[0], 'n/a')
if val != 'n/a':
pname = possibleMatch[0]
rst.traverseLogger.error('{} was not found in payload, attempting closest match: {}'.format(newProp, pname))
props.append(PropItem(schemaObj, newPropOwner, newProp, val, topVersion, payloadName=pname))
pname = get_fuzzy_property(newProp, jsondata, allPropList)
validTypes = [getNamespace(x) for x in self.getTypeChain()]
if (topVersion in validTypes):
validTypes = validTypes[validTypes.index(topVersion):]
props.append(PropItem(schemaObj, newPropOwner, newProp, val, topVersion, payloadName=pname, versionList=validTypes))

return props

Expand All @@ -475,12 +483,27 @@ def getActions(self):
for prop in node.actionList:
yield prop
node = node.parent
raise StopIteration
return

def compareURI(self, uri, my_id):
expected_uris = self.expectedURI
return compareRedfishURI(expected_uris, uri, my_id)

if expected_uris is not None:
regex = re.compile(r"{.*?}")
for e in expected_uris:
e_left, e_right = tuple(e.rsplit('/', 1))
_uri_left, uri_right = tuple(uri.rsplit('/', 1))
e_left = regex.sub('[a-zA-Z0-9_.-]+', e_left)
if regex.match(e_right):
if my_id is None:
rst.traverseLogger.warn('No Id provided by payload')
e_right = str(my_id)
e_compare_to = '/'.join([e_left, e_right])
success = re.fullmatch(e_compare_to, uri) is not None
if success:
break
else:
success = True
return success


def getTypeDetails(schemaObj, SchemaAlias):
Expand Down Expand Up @@ -614,14 +637,14 @@ def getTypeObject(typename, schemaObj):


class PropItem:
def __init__(self, schemaObj, propOwner, propChild, val, topVersion=None, customType=None, payloadName=None):
def __init__(self, schemaObj, propOwner, propChild, val, topVersion=None, customType=None, payloadName=None, versionList=None):
try:
self.name = propOwner + ':' + propChild
self.propOwner, self.propChild = propOwner, propChild
self.val = val
self.valid = topVersion is None or \
(getNamespaceUnversioned(propOwner) in topVersion and getNamespace(propOwner) <= topVersion)\
or getNamespaceUnversioned(propOwner) not in topVersion
self.valid = topVersion is None or\
versionList is None or\
(getNamespace( propOwner ) in versionList)
self.exists = val != 'n/a'
self.payloadName = payloadName if payloadName is not None else propChild
self.propDict = getPropertyDetails(
Expand Down
Loading

0 comments on commit 8c660af

Please sign in to comment.