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.
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/LICENSE.md +1 -1
- {redfish_interop_validator-2.2.7/redfish_interop_validator.egg-info → redfish_interop_validator-2.2.8}/PKG-INFO +13 -4
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/README.md +1 -1
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/RedfishInteropValidator.py +82 -60
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/RedfishLogo.py +1 -1
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/config.py +1 -1
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/interop.py +1 -1
- redfish_interop_validator-2.2.8/redfish_interop_validator/profile.py +202 -0
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/redfish.py +1 -1
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/session.py +1 -1
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/tohtml.py +1 -1
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/traverseInterop.py +1 -1
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/validateResource.py +1 -1
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8/redfish_interop_validator.egg-info}/PKG-INFO +13 -4
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/setup.py +2 -2
- redfish_interop_validator-2.2.7/redfish_interop_validator/profile.py +0 -154
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/AUTHORS.md +0 -0
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator/__init__.py +0 -0
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator.egg-info/SOURCES.txt +0 -0
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator.egg-info/dependency_links.txt +0 -0
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator.egg-info/entry_points.txt +0 -0
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator.egg-info/requires.txt +0 -0
- {redfish_interop_validator-2.2.7 → redfish_interop_validator-2.2.8}/redfish_interop_validator.egg-info/top_level.txt +0 -0
- {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-
|
|
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
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: redfish_interop_validator
|
|
3
|
-
Version: 2.2.
|
|
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
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
# Copyright Notice:
|
|
3
|
-
# Copyright 2017-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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: ' +
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
my_logger.info('Elapsed time: {}'.format(str(
|
|
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
|
-
|
|
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
|
|
266
|
-
if
|
|
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
|
|
290
|
+
if any(x in count_type for x in ['problem', 'fail', 'bad', 'exception']):
|
|
269
291
|
counters_all_pass = False
|
|
270
|
-
if 'fail' in
|
|
271
|
-
my_logger.error('{} {} errors in {}'.format(
|
|
272
|
-
|
|
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
|
-
|
|
299
|
+
inner_counts['warningPresent'] = 1
|
|
278
300
|
if counters_all_pass and error_messages_present:
|
|
279
|
-
|
|
301
|
+
inner_counts['failErrorPresent'] = 1
|
|
280
302
|
|
|
281
|
-
|
|
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
|
|
287
|
-
if
|
|
288
|
-
del
|
|
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 +=
|
|
313
|
+
fails += final_counts[key]
|
|
292
314
|
|
|
293
|
-
html_str = tohtml.renderHtml(results,
|
|
315
|
+
html_str = tohtml.renderHtml(results, final_counts, tool_version, start_tick, now_tick, currentService.config)
|
|
294
316
|
|
|
295
|
-
lastResultsPage = datetime.strftime(
|
|
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(
|
|
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-
|
|
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-
|
|
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-
|
|
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,5 +1,5 @@
|
|
|
1
1
|
# Copyright Notice:
|
|
2
|
-
# Copyright 2017-
|
|
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-
|
|
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
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: redfish_interop_validator
|
|
3
|
-
Version: 2.2.
|
|
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
|
-
|
|
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-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|