├── AUTHORS.md ├── CHANGELOG.md ├── LICENSE.md ├── README.md └── Redfish-JsonSchema-ResponseValidator.py /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Original Contribution: 2 | 3 | * Paul Vancil - Dell ESI -- tool definition 4 | * Jon Hass - Dell CTO -- tool definition 5 | * John Watts - Dell ESI tools contractor -- initial implementation 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.0.2] - 2023-08-08 4 | - Modified directory traversal to skip JSON Schema files 5 | 6 | ## [1.0.1] - 2018-10-12 7 | - Added -l option to validate a single, local JSON file 8 | 9 | ## [1.0.0] - 2018-08-17 10 | - Error message improvements 11 | - Added support for caching of schemas 12 | 13 | ## [0.9.0] - 2017-09-28 14 | - Initial version 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017-2024, Contributing Member(s) of Distributed Management Task 4 | Force, Inc.. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Copyright 2017-2018 DMTF. All rights reserved. 2 | 3 | # Redfish-JsonSchema-ResponseValidator 4 | 5 | ## Deprecated 6 | 7 | This tool is deprecated in favor of response payload validation performed by the [Redfish-Service-Validator](https://github.com/DMTF/Redfish-Service-Validator). 8 | 9 | ## About 10 | 11 | ***Redfish-JsonSchema-ResponseValidator.py*** is a Python3 utility used to validate any JSON resource against DMTF provided JSON schemas. 12 | 13 | ### To run: 14 | 15 | * Run from anywhere which has the proper Python3.4 or later environment, 16 | * You can validate against local JsonSchema files in a local directory on the client, --OR-- you can tell the validator to use the JsonSchema files at the DMTF site. 17 | * if you use the option to access the DMTF hosted JsonSchema files, you must of course have internet access from the client to the DMTF site for http GETs. 18 | * The typical use case is: 19 | * use Redfish-Mockup-Creator to pull a full mockup tree of all GET responses from a live system 20 | * then run resourceValidate pointing at the mockup tree to validate all of the responses in the mockup 21 | * If you have one or two error cases, you can then re-run pointing at a specific file in the mockup tree, 22 | * Or you can point resourceValidate at a live system and validate a single URI response 23 | * **See Examples below** 24 | 25 | ### [OPTIONS]: 26 | 27 | ``` 28 | Redfish-JsonSchema-ResponseValidator.py usage: 29 | -h display usage and exit 30 | -v verbose 31 | -m directory path to a mockup tree to validate against. default: ./mockup-sim-pull 32 | -s path to a local directory containing the json schema files to validate against. default ./DMTFSchemas 33 | -S Tells resourceValicate to get the schema from http://redfish.dmtf.org/schemas/v1/ 34 | -u user name, default root 35 | -p password, default calvin 36 | -e error output path/filename, default ./validate_errs 37 | -f comma separated list of files to validate. If -f is not specified, it will validate all index.json fils in the mockup 38 | -r hostname or IP address [:portno], default None 39 | -i url --used with -r option to specify the url to test, default /redfish/v1 40 | -x comma separated list of patterns to exclude from errors 41 | -g validate only resources which failed a previous run 42 | -l a local json file to validate 43 | NOTE: if -r is specified, this will validate 44 | one resource (rest API) from a host 45 | NOTE: if -f is specified, this will validate individual 46 | resources from the mockup directory 47 | NOTE: if -v is specified, resource JSON will be 48 | printed to std out 49 | NOTE: if -g is specified, input files will be the files 50 | found in a previous error file. If used with -v, 51 | the output will include the resource JSON and the Schema 52 | ``` 53 | 54 | ## Installation, Path, and Dependencies: 55 | 56 | * clone repo with validateResource Redfish-JsonSchema-ResponseValidator directory 57 | * If using the -m option, you must know the path to the mockup directory. 58 | * Dependent modules: 59 | * python3.4, 60 | * jsonschema, 61 | ``` 62 | NOTE: jsonschema should be installable by pip. 63 | # pip3 install jsonschema 64 | # This is the pypy implementation of the standard validator from json-schema.org 65 | ``` 66 | 67 | * requests `pip3 install requests` 68 | 69 | ## Examples: 70 | 71 | ``` 72 | Redfish-JsonSchema-ResponseValidator.py -m mockupdir 73 | -- walks the tree in mockupdir and validate every index.json file found 74 | 75 | Redfish-JsonSchema-ResponseValidator.py -r 199.199.199.1[:port] or MyRedfishHost -i /redfish/vi/Systems 76 | -- validates one response from a live service 77 | 78 | Redfish-JsonSchema-ResponseValidator.py -g -v [-e errorfile] > saveout 79 | -- validates the resources for a previous error file (-g) 80 | includes in the output the json resource and the json schema 81 | saves the standard out to be examined with an editor 82 | 83 | Redfish-JsonSchema-ResponseValidator.py -l LocalResourceFile -S 84 | -- validates the lcoal resources file with the schema from http://redfish.dmtf.org/schemas/v1/ 85 | 86 | NOTE: here is a shortcut bash script 87 | ``` 88 | 89 | ## Known Issues 90 | 91 | * filter tests to not try to validate /redfish (the version response) since it does not contain an @odata.id prop 92 | * filter tests to not try to validate /redfish/v1/odata/index.json (the Odata Service Doc) since it does not have an @odata.id prop 93 | 94 | ## Release Process 95 | 96 | 1. Update `CHANGELOG.md` with the list of changes since the last release 97 | 2. Update the `tool_version` variable in `Redfish-JsonSchema-ResponseValidator.py` to reflect the new tool version 98 | 3. Push changes to Github 99 | 4. Create a new release in Github 100 | 101 | ## See Also: 102 | 103 | * Redfish-Mockup-Creator 104 | -------------------------------------------------------------------------------- /Redfish-JsonSchema-ResponseValidator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright Notice: 3 | # Copyright 2017 DMTF. All rights reserved. 4 | # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-JsonSchema-ResponseValidator/blob/main/LICENSE.md 5 | 6 | ''' 7 | python34 resourceValidate.py [args] 8 | requirements: 9 | Python 3 10 | jsonschema -- pip(3) install jsonschema 11 | 12 | The purpose of this program is to validate 13 | redfish resources against DMTF json schemas. 14 | There are three ways to run this: 15 | 1) against a directory of all 16 | resources created by the mockup creator 17 | 2) selecting individual resources from 18 | that same directory 19 | ( the -f option ) 20 | 3) pulling a resource from an actual host 21 | of a redfish service. 22 | ( -r and -i options ) 23 | 24 | resourceValidate.py -h 25 | to see options and defaults 26 | 27 | NOTE: 28 | If you are running from a OS in which the 29 | default Python is 2.x, 30 | the following script might help. 31 | 32 | #!/usr/bin/env bash 33 | cmd="/usr/bin/scl enable rh-python34" 34 | args="$@" 35 | $cmd "./resourceValidate.py $args " 36 | 37 | ''' 38 | 39 | import os,sys 40 | import subprocess 41 | import re 42 | import json 43 | import jsonschema 44 | import getopt 45 | import requests 46 | 47 | tool_version = '1.0.2' 48 | 49 | def usage(): 50 | print ('\nRedfish-JsonSchema-ResponseValidator.py usage:') 51 | print (' -h display usage and exit') 52 | print (' -v verbose') 53 | print (' -m directory path to a mockup tree to validate against, default ./mockup-sim-pull') 54 | print (' -s path to a local dir containing the json schema files to validate against, default ./DMTFSchemas') 55 | print (' -S tell resourceValidate to get the schemaFiles from http://redfish.dmtf.org/schemas/v1/') 56 | print (' -u user name, default root') 57 | print (' -p password, default calvin') 58 | print (' -e error output file, default ./validate_errs') 59 | print (' -f comma separated list of files to validate. if no -f, it validates entire mockup') 60 | print (' -r hostname or IP address [:portno], default None') 61 | print (' -i url, --used with -r option to specify url for a live system. default /redfish/v1') 62 | print (' -x comma separated list of patterns to exclude from errors') 63 | print (' -g validate only resources which failed a previous run') 64 | print (' -l a local json file to validate') 65 | print ('\n') 66 | print ('NOTE: if -r is specified, this will validate ') 67 | print (' one resource (rest API) from a host') 68 | print ('NOTE: if -f is specified, this will validate individual') 69 | print (' resources from the mockup directory') 70 | print ('NOTE: if -v is specified, resource JSON will be') 71 | print (' printed to std out') 72 | print ('NOTE: if -g is specified, input files will be the files') 73 | print (' found in a previous error file. If used with -v,') 74 | print (' the output will include the resource JSON and the Schema') 75 | print ('\n') 76 | sys.exit() 77 | 78 | def parseArgs(rv,argv): 79 | # parse args 80 | try: 81 | opts, args = getopt.gnu_getopt(argv[1:],"hvgSm:s:u:p:e:f:r:i:x:l:") 82 | except getopt.GetoptError: 83 | print("Error parsing options") 84 | usage() 85 | 86 | for opt,arg in opts: 87 | if opt in ('-h'): usage() 88 | elif opt in ('-v'): rv.verbose = True 89 | elif opt in ('-m'): rv.mockdir = arg 90 | elif opt in ('-s'): rv.schemadir = arg 91 | elif opt in ('-S'): rv.schemaorg = True 92 | elif opt in ('-u'): rv.user = arg 93 | elif opt in ('-p'): rv.password = arg 94 | elif opt in ('-e'): rv.errfile = arg 95 | elif opt in ('-f'): rv.files = arg 96 | elif opt in ('-r'): rv.ipaddr = arg 97 | elif opt in ('-i'): rv.url = arg 98 | elif opt in ('-x'): rv.excludes = arg 99 | elif opt in ('-g'): rv.doerrs = True 100 | elif opt in ('-l'): rv.file = arg 101 | 102 | 103 | class ResourceValidate(object): 104 | def __init__(self, argv): 105 | self.verbose = False 106 | self.ipaddr = None 107 | self.url = '/redfish/v1' 108 | self.user = 'root' 109 | self.password = 'calvin' 110 | self.schemadir = './DMTFSchemas' 111 | self.schemaorg = False 112 | self.mockdir = './mockup-sim-pull' 113 | self.errfile = './validate_errs' 114 | self.files = None 115 | self.doerrs = False 116 | self.excludes = '' 117 | self.errcount = 0 118 | self.rescount = 0 119 | self.retget = 0 120 | self.retcache = 0 121 | self.savedata = '' 122 | self.file = '' 123 | parseArgs(self,argv) 124 | 125 | self.cachelist = [] 126 | self.cachedict = {} 127 | 128 | self.orgurl = 'http://redfish.dmtf.org/schemas/v1/' 129 | 130 | if not self.doerrs: 131 | self.ef = open( self.errfile,'w') 132 | 133 | if self.excludes: 134 | self.excludes = self.excludes.split(',') 135 | 136 | if self.doerrs: 137 | self.doErrors() 138 | elif self.ipaddr: 139 | self.valFromHost() 140 | elif self.file: 141 | self.localFile() 142 | elif self.files: 143 | self.traverseFiles() 144 | else: 145 | self.traverseDir() 146 | 147 | print ('\n{} resources validated.'.format(self.rescount)) 148 | if self.errcount: 149 | print ('{} errors. See {}'.format(self.errcount, self.errfile) ) 150 | else: print ('0 errors') 151 | print ('schemas returned from GET ',self.retget) 152 | print ('schemas returned from cache',self.retcache) 153 | 154 | def doErrors(self): 155 | self.procerrs = [] 156 | f = open(self.errfile,'r') 157 | lines = f.readlines() 158 | f.close() 159 | self.files = '' 160 | for line in lines: 161 | line = line.strip() 162 | if self.mockdir in line: 163 | line = line.replace(self.mockdir,'') 164 | line = line.replace('/index.json','') 165 | self.files += line + ',' 166 | self.traverseFiles() 167 | 168 | def valFromHost(self): 169 | ''' GET one resource from a host (rackmanager?) 170 | and validate against a DMTF schema. 171 | ''' 172 | print( self.ipaddr + ':' + self.url ) 173 | ret = self.get(self.ipaddr,self.url,self.user,self.password) 174 | if not ret: 175 | print('Invalid response from request') 176 | return 177 | 178 | if self.verbose: print(ret) 179 | try: 180 | data = json.loads(ret) 181 | except Exception as e: 182 | self.errHandle (str(e) + 'json load failed',self.url) 183 | return 184 | if '@odata.type' not in data: 185 | msg = 'ERROR1: Missing @odata.type ' 186 | self.errHandle(msg,self.url) 187 | return 188 | schname = self.parseOdataType(data) 189 | if schname[1]: 190 | schname = '.'.join(schname[:2]) 191 | else: 192 | schname = schname[0] 193 | schname += '.json' 194 | self.rescount += 1 195 | self.validate(data,schname,self.url) 196 | 197 | def localFile(self): 198 | ''' read a resources specified 199 | with the -l option, 200 | and validate against a DMTF schema. 201 | ''' 202 | try: 203 | print ('\n' + self.file) 204 | f = open(self.file,'r') 205 | except: 206 | print(self.file + ' not found') 207 | return 208 | try: 209 | data = f.read() 210 | if self.verbose: 211 | print(data) 212 | print('\n') 213 | data = json.loads(data) 214 | except Exception as e: 215 | self.errHandle (str(e) + ' load failed',self.file) 216 | return 217 | if '@odata.type' not in data: 218 | if 'redfish/index.json' not in fname: 219 | if 'redfish/v1/odata/index.json' not in self.file: 220 | msg = 'ERROR1: Missing @odata.type ' 221 | self.errHandle(msg,self.file) 222 | schname = self.parseOdataType(data) 223 | if schname[1]: 224 | schname = '.'.join(schname[:2]) 225 | else: 226 | schname = schname[0] 227 | schname += '.json' 228 | print ('JSON schema name is {}'.format(schname)) 229 | self.rescount += 1 230 | self.validate(data,schname,self.file) 231 | 232 | def traverseFiles(self): 233 | ''' read a list of resources specified 234 | with the -f option, 235 | and validate against a DMTF schema. 236 | ''' 237 | files = self.files.split(',') 238 | files = list(set(files)) 239 | for dirn in files: 240 | try: 241 | fname = self.mockdir + '/' + dirn + '/' + 'index.json' 242 | print ('\n' + fname) 243 | f = open(fname,'r') 244 | except: 245 | print('index.json not found') 246 | continue 247 | try: 248 | data = f.read() 249 | if self.verbose: 250 | print(data) 251 | print('\n') 252 | data = json.loads(data) 253 | except Exception as e: 254 | self.errHandle (str(e) + 'json load failed',fname) 255 | continue 256 | if '$schema' in data: 257 | # Schema file; skip 258 | continue 259 | if '@odata.type' not in data: 260 | if 'redfish/index.json' not in fname: 261 | if 'redfish/v1/odata/index.json' not in fname: 262 | msg = 'ERROR1: Missing @odata.type ' 263 | self.errHandle(msg,fname) 264 | continue 265 | schname = self.parseOdataType(data) 266 | if schname[1]: 267 | schname = '.'.join(schname[:2]) 268 | else: 269 | schname = schname[0] 270 | schname += '.json' 271 | self.rescount += 1 272 | self.validate(data,schname,fname) 273 | 274 | def traverseDir(self): 275 | ''' walk a directory of resources,i.e a "mockup" 276 | and validate against a DMTF schema. 277 | ''' 278 | for dirn, subdir, filelist in os.walk(self.mockdir): 279 | for fname in filelist: 280 | if fname == 'index.json': 281 | fname = dirn + '/' + fname 282 | print(fname) 283 | try: 284 | f = open(fname,'r') 285 | data = f.read() 286 | f.close() 287 | if self.verbose: print(data) 288 | except: 289 | self.errHandle ('failed to open and read',fname) 290 | continue 291 | try: 292 | data = json.loads(data) 293 | except Exception as e: 294 | self.errHandle (str(e) + 'json load failed',fname) 295 | continue 296 | if '$schema' in data: 297 | # Schema file; skip 298 | continue 299 | if '@odata.type' not in data: 300 | if 'redfish/index.json' not in fname: 301 | if 'redfish/v1/odata/index.json' not in fname: 302 | msg = 'ERROR1: Missing @odata.type ' 303 | self.errHandle(msg,fname) 304 | continue 305 | schname = self.parseOdataType(data) 306 | if schname[1]: 307 | schname = '.'.join(schname[:2]) 308 | else: 309 | schname = schname[0] 310 | schname += '.json' 311 | self.rescount += 1 312 | self.validate(data,schname,fname) 313 | 314 | def getFromOrg(self,schname): 315 | ''' Fetch the schema from the redfish organization 316 | ''' 317 | r = requests.get(self.orgurl + schname) 318 | if r.status_code != 200: 319 | self.errHandle('ERROR GET ERROR: schema not found', 320 | r.status_code,schname) 321 | return -1 322 | return r.text 323 | 324 | def getFromLocal(self,schname): 325 | ''' Fetch the schema from the local copy 326 | of the redfish schemas 327 | ''' 328 | try: 329 | schfile = self.schemadir + '/' + schname 330 | f = open(schfile) 331 | data = f.read() 332 | f.close() 333 | return data 334 | except: 335 | self.errHandle('ERROR: schema not found',fname,schname) 336 | return -1 337 | 338 | def getorcache(self,schname,src): 339 | ''' 1. check the cache to see if we already have it 340 | 2. if not, do a get from the schema org or local 341 | 3. store it in the cache 342 | ''' 343 | if schname in self.cachedict: 344 | self.retcache += 1 345 | return self.cachedict[schname] 346 | else: 347 | if src == 'org': 348 | data = self.getFromOrg(schname) 349 | if data == -1: return -1 350 | else: 351 | if src == 'local': 352 | data = self.getFromLocal(schname) 353 | if data == -1: return -1 354 | self.cachelist.append(schname) 355 | if len(self.cachelist) > 20: 356 | del self.cachedict[ self.cachelist[0] ] 357 | self.cachelist.pop(0) 358 | self.cachedict[schname] = data 359 | self.retget += 1 360 | return (data) 361 | 362 | def validate(self,data,schname,fname): 363 | ''' Fetch the schema from either redfish.org 364 | or local schemas, then validate 365 | ''' 366 | # get schema from redfish.org 367 | if self.schemaorg: 368 | datac = self.getorcache(schname,'org') 369 | if datac == -1: return 370 | try: 371 | schema = json.loads(datac) 372 | except Exception as e: 373 | input () 374 | self.errHandle (str(e) + 'json load failed',schname) 375 | return 376 | 377 | # get schema from local mockup 378 | else: 379 | datac = self.getorcache(schname,'local') 380 | if datac == -1: return 381 | try: 382 | schema = json.loads(datac) 383 | except Exception as e: 384 | self.errHandle (str(e) + 'json load failed',fname,schname) 385 | return 386 | 387 | ''' this sample from the jsonschema website 388 | ''' 389 | try: 390 | v = jsonschema.Draft4Validator(schema) 391 | for error in sorted(v.iter_errors(data), key=str): 392 | x = False 393 | for item in self.excludes: 394 | if item in error.message: x = True 395 | if not x: 396 | self.errHandle(error.message,fname,schname) 397 | except jsonschema.ValidationError as e: 398 | print (e.message) 399 | if self.verbose: 400 | print('\n',schema) 401 | print('\n') 402 | 403 | def errHandle(self,msg,fname,schname=''): 404 | print ('>>> ',msg) 405 | if not self.doerrs: 406 | outp = '\n\n' + fname + '\n schema: ' + schname + '\n>>>' + msg 407 | self.ef.write(outp) 408 | self.errcount += 1 409 | 410 | def subp (self,cmd): 411 | p=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) 412 | out, errors = p.communicate() 413 | ret = p.wait() 414 | if ret < 0: 415 | return ( errors ) 416 | else: 417 | try: 418 | return (out.decode() ) 419 | except: 420 | return out 421 | 422 | def get(self, host, url, user, password): 423 | ret = requests.get(host + url,auth=(user, password),verify=False) 424 | if ret.status_code == 200: 425 | return ret.text 426 | else: return None 427 | 428 | def parseOdataType(self,resource): 429 | ''' parse the @odata.type and return 430 | a usable tuple to construct 431 | the schema file name. 432 | ''' 433 | if not "@odata.type" in resource: 434 | print("Transport:parseOdataType: Error: No @odata.type in resource") 435 | return(None,None,None) 436 | 437 | resourceOdataType=resource["@odata.type"] 438 | 439 | #the odataType format is: .. where version may have periods in it 440 | odataTypeMatch = re.compile('^#([a-zA-Z0-9]*)\.([a-zA-Z0-9\._]*)\.([a-zA-Z0-9]*)$') 441 | resourceMatch = re.match(odataTypeMatch, resourceOdataType) 442 | if(resourceMatch is None): 443 | # try with no version component 444 | odataTypeMatch = re.compile('^#([a-zA-Z0-9]*)\.([a-zA-Z0-9]*)$') 445 | resourceMatch = re.match(odataTypeMatch, resourceOdataType) 446 | if (resourceMatch is None): 447 | print("Transport:parseOdataType: Error parsing @odata.type") 448 | return(None,None,None) 449 | else: 450 | namespace = resourceMatch.group(1) 451 | version = None 452 | resourceType = resourceMatch.group(2) 453 | else: 454 | namespace=resourceMatch.group(1) 455 | version=resourceMatch.group(2) 456 | resourceType=resourceMatch.group(3) 457 | 458 | return(namespace, version, resourceType) 459 | 460 | if __name__ == '__main__': 461 | print( "Redfish-JsonSchema-ResponseValidator version {}".format( tool_version ) ) 462 | rv = ResourceValidate(sys.argv) 463 | --------------------------------------------------------------------------------