redfish-interop-validator 2.2.7__tar.gz → 2.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/LICENSE.md +1 -1
  2. {redfish_interop_validator-2.2.7/redfish_interop_validator.egg-info → redfish_interop_validator-2.2.8}/PKG-INFO +13 -4
  3. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/README.md +1 -1
  4. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/RedfishInteropValidator.py +82 -60
  5. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/RedfishLogo.py +1 -1
  6. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/config.py +1 -1
  7. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/interop.py +1 -1
  8. redfish_interop_validator-2.2.8/redfish_interop_validator/profile.py +202 -0
  9. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/redfish.py +1 -1
  10. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/session.py +1 -1
  11. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/tohtml.py +1 -1
  12. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/traverseInterop.py +1 -1
  13. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/validateResource.py +1 -1
  14. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8/redfish_interop_validator.egg-info}/PKG-INFO +13 -4
  15. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/setup.py +2 -2
  16. redfish_interop_validator-2.2.7/redfish_interop_validator/profile.py +0 -154
  17. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/AUTHORS.md +0 -0
  18. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/__init__.py +0 -0
  19. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator.egg-info/SOURCES.txt +0 -0
  20. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator.egg-info/dependency_links.txt +0 -0
  21. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator.egg-info/entry_points.txt +0 -0
  22. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator.egg-info/requires.txt +0 -0
  23. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator.egg-info/top_level.txt +0 -0
  24. {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  BSD 3-Clause License
2
2
 
3
- Copyright (c) 2017-2024, Contributing Member(s) of Distributed Management Task
3
+ Copyright (c) 2017-2025, Contributing Member(s) of Distributed Management Task
4
4
  Force, Inc.. All rights reserved.
5
5
 
6
6
  Redistribution and use in source and binary forms, with or without modification,
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: redfish_interop_validator
3
- Version: 2.2.7
3
+ Version: 2.2.8
4
4
  Summary: Redfish Interop Validator
5
5
  Home-page: https://github.com/DMTF/Redfish-Interop-Validator
6
6
  Author: DMTF, https://www.dmtf.org/standards/feedback
@@ -17,8 +17,17 @@ Requires-Dist: requests
17
17
  Requires-Dist: beautifulsoup4>=4.6.0
18
18
  Requires-Dist: lxml
19
19
  Requires-Dist: jsonschema
20
-
21
- Copyright 2017-2024 DMTF. All rights reserved.
20
+ Dynamic: author
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: keywords
26
+ Dynamic: license
27
+ Dynamic: requires-dist
28
+ Dynamic: summary
29
+
30
+ Copyright 2017-2025 DMTF. All rights reserved.
22
31
 
23
32
  # Redfish Interop Validator
24
33
 
@@ -1,4 +1,4 @@
1
- Copyright 2017-2024 DMTF. All rights reserved.
1
+ Copyright 2017-2025 DMTF. All rights reserved.
2
2
 
3
3
  # Redfish Interop Validator
4
4
 
@@ -1,6 +1,6 @@
1
1
 
2
2
  # Copyright Notice:
3
- # Copyright 2017-2024 DMTF. All rights reserved.
3
+ # Copyright 2017-2025 DMTF. All rights reserved.
4
4
  # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Interop-Validator/blob/master/LICENSE.md
5
5
 
6
6
  import os
@@ -17,7 +17,7 @@ import redfish_interop_validator.traverseInterop as traverseInterop
17
17
  from redfish_interop_validator.profile import getProfiles, checkProfileAgainstSchema, hashProfile
18
18
  from redfish_interop_validator.validateResource import validateSingleURI, validateURITree
19
19
 
20
- tool_version = '2.2.7'
20
+ tool_version = '2.2.8'
21
21
 
22
22
  # Set up the custom debug levels
23
23
  VERBOSE1 = logging.INFO - 1
@@ -90,7 +90,7 @@ def main(argslist=None, configfile=None):
90
90
  if configfile is None:
91
91
  configfile = args.config
92
92
 
93
- startTick = datetime.now()
93
+ start_tick = datetime.now()
94
94
 
95
95
  # Set logging file
96
96
  standard_out.setLevel(logging.INFO - args.verbose if args.verbose < 3 else logging.DEBUG)
@@ -101,7 +101,7 @@ def main(argslist=None, configfile=None):
101
101
  os.makedirs(logpath)
102
102
 
103
103
  fmt = logging.Formatter('%(levelname)s - %(message)s')
104
- file_handler = logging.FileHandler(datetime.strftime(startTick, os.path.join(logpath, "InteropLog_%m_%d_%Y_%H%M%S.txt")))
104
+ file_handler = logging.FileHandler(datetime.strftime(start_tick, os.path.join(logpath, "InteropLog_%m_%d_%Y_%H%M%S.txt")))
105
105
  file_handler.setLevel(min(logging.INFO if not args.debugging else logging.DEBUG, standard_out.level))
106
106
  file_handler.setFormatter(fmt)
107
107
  my_logger.addHandler(file_handler)
@@ -121,7 +121,7 @@ def main(argslist=None, configfile=None):
121
121
  else:
122
122
  from redfish_interop_validator.config import convert_args_to_config
123
123
  my_logger.info('Writing config file to log directory')
124
- configfilename = datetime.strftime(startTick, os.path.join(logpath, "ConfigFile_%m_%d_%Y_%H%M%S.ini"))
124
+ configfilename = datetime.strftime(start_tick, os.path.join(logpath, "ConfigFile_%m_%d_%Y_%H%M%S.ini"))
125
125
  my_config = convert_args_to_config(args)
126
126
  with open(configfilename, 'w') as f:
127
127
  my_config.write(f)
@@ -144,7 +144,7 @@ def main(argslist=None, configfile=None):
144
144
  my_logger.info('Target URI: ' + args.ip)
145
145
  my_logger.info('\n'.join(
146
146
  ['{}: {}'.format(x, vars(args)[x] if x not in ['password'] else '******') for x in sorted(list(vars(args).keys() - set(['description']))) if vars(args)[x] not in ['', None]]))
147
- my_logger.info('Start time: ' + startTick.strftime('%x - %X'))
147
+ my_logger.info('Start time: ' + start_tick.strftime('%x - %X'))
148
148
  my_logger.info("")
149
149
 
150
150
  # Start our service
@@ -162,7 +162,7 @@ def main(argslist=None, configfile=None):
162
162
  my_uuid = currentService.service_root.get('UUID', 'No UUID')
163
163
  setattr(args, 'description', 'My Target System {}, version {}, {}'.format(my_name, my_version, my_uuid))
164
164
 
165
- my_logger.info('Description of service: {}'.format(args.description))
165
+ my_logger.info('Description of service: {}\n'.format(args.description))
166
166
 
167
167
  # Interop Profile handling
168
168
  my_profiles = []
@@ -183,17 +183,12 @@ def main(argslist=None, configfile=None):
183
183
 
184
184
  if args.required_profiles_dir is not None:
185
185
  my_paths += glob.glob("{}/**/".format(args.required_profiles_dir), recursive=True)
186
-
187
- # Create a list of profiles, required imports, and show their hashes
188
- all_profiles = []
189
- for name, profile in my_profiles:
190
- all_profiles.extend(getProfiles(profile, [os.getcwd()] + my_paths, online=args.online_profiles))
191
-
192
- my_logger.info('\nProfile Hashes: ')
193
- for profile in all_profiles:
194
- profileName = profile.get('ProfileName')
195
- profileVersion = profile.get('ProfileVersion')
196
- my_logger.info('profile: {} {}, dict md5 hash: {}'.format(profileName, profileVersion, hashProfile(profile)))
186
+
187
+ my_logger.info('Profile Hashes (run-time): ')
188
+ for file_name, profile in my_profiles:
189
+ profile_name = profile.get('ProfileName')
190
+ profile_version = profile.get('ProfileVersion')
191
+ my_logger.info('profile: {} {} {}, dict md5 hash: {}'.format(file_name, profile_name, profile_version, hashProfile(profile)))
197
192
 
198
193
  # Start main
199
194
  status_code = 1
@@ -220,28 +215,55 @@ def main(argslist=None, configfile=None):
220
215
 
221
216
  try:
222
217
  results = None
223
- for profile in all_profiles:
224
- if 'single' in pmode:
225
- success, _, resultsNew, _, _ = validateSingleURI(ppath, profile, 'Target', expectedJson=jsonData)
226
- elif 'tree' in pmode:
227
- success, _, resultsNew, _, _ = validateURITree(ppath, profile, 'Target', expectedJson=jsonData)
228
- else:
229
- success, _, resultsNew, _, _ = validateURITree('/redfish/v1/', profile, 'ServiceRoot', expectedJson=jsonData)
230
- profileName = profile.get('ProfileName')
231
- if results is None:
232
- results = resultsNew
233
- else:
234
- for item in resultsNew:
235
- for x in resultsNew[item]['messages']:
236
- x.name = profileName + ' -- ' + x.name
237
- if item in results:
238
- innerCounts = results[item]['counts']
239
- innerCounts.update(resultsNew[item]['counts'])
240
- results[item]['messages'].extend(resultsNew[item]['messages'])
241
- else:
242
- results[item] = resultsNew[item]
243
- # resultsNew = {profileName+key: resultsNew[key] for key in resultsNew if key in results}
244
- # results.update(resultsNew)
218
+ processed_profiles = set()
219
+ for file_name, profile in my_profiles:
220
+ profile_name = profile.get('ProfileName')
221
+ profile_version = profile.get('ProfileVersion')
222
+
223
+ # Create a list of profiles, required imports, and show their hashes
224
+ included_profiles, required_by_resource = getProfiles(profile, [os.getcwd()] + my_paths, online=args.online_profiles)
225
+
226
+ all_profiles = [profile] + included_profiles
227
+
228
+ my_logger.info('Profile Hashes (included by {}): '.format(file_name))
229
+ for inner_profile in included_profiles:
230
+ inner_profile_name = profile.get('ProfileName')
231
+ inner_profile_version = profile.get('ProfileVersion')
232
+ my_logger.info('\t{} {}, dict md5 hash: {}'.format(inner_profile_name, inner_profile_version, hashProfile(inner_profile)))
233
+
234
+ my_logger.info('Profile Hashes (required by Resource): '.format(file_name))
235
+ for inner_profile in required_by_resource:
236
+ inner_profile_name = profile.get('ProfileName')
237
+ inner_profile_version = profile.get('ProfileVersion')
238
+ my_logger.info('\t{} {}, dict md5 hash: {}'.format(inner_profile_name, inner_profile_version, hashProfile(inner_profile)))
239
+
240
+ for profile_to_process in all_profiles:
241
+ processing_profile_name = profile_to_process.get('ProfileName')
242
+ if processing_profile_name not in processed_profiles:
243
+ processed_profiles.add(profile_name)
244
+ else:
245
+ my_logger.warn("Profile {} already processed".format({}))
246
+
247
+ if 'single' in pmode:
248
+ success, _, new_results, _, _ = validateSingleURI(ppath, profile_to_process, 'Target', expectedJson=jsonData)
249
+ elif 'tree' in pmode:
250
+ success, _, new_results, _, _ = validateURITree(ppath, profile_to_process, 'Target', expectedJson=jsonData)
251
+ else:
252
+ success, _, new_results, _, _ = validateURITree('/redfish/v1/', profile_to_process, 'ServiceRoot', expectedJson=jsonData)
253
+ if results is None:
254
+ results = new_results
255
+ else:
256
+ for item_name, item in new_results.items():
257
+ for x in item['messages']:
258
+ x.name = profile_name + ' -- ' + x.name
259
+ if item_name in results:
260
+ inner_counts = results[item_name]['counts']
261
+ inner_counts.update(item['counts'])
262
+ results[item_name]['messages'].extend(item['messages'])
263
+ else:
264
+ results[item_name] = item
265
+ # resultsNew = {profileName+key: resultsNew[key] for key in resultsNew if key in results}
266
+ # results.update(resultsNew)
245
267
  except traverseInterop.AuthenticationError as e:
246
268
  # log authentication error and terminate program
247
269
  my_logger.error('{}'.format(e))
@@ -253,51 +275,51 @@ def main(argslist=None, configfile=None):
253
275
  except Exception as e:
254
276
  my_logger.error('Failed to log out of service; session may still be active ({})'.format(e))
255
277
 
256
- finalCounts = Counter()
257
- nowTick = datetime.now()
258
- my_logger.info('Elapsed time: {}'.format(str(nowTick - startTick).rsplit('.', 1)[0]))
278
+ final_counts = Counter()
279
+ now_tick = datetime.now()
280
+ my_logger.info('Elapsed time: {}'.format(str(now_tick - start_tick).rsplit('.', 1)[0]))
259
281
 
260
282
  for item in results:
261
- innerCounts = results[item]['counts']
283
+ inner_counts = results[item]['counts']
262
284
 
263
285
  # detect if there are error messages for this resource, but no failure counts; if so, add one to the innerCounts
264
286
  counters_all_pass = True
265
- for countType in sorted(innerCounts.keys()):
266
- if innerCounts.get(countType) == 0:
287
+ for count_type in sorted(inner_counts.keys()):
288
+ if inner_counts.get(count_type) == 0:
267
289
  continue
268
- if any(x in countType for x in ['problem', 'fail', 'bad', 'exception']):
290
+ if any(x in count_type for x in ['problem', 'fail', 'bad', 'exception']):
269
291
  counters_all_pass = False
270
- if 'fail' in countType or 'exception' in countType:
271
- my_logger.error('{} {} errors in {}'.format(innerCounts[countType], countType, results[item]['uri']))
272
- innerCounts[countType] += 0
292
+ if 'fail' in count_type or 'exception' in count_type:
293
+ my_logger.error('{} {} errors in {}'.format(inner_counts[count_type], count_type, results[item]['uri']))
294
+ inner_counts[count_type] += 0
273
295
  error_messages_present = False
274
296
  if results[item]['errors'] is not None and len(results[item]['errors']) > 0:
275
297
  error_messages_present = True
276
298
  if results[item]['warns'] is not None and len(results[item]['warns']) > 0:
277
- innerCounts['warningPresent'] = 1
299
+ inner_counts['warningPresent'] = 1
278
300
  if counters_all_pass and error_messages_present:
279
- innerCounts['failErrorPresent'] = 1
301
+ inner_counts['failErrorPresent'] = 1
280
302
 
281
- finalCounts.update(results[item]['counts'])
303
+ final_counts.update(results[item]['counts'])
282
304
 
283
305
  import redfish_interop_validator.tohtml as tohtml
284
306
 
285
307
  fails = 0
286
- for key in [key for key in finalCounts.keys()]:
287
- if finalCounts[key] == 0:
288
- del finalCounts[key]
308
+ for key in [key for key in final_counts.keys()]:
309
+ if final_counts[key] == 0:
310
+ del final_counts[key]
289
311
  continue
290
312
  if any(x in key for x in ['problem', 'fail', 'bad', 'exception']):
291
- fails += finalCounts[key]
313
+ fails += final_counts[key]
292
314
 
293
- html_str = tohtml.renderHtml(results, finalCounts, tool_version, startTick, nowTick, currentService.config)
315
+ html_str = tohtml.renderHtml(results, final_counts, tool_version, start_tick, now_tick, currentService.config)
294
316
 
295
- lastResultsPage = datetime.strftime(startTick, os.path.join(logpath, "InteropHtmlLog_%m_%d_%Y_%H%M%S.html"))
317
+ lastResultsPage = datetime.strftime(start_tick, os.path.join(logpath, "InteropHtmlLog_%m_%d_%Y_%H%M%S.html"))
296
318
 
297
319
  tohtml.writeHtml(html_str, lastResultsPage)
298
320
 
299
321
  success = success and not (fails > 0)
300
- my_logger.info("\n".join('{}: {} '.format(x, y) for x, y in sorted(finalCounts.items())))
322
+ my_logger.info("\n".join('{}: {} '.format(x, y) for x, y in sorted(final_counts.items())))
301
323
 
302
324
  # dump cache info to debug log
303
325
  my_logger.debug('callResourceURI() -> {}'.format(currentService.callResourceURI.cache_info()))
@@ -1,5 +1,5 @@
1
1
  # Copyright Notice:
2
- # Copyright 2017-2024 DMTF. All rights reserved.
2
+ # Copyright 2017-2025 DMTF. All rights reserved.
3
3
  # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Service-Validator/blob/master/LICENSE.md
4
4
 
5
5
  """
@@ -1,5 +1,5 @@
1
1
  # Copyright Notice:
2
- # Copyright 2017-2024 DMTF. All rights reserved.
2
+ # Copyright 2017-2025 DMTF. All rights reserved.
3
3
  # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Service-Validator/blob/master/LICENSE.md
4
4
 
5
5
  import configparser
@@ -1,6 +1,6 @@
1
1
 
2
2
  # Copyright Notice:
3
- # Copyright 2017-2024 DMTF. All rights reserved.
3
+ # Copyright 2017-2025 DMTF. All rights reserved.
4
4
  # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Interop-Validator/blob/master/LICENSE.md
5
5
 
6
6
  import re
@@ -0,0 +1,202 @@
1
+
2
+ # Copyright Notice:
3
+ # Copyright 2017-2025 DMTF. All rights reserved.
4
+ # License: BSD 3-Clause License. For full text see link:
5
+ # https://github.com/DMTF/Redfish-Service-Validator/LICENSE.md
6
+
7
+ import os
8
+ import re
9
+ import glob
10
+ import json
11
+ import logging
12
+
13
+ from urllib.request import urlopen
14
+ from collections.abc import Mapping
15
+
16
+ from redfish_interop_validator.redfish import splitVersionString, versionpattern
17
+
18
+ my_logger = logging.getLogger()
19
+ my_logger.setLevel(logging.DEBUG)
20
+
21
+
22
+ def hashProfile(profile):
23
+ from hashlib import md5
24
+ my_md5 = md5(json.dumps(profile, sort_keys=True).encode())
25
+ return my_md5.hexdigest()
26
+
27
+
28
+ def checkProfileAgainstSchema(profile, schema):
29
+ """
30
+ Checks if a profile is conformant
31
+ """
32
+ # what is required in a profile? use the json schema
33
+ import jsonschema
34
+ try:
35
+ jsonschema.validate(profile, schema)
36
+ except jsonschema.ValidationError as e:
37
+ my_logger.exception(e)
38
+ my_logger.info('ValidationError')
39
+ return False
40
+ except jsonschema.SchemaError as e:
41
+ my_logger.exception(e)
42
+ my_logger.info('SchemaError')
43
+ return False
44
+ # consider @odata.type, with regex
45
+ return True
46
+
47
+
48
+ defaultrepository = 'http://redfish.dmtf.org/profiles'
49
+
50
+
51
+ def getProfilesMatchingName(name, directories):
52
+ pattern = r'\.{}\.'.format(versionpattern)
53
+ filepattern = re.compile(pattern.join(name.split('.')) + "|{}".format(name.replace('.', '\.')))
54
+ for dirname in directories:
55
+ for file in glob.glob(os.path.join(dirname, '*.json')):
56
+ if filepattern.match(os.path.basename(file)):
57
+ yield file
58
+
59
+ def dict_merge(dct, merge_dct):
60
+ """
61
+ https://gist.github.com/angstwad/bf22d1822c38a92ec0a9 modified
62
+ Recursive dict merge. Inspired by :meth:``dict.update()``, instead of
63
+ updating only top-level keys, dict_merge recurses down into dicts nested
64
+ to an arbitrary depth, updating keys. The ``merge_dct`` is merged into
65
+ ``dct``.
66
+ :param dct: dict onto which the merge is executed
67
+ :param merge_dct: dct merged into dct
68
+ :return: None
69
+ """
70
+ for k in merge_dct:
71
+ if (k in dct and isinstance(dct[k], dict)
72
+ and isinstance(merge_dct[k], Mapping)):
73
+ dict_merge(dct[k], merge_dct[k])
74
+ else:
75
+ dct[k] = merge_dct[k]
76
+
77
+
78
+ def updateWithProfile(profile, data):
79
+ dict_merge(data, profile)
80
+ return data
81
+
82
+
83
+ def getProfileFromRepo(profilename, repo=None):
84
+ try:
85
+ if repo is None:
86
+ repo = 'http://redfish.dmtf.org/profiles'
87
+
88
+ urlpath = urlopen(repo)
89
+ string = urlpath.read().decode('utf-8')
90
+
91
+ pattern = r'\.{}\.'.format(versionpattern)
92
+ filepattern = re.compile(pattern.join(profilename.split('.')))
93
+
94
+ filelist = filepattern.findall(string)
95
+
96
+ profilename = None
97
+ for filename in filelist:
98
+ filename = filename[:-1]
99
+ if profilename is None:
100
+ profilename = filename
101
+ continue
102
+ profilename = max(profilename, filename)
103
+ if profilename is None:
104
+ return None
105
+
106
+ remotefile = urlopen(repo + '/' + profilename)
107
+ return remotefile.read()
108
+ except Exception as e:
109
+ print(e)
110
+ return None
111
+
112
+ # Presumes the cache does not need to handle multiple of the same profile with different versions
113
+ profile_cache = {}
114
+
115
+ def parseProfileInclude(target_name, target_profile_info, directories, online):
116
+ # Grab data of profile from online or locally
117
+
118
+ min_version = splitVersionString(target_profile_info.get('MinVersion', '1.0.0'))
119
+ target_version = 'v{}_{}_{}'.format(*min_version)
120
+ target_file = '{}.{}'.format(target_name, 'json')
121
+
122
+ if target_file in profile_cache:
123
+ return profile_cache[target_file]
124
+
125
+ # get max filename
126
+ repo = target_profile_info.get('Repository')
127
+ if online:
128
+ data = getProfileFromRepo(target_file, repo)
129
+ else:
130
+ data = None
131
+
132
+ if data is None:
133
+ target_list = list(getProfilesMatchingName(target_file, directories))
134
+ if len(target_list) > 0:
135
+ max_version = (1,0,0)
136
+ for target_name in target_list:
137
+ with open(target_name) as f:
138
+ my_profile = json.load(f)
139
+ my_version = splitVersionString(my_profile.get('ProfileVersion', '1.0.0'))
140
+ max_version = max(max_version, my_version)
141
+ if my_version == max_version or data is None:
142
+ data = my_profile
143
+ if min_version > max_version:
144
+ my_logger.warning('File version smaller than target MinVersion')
145
+ else:
146
+ my_logger.error('Could not acquire this profile {} {}'.format(target_name, repo))
147
+ data = None
148
+
149
+ profile_cache[target_file] = data
150
+ return data
151
+
152
+
153
+ def getProfiles(profile, directories, chain=None, online=False):
154
+ profile_includes, required_by_resource = [], []
155
+
156
+ # Prevent cyclical imports when possible
157
+ profile_name = profile.get('ProfileName')
158
+ if chain is None:
159
+ chain = []
160
+ if profile_name in chain:
161
+ my_logger.error('Suspected duplicate/cyclical import error: {} {}'.format(chain, profile_name))
162
+ return [], []
163
+ chain.append(profile_name)
164
+
165
+ # Gather all included profiles, these are each run independently in validateResource.
166
+ # TODO: Process them simultaneously in validateResource, to avoid polling the target machine multiple times
167
+ required_profiles = profile.get('RequiredProfiles', {})
168
+ for target_name, target_profile_info in required_profiles.items():
169
+ profile_data = parseProfileInclude(target_name, target_profile_info, directories, online)
170
+
171
+ if profile_data:
172
+ profile_includes.append(profile_data)
173
+
174
+ inner_includes, inner_reqs = getProfiles(profile_data, directories, chain)
175
+ profile_includes.extend(inner_includes)
176
+ required_by_resource.extend(inner_reqs)
177
+
178
+ # Process all RequiredResourceProfile by modifying profiles
179
+ profile_resources = profile.get('Resources')
180
+
181
+ for resource_name, resource in profile_resources.items():
182
+ # Modify just the resource or its UseCases. Should not have concurrent UseCases and RequiredResourceProfile in Resource
183
+ if 'UseCases' not in resource:
184
+ modifying_objects = [resource]
185
+ else:
186
+ modifying_objects = resource['UseCases']
187
+ # Use same format as RequiredProfiles
188
+ for inner_object in modifying_objects:
189
+ required_profiles = inner_object.get('RequiredResourceProfile', {})
190
+ for target_name, target_profile_info in required_profiles.items():
191
+ profile_data = parseProfileInclude(target_name, target_profile_info, directories, online)
192
+
193
+ if profile_data:
194
+ target_resources = profile_data.get('Resources')
195
+ # Merge if our data exists
196
+ if resource_name in target_resources:
197
+ dict_merge(inner_object, target_resources[resource_name])
198
+ required_by_resource.append(profile_data)
199
+ else:
200
+ my_logger.error('Import {} does not have Resource {}'.format(target_name, resource_name))
201
+
202
+ return profile_includes, required_by_resource
@@ -1,5 +1,5 @@
1
1
  # Copyright Notice:
2
- # Copyright 2017-2024 DMTF. All rights reserved.
2
+ # Copyright 2017-2025 DMTF. All rights reserved.
3
3
  # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Service-Validator/blob/master/LICENSE.md
4
4
 
5
5
  import re
@@ -1,5 +1,5 @@
1
1
  # Copyright Notice:
2
- # Copyright 2017-2024 DMTF. All rights reserved.
2
+ # Copyright 2017-2025 DMTF. All rights reserved.
3
3
  # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Service-Validator/blob/master/LICENSE.md
4
4
 
5
5
  from datetime import datetime, timedelta
@@ -1,6 +1,6 @@
1
1
 
2
2
  # Copyright Notice:
3
- # Copyright 2017-2024 DMTF. All rights reserved.
3
+ # Copyright 2017-2025 DMTF. All rights reserved.
4
4
  # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Interop-Validator/blob/master/LICENSE.md
5
5
 
6
6
 
@@ -1,5 +1,5 @@
1
1
  # Copyright Notice:
2
- # Copyright 2017-2024 DMTF. All rights reserved.
2
+ # Copyright 2017-2025 DMTF. All rights reserved.
3
3
  # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Service-Validator/blob/master/LICENSE.md
4
4
 
5
5
  import requests
@@ -1,5 +1,5 @@
1
1
  # Copyright Notice:
2
- # Copyright 2017-2024 DMTF. All rights reserved.
2
+ # Copyright 2017-2025 DMTF. All rights reserved.
3
3
  # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Service-Validator/blob/master/LICENSE.md
4
4
 
5
5
  import logging
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: redfish_interop_validator
3
- Version: 2.2.7
3
+ Version: 2.2.8
4
4
  Summary: Redfish Interop Validator
5
5
  Home-page: https://github.com/DMTF/Redfish-Interop-Validator
6
6
  Author: DMTF, https://www.dmtf.org/standards/feedback
@@ -17,8 +17,17 @@ Requires-Dist: requests
17
17
  Requires-Dist: beautifulsoup4>=4.6.0
18
18
  Requires-Dist: lxml
19
19
  Requires-Dist: jsonschema
20
-
21
- Copyright 2017-2024 DMTF. All rights reserved.
20
+ Dynamic: author
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: keywords
26
+ Dynamic: license
27
+ Dynamic: requires-dist
28
+ Dynamic: summary
29
+
30
+ Copyright 2017-2025 DMTF. All rights reserved.
22
31
 
23
32
  # Redfish Interop Validator
24
33
 
@@ -1,5 +1,5 @@
1
1
  # Copyright Notice:
2
- # Copyright 2017-2024 DMTF. All rights reserved.
2
+ # Copyright 2017-2025 DMTF. All rights reserved.
3
3
  # License: BSD 3-Clause License. For full text see link:
4
4
  # https://github.com/DMTF/Redfish-Interop-Validator/blob/main/LICENSE.md
5
5
 
@@ -11,7 +11,7 @@ with open("README.md", "r", "utf-8") as f:
11
11
 
12
12
  setup(
13
13
  name="redfish_interop_validator",
14
- version="2.2.7",
14
+ version="2.2.8",
15
15
  description="Redfish Interop Validator",
16
16
  long_description=long_description,
17
17
  long_description_content_type="text/markdown",
@@ -1,154 +0,0 @@
1
-
2
- # Copyright Notice:
3
- # Copyright 2017-2024 DMTF. All rights reserved.
4
- # License: BSD 3-Clause License. For full text see link:
5
- # https://github.com/DMTF/Redfish-Service-Validator/LICENSE.md
6
-
7
- import os
8
- import re
9
- import glob
10
- import json
11
- import logging
12
-
13
- from redfish_interop_validator.redfish import splitVersionString, versionpattern
14
-
15
- my_logger = logging.getLogger()
16
- my_logger.setLevel(logging.DEBUG)
17
-
18
- from urllib.request import urlopen
19
-
20
- def hashProfile(profile):
21
- from hashlib import md5
22
- my_md5 = md5(json.dumps(profile, sort_keys=True).encode())
23
- return my_md5.hexdigest()
24
-
25
- def checkProfileAgainstSchema(profile, schema):
26
- """
27
- Checks if a profile is conformant
28
- """
29
- # what is required in a profile? use the json schema
30
- import jsonschema
31
- try:
32
- jsonschema.validate(profile, schema)
33
- except jsonschema.ValidationError as e:
34
- my_logger.exception(e)
35
- my_logger.info('ValidationError')
36
- return False
37
- except jsonschema.SchemaError as e:
38
- my_logger.exception(e)
39
- my_logger.info('SchemaError')
40
- return False
41
- # consider @odata.type, with regex
42
- return True
43
-
44
-
45
- defaultrepository = 'http://redfish.dmtf.org/profiles'
46
-
47
- def getProfilesMatchingName(name, directories):
48
- pattern = r'\.{}\.'.format(versionpattern)
49
- filepattern = re.compile(pattern.join(name.split('.')) + "|{}".format(name.replace('.', '\.')))
50
- for dirname in directories:
51
- for file in glob.glob(os.path.join(dirname, '*.json')):
52
- if filepattern.match(os.path.basename(file)):
53
- yield file
54
-
55
- def dict_merge(dct, merge_dct):
56
- """
57
- https://gist.github.com/angstwad/bf22d1822c38a92ec0a9 modified
58
- Recursive dict merge. Inspired by :meth:``dict.update()``, instead of
59
- updating only top-level keys, dict_merge recurses down into dicts nested
60
- to an arbitrary depth, updating keys. The ``merge_dct`` is merged into
61
- ``dct``.
62
- :param dct: dict onto which the merge is executed
63
- :param merge_dct: dct merged into dct
64
- :return: None
65
- """
66
- from collections import Mapping
67
- for k in merge_dct:
68
- if (k in dct and isinstance(dct[k], dict)
69
- and isinstance(merge_dct[k], Mapping)):
70
- dict_merge(dct[k], merge_dct[k])
71
- else:
72
- dct[k] = merge_dct[k]
73
-
74
- def updateWithProfile(profile, data):
75
- dict_merge(data, profile)
76
- return data
77
-
78
- def getProfileFromRepo(profilename, repo=None):
79
- try:
80
- if repo is None:
81
- repo = 'http://redfish.dmtf.org/profiles'
82
-
83
- urlpath = urlopen(repo)
84
- string = urlpath.read().decode('utf-8')
85
-
86
- pattern = r'\.{}\.'.format(versionpattern)
87
- filepattern = re.compile(pattern.join(profilename.split('.')))
88
-
89
- filelist = filepattern.findall(string)
90
-
91
- profilename = None
92
- for filename in filelist:
93
- filename = filename[:-1]
94
- if profilename is None:
95
- profilename = filename
96
- continue
97
- profilename = max(profilename, filename)
98
- if profilename is None:
99
- return None
100
-
101
- remotefile = urlopen(repo + '/' + profilename)
102
- return remotefile.read()
103
- except Exception as e:
104
- print(e)
105
- return None
106
-
107
-
108
- def getProfiles(profile, directories, chain=None, online=False):
109
- alldata = [profile]
110
- if 'RequiredProfiles' not in profile:
111
- my_logger.debug('No such item RequiredProfiles')
112
- else:
113
- profileName = profile.get('ProfileName')
114
- if chain is None:
115
- chain = []
116
- if profileName in chain:
117
- my_logger.error('Suspected duplicate/cyclical import error: {} {}'.format(chain, profileName))
118
- return []
119
- chain.append(profileName)
120
-
121
- requiredProfiles = profile['RequiredProfiles']
122
- for item in requiredProfiles:
123
- targetName = item
124
- rp = requiredProfiles[targetName]
125
- min_version = splitVersionString(rp.get('MinVersion', '1.0.0'))
126
- targetVersion = 'v{}_{}_{}'.format(*min_version)
127
- targetFile = '{}.{}'.format(targetName, 'json')
128
-
129
- # get max filename
130
- repo = rp.get('Repository')
131
- if online:
132
- data = getProfileFromRepo(targetFile, repo)
133
- else:
134
- data = None
135
-
136
- if data is None:
137
- targetList = list(getProfilesMatchingName(targetFile, directories))
138
- if len(targetList) > 0:
139
- max_version = (1,0,0)
140
- for item in targetList:
141
- with open(item) as f:
142
- my_profile = json.load(f)
143
- my_version = splitVersionString(my_profile.get('ProfileVersion', '1.0.0'))
144
- max_version = max(max_version, my_version)
145
- if my_version == max_version or data == None:
146
- data = my_profile
147
- if min_version > max_version:
148
- my_logger.warning('File version smaller than target MinVersion')
149
- else:
150
- my_logger.error('Could not acquire this profile {} {}'.format(targetName, repo))
151
- continue
152
-
153
- alldata.extend(getProfiles(data, directories, chain))
154
- return alldata