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
65import sys
6+ import os
77import argparse
88import logging
99import json
1010from datetime import datetime
11- import traceback
11+ from urllib .parse import urlparse
12+ from collections import Counter
13+
1214from redfish_service_validator .metadata import getSchemaDetails
1315from redfish_service_validator .config import convert_config_to_args , convert_args_to_config
1416from 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
1919tool_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-
4921def 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 ('\n Elapsed time: {}' .format (str (nowTick - startTick ).rsplit ('.' , 1 )[0 ]))
192+ my_logger .info ('\n Elapsed 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 ( ' \n Listing 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 ("\n Results 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+
256252def main ():
257253 """
258254 Entry point for the program.
259255 """
260256 status_code , _ , _ = validate ()
261257 return status_code
262258
259+
263260if __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