Skip to content

Commit c6f5b40

Browse files
authored
Merge pull request #610 from DMTF/log-update
Updates to Log output and associated functions
2 parents c26a40d + cf21aa6 commit c6f5b40

File tree

13 files changed

+643
-685
lines changed

13 files changed

+643
-685
lines changed

RedfishServiceValidator.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
# License: BSD 3-Clause License. For full text see link:
44
# https://github.com/DMTF/Redfish-Service-Validator/blob/main/LICENSE.md
55

6-
from redfish_service_validator.RedfishServiceValidator import main, my_logger
76
import sys
7+
import logging
8+
from redfish_service_validator.RedfishServiceValidator import main
9+
10+
my_logger = logging.getLogger('rsv')
11+
my_logger.setLevel(logging.DEBUG)
812

913
if __name__ == '__main__':
1014
try:

redfish_service_validator/RedfishServiceValidator.py

Lines changed: 83 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,28 @@
22
# Copyright 2016-2025 DMTF. All rights reserved.
33
# License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Service-Validator/blob/main/LICENSE.md
44

5-
import os
65
import sys
6+
import os
77
import argparse
88
import logging
99
import json
1010
from datetime import datetime
11-
import traceback
11+
from urllib.parse import urlparse
12+
from collections import Counter
13+
1214
from redfish_service_validator.metadata import getSchemaDetails
1315
from redfish_service_validator.config import convert_config_to_args, convert_args_to_config
1416
from redfish_service_validator.validateResource import validateSingleURI, validateURITree
15-
from redfish_service_validator import tohtml, schema_pack, traverse
16-
from urllib.parse import urlparse
17-
from collections import Counter
17+
from redfish_service_validator import tohtml, schema_pack, traverse, logger
1818

1919
tool_version = '2.5.0'
2020

21-
# Set up the custom debug levels
22-
VERBOSE1 = logging.INFO-1
23-
VERBOSE2 = logging.INFO-2
24-
25-
logging.addLevelName(VERBOSE1, "VERBOSE1")
26-
logging.addLevelName(VERBOSE2, "VERBOSE2")
27-
28-
29-
def print_verbose_1(self, msg, *args, **kwargs):
30-
if self.isEnabledFor(VERBOSE1):
31-
self._log(VERBOSE1, msg, args, **kwargs)
32-
33-
34-
def print_verbose_2(self, msg, *args, **kwargs):
35-
if self.isEnabledFor(VERBOSE2):
36-
self._log(VERBOSE2, msg, args, **kwargs)
37-
38-
39-
logging.Logger.verbose1 = print_verbose_1
40-
logging.Logger.verbose2 = print_verbose_2
41-
42-
my_logger = logging.getLogger()
43-
my_logger.setLevel(logging.DEBUG)
44-
standard_out = logging.StreamHandler(sys.stdout)
45-
standard_out.setLevel(logging.INFO)
46-
my_logger.addHandler(standard_out)
47-
48-
4921
def validate(argslist=None, configfile=None):
5022
"""Main command
5123
5224
Args:
5325
argslist ([type], optional): List of arguments in the form of argv. Defaults to None.
54-
"""
26+
"""
5527
argget = argparse.ArgumentParser(description='DMTF tool to test a service against a collection of Schema, version {}'.format(tool_version))
5628

5729
# base tool
@@ -90,58 +62,60 @@ def validate(argslist=None, configfile=None):
9062
configfile = args.config
9163

9264
# set logging file
93-
startTick = datetime.now()
65+
start_tick = datetime.now()
9466

95-
standard_out.setLevel(logging.INFO - args.verbose if args.verbose < 3 else logging.DEBUG)
67+
logger.set_standard_out(logger.Level.INFO - args.verbose if args.verbose < 3 else logger.Level.DEBUG)
9668

9769
logpath = args.logdir
9870

9971
if not os.path.isdir(logpath):
10072
os.makedirs(logpath)
10173

102-
fmt = logging.Formatter('%(levelname)s - %(message)s')
103-
file_handler = logging.FileHandler(datetime.strftime(startTick, os.path.join(logpath, "ConformanceLog_%m_%d_%Y_%H%M%S.txt")))
104-
file_handler.setLevel(min(logging.INFO if not args.debugging else logging.DEBUG, standard_out.level))
105-
file_handler.setFormatter(fmt)
106-
my_logger.addHandler(file_handler)
74+
log_level = logger.Level.INFO if not args.debugging else logger.Level.DEBUG
75+
file_name = datetime.strftime(start_tick, os.path.join(logpath, "ConformanceLog_%m_%d_%Y_%H%M%S.txt"))
76+
77+
logger.create_logging_file_handler(log_level, file_name)
78+
79+
my_logger = logging.getLogger('rsv')
80+
my_logger.setLevel(logging.DEBUG)
10781

10882
# begin logging
10983
my_logger.info("Redfish Service Validator, version {}".format(tool_version))
11084
my_logger.info("")
11185

11286
# config verification
11387
if args.ip is None and configfile is None:
114-
my_logger.error('No IP or Config Specified')
88+
my_logger.error('Configuration Error: No IP or Config Specified')
11589
argget.print_help()
11690
return 1, None, 'Configuration Incomplete'
11791

11892
if configfile:
11993
convert_config_to_args(args, configfile)
12094
else:
12195
my_logger.info('Writing config file to log directory')
122-
configfilename = datetime.strftime(startTick, os.path.join(logpath, "ConfigFile_%m_%d_%Y_%H%M%S.ini"))
96+
configfilename = datetime.strftime(start_tick, os.path.join(logpath, "ConfigFile_%m_%d_%Y_%H%M%S.ini"))
12397
my_config = convert_args_to_config(args)
12498
with open(configfilename, 'w') as f:
12599
my_config.write(f)
126100

127-
scheme, netloc, path, params, query, fragment = urlparse(args.ip)
101+
scheme, netloc, _path, _params, _query, _fragment = urlparse(args.ip)
128102
if scheme not in ['http', 'https', 'http+unix']:
129-
my_logger.error('IP is missing http or https or http+unix')
103+
my_logger.error('Configuration Error: IP is missing http or https or http+unix')
130104
return 1, None, 'IP Incomplete'
131105

132106
if netloc == '':
133-
my_logger.error('IP is missing ip/host')
107+
my_logger.error('Configuration Error: IP is missing ip/host')
134108
return 1, None, 'IP Incomplete'
135109

136110
if len(args.collectionlimit) % 2 != 0:
137-
my_logger.error('Collection Limit requires two arguments per entry (ResourceType Count)')
111+
my_logger.error('Configuration Error: Collection Limit requires two arguments per entry (ResourceType Count)')
138112
return 1, None, 'Collection Limit Incomplete'
139113

140114
# start printing config details, remove redundant/private info from print
141-
my_logger.info('Target URI: ' + args.ip)
115+
my_logger.info('Target URI: {}'.format(args.ip))
142116
my_logger.info('\n'.join(
143117
['{}: {}'.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]]))
144-
my_logger.info('Start time: ' + startTick.strftime('%x - %X'))
118+
my_logger.info('Start time: {}'.format(start_tick.strftime('%x - %X')))
145119
my_logger.info("")
146120

147121
# schema and service init
@@ -150,29 +124,27 @@ def validate(argslist=None, configfile=None):
150124
if not os.path.isdir(schemadir):
151125
my_logger.info('Downloading initial schemas from online')
152126
my_logger.info('The tool will, by default, attempt to download and store XML files to relieve traffic from DMTF/service')
153-
schema_pack.my_logger.addHandler(file_handler)
154127
schema_pack.setup_schema_pack('latest', args.schema_directory, args.ext_http_proxy, args.ext_https_proxy)
155128

156129
try:
157130
currentService = traverse.rfService(vars(args))
158131
except Exception as ex:
159-
traceback.print_exc()
160132
my_logger.verbose1('Exception caught while creating Service', exc_info=1)
161-
my_logger.error("Service could not be started: {}".format(repr(ex)))
133+
my_logger.error("Redfish Service Error: Service could not be started: {}".format(repr(ex)))
162134
my_logger.error("Try running the Redfish Protocol Validator to ensure the service meets basic protocol conformance")
163135
return 1, None, 'Service Exception'
164-
136+
165137
if args.description is None and currentService.service_root:
166-
my_version = currentService.service_root.get('RedfishVersion', 'No Version')
138+
my_version = currentService.service_root.get('RedfishVersion', 'No Given Version')
167139
my_name = currentService.service_root.get('Name', '')
168-
my_uuid = currentService.service_root.get('UUID', 'No UUID')
169-
setattr(args, 'description', 'My Target System {}, version {}, {}'.format(my_name, my_version, my_uuid))
170-
140+
my_uuid = currentService.service_root.get('UUID', 'No Given UUID')
141+
setattr(args, 'description', 'System Under Test - {} version {}, {}'.format(my_name, my_version, my_uuid))
142+
171143
my_logger.info('Description of service: {}'.format(args.description))
172144

173145
# Start main
174146
status_code = 1
175-
jsonData = None
147+
json_data = None
176148

177149
if args.payload:
178150
pmode, ppath = args.payload
@@ -182,25 +154,28 @@ def validate(argslist=None, configfile=None):
182154

183155
if pmode not in ['tree', 'single', 'singlefile', 'treefile', 'default']:
184156
pmode = 'Default'
185-
my_logger.error('PayloadMode or path invalid, using Default behavior')
157+
my_logger.error('Configuration Error: PayloadMode or path invalid, using Default behavior')
186158
if 'file' in pmode:
187159
if ppath is not None and os.path.isfile(ppath):
188160
with open(ppath) as f:
189-
jsonData = json.load(f)
161+
json_data = json.load(f)
190162
f.close()
191163
else:
192-
my_logger.error('File not found for payload: {}'.format(ppath))
164+
my_logger.error('Configuration Error: File not found for payload: {}'.format(ppath))
193165
return 1, None, 'File not found for payload: {}'.format(ppath)
194166
try:
195167
if 'single' in pmode:
196-
success, counts, results, xlinks, topobj = validateSingleURI(currentService, ppath, 'Target', expectedJson=jsonData)
168+
my_logger.push_uri(ppath)
169+
success, my_result, reference_only_links, top_object = validateSingleURI(currentService, ppath, expectedJson=json_data)
170+
results = {'Target': my_result}
171+
my_logger.pop_uri()
197172
elif 'tree' in pmode:
198-
success, counts, results, xlinks, topobj = validateURITree(currentService, ppath, 'Target', expectedJson=jsonData)
173+
success, results, reference_only_links, top_object = validateURITree(currentService, ppath, 'Target', expectedJson=json_data)
199174
else:
200-
success, counts, results, xlinks, topobj = validateURITree(currentService, '/redfish/v1/', 'ServiceRoot', expectedJson=jsonData)
175+
success, results, reference_only_links, top_object = validateURITree(currentService, '/redfish/v1/', 'ServiceRoot', expectedJson=json_data)
201176
except traverse.AuthenticationError as e:
202177
# log authentication error and terminate program
203-
my_logger.error('{}'.format(e))
178+
my_logger.error('Authetication Error: {}'.format(e))
204179
return 1, None, 'Failed to authenticate with the service'
205180

206181
currentService.close()
@@ -211,58 +186,80 @@ def validate(argslist=None, configfile=None):
211186
my_logger.info('Metadata: Namespaces missing from $metadata: {}'.format(metadata.get_missing_namespaces()))
212187

213188
if len(metadata.get_missing_namespaces()) > 0:
214-
my_logger.error('Metadata is missing Namespaces that are referenced by the service.')
189+
my_logger.error('Metadata Error: Metadata is missing Namespaces that are referenced by the service.')
215190

216-
finalCounts = Counter()
217191
nowTick = datetime.now()
218-
my_logger.info('\nElapsed time: {}'.format(str(nowTick-startTick).rsplit('.', 1)[0]))
192+
my_logger.info('\nElapsed time: {}'.format(str(nowTick-start_tick).rsplit('.', 1)[0]))
193+
194+
final_counts = Counter()
219195

220-
error_lines, finalCounts = tohtml.count_errors(results)
196+
my_logger.info('\nListing any warnings and errors: ')
221197

222-
for line in error_lines:
223-
my_logger.error(line)
198+
for k, my_result in results.items():
224199

225-
finalCounts.update(metadata.get_counter())
200+
for record in my_result['records']:
201+
if record.result:
202+
final_counts[record.result] += 1
226203

227-
fails = 0
228-
for key in [key for key in finalCounts.keys()]:
229-
if finalCounts[key] == 0:
230-
del finalCounts[key]
231-
continue
232-
if any(x in key for x in ['problem', 'fail', 'bad', 'exception']):
233-
fails += finalCounts[key]
204+
warns = [x for x in my_result['records'] if x.levelno == logger.Level.WARN]
205+
errors = [x for x in my_result['records'] if x.levelno == logger.Level.ERROR]
206+
if len(warns + errors):
207+
my_logger.info(" ")
208+
my_logger.info(my_result['uri'])
234209

235-
html_str = tohtml.renderHtml(results, tool_version, startTick, nowTick, currentService)
210+
if len(warns):
211+
my_logger.info("Warnings")
212+
for record in warns:
213+
final_counts[record.levelname.lower()] += 1
214+
my_logger.log(record.levelno, ", ".join([x for x in [record.msg, record.result] if x]))
236215

237-
lastResultsPage = datetime.strftime(startTick, os.path.join(logpath, "ConformanceHtmlLog_%m_%d_%Y_%H%M%S.html"))
216+
if len(errors):
217+
my_logger.info("Errors")
218+
for record in errors:
219+
final_counts[record.levelname.lower()] += 1
220+
my_logger.log(record.levelno, ", ".join([x for x in [record.msg, record.result] if x]))
221+
222+
final_counts.update({x: k for x, k in metadata.get_counter().items() if k > 0})
223+
224+
html_str = tohtml.renderHtml(results, tool_version, start_tick, nowTick, currentService)
225+
226+
lastResultsPage = datetime.strftime(start_tick, os.path.join(logpath, "ConformanceHtmlLog_%m_%d_%Y_%H%M%S.html"))
238227

239228
tohtml.writeHtml(html_str, lastResultsPage)
240229

241-
success = success and not (fails > 0)
242-
my_logger.info("\n".join('{}: {} '.format(x, y) for x, y in sorted(finalCounts.items())))
230+
my_logger.info("\nResults Summary:")
231+
my_logger.info(", ".join([
232+
'Pass: {}'.format(final_counts['pass']),
233+
'Fail: {}'.format(final_counts['error']),
234+
'Warning: {}'.format(final_counts['warning']),
235+
]))
243236

244237
# dump cache info to debug log
245238
my_logger.debug('getSchemaDetails() -> {}'.format(getSchemaDetails.cache_info()))
246239
my_logger.debug('callResourceURI() -> {}'.format(currentService.cache_order))
247240

241+
success = final_counts['error'] == 0
242+
248243
if not success:
249-
my_logger.error("Validation has failed: {} problems found".format(fails))
244+
my_logger.error("Validation has failed: {} problems found".format(final_counts['error']))
250245
else:
251246
my_logger.info("Validation has succeeded.")
252247
status_code = 0
253-
248+
254249
return status_code, lastResultsPage, 'Validation done'
255250

251+
256252
def main():
257253
"""
258254
Entry point for the program.
259255
"""
260256
status_code, _, _ = validate()
261257
return status_code
262258

259+
263260
if __name__ == '__main__':
264261
try:
265262
sys.exit(main())
266263
except Exception as e:
267-
my_logger.exception("Program finished prematurely: %s", e)
264+
logger.my_logger.exception("Program finished prematurely: %s", e)
268265
raise

0 commit comments

Comments
 (0)