deprecated -y option of run_log/TestMain
[tests.git] / system / TestMain.py
1 #!/usr/bin/python3 -u
2
3 # Thierry Parmentelat <thierry.parmentelat@inria.fr>
4 # Copyright (C) 2010 INRIA 
5 #
6 import sys, os, os.path
7 from argparse import ArgumentParser
8 import traceback
9 import readline
10 import glob
11 from datetime import datetime
12
13 import utils
14 from TestPlc import TestPlc, Ignored
15 from TestBonding import TestBonding, onelab_bonding_spec
16 from TestSite import TestSite
17 from TestNode import TestNode
18 from macros import sequences
19
20 # add $HOME in PYTHONPATH so we can import LocalSubstrate.py
21 sys.path.append(os.environ['HOME'])
22 import LocalSubstrate
23
24 class Step:
25
26     natives = TestPlc.__dict__
27
28     def display (self):
29         return self.name.replace('_', '-')
30     def internal (self):
31         return self.name.replace('-', '_')
32
33     def __init__ (self, name):
34         self.name = name
35         # a native step is implemented as a method on TestPlc
36         self.native = self.internal() in Step.natives
37         if self.native:
38             self.method = Step.natives[self.internal()]
39         else:
40             try:
41                 self.substeps = sequences[self.internal()]
42             except Exception as e:
43                 print("macro step {} not found in macros.py ({}) - exiting".format(self.display(),e))
44                 raise
45
46     def print_doc (self, level=0):
47         tab = 32
48         trail = 8
49         if self.native:
50             start = level*' ' + '* '
51             # 2 is the len of '* '
52             width = tab - level - 2
53             format = "%%-%ds" % width
54             line = start + format % self.display()
55             print(line, end=' ')
56             try:
57                 print(self.method.__doc__)
58             except:
59                 print("*** no doc found")
60         else:
61             beg_start = level*' ' + '>>> '
62             end_start = level*' ' + '<<< '
63             trailer = trail * '-'
64             # 4 is the len of '>>> '
65             width = tab - level - 4 - trail
66             format = "%%-%ds" % width
67             beg_line = beg_start + format % self.display() + trail*'>'
68             end_line = end_start + format % self.display() + trail*'<'
69             print(beg_line)
70             for step in self.substeps:
71                 Step(step).print_doc(level+1)
72             print(end_line)
73
74     # return a list of (name, method) for all native steps involved
75     def tuples (self):
76         if self.native:
77             return [ (self.internal(), self.method,) ]
78         else:
79             result = []
80             for substep in [ Step(name) for name in self.substeps ] : 
81                 result += substep.tuples()
82             return result
83
84     # convenience for listing macros
85     # just do a listdir, hoping we're in the right directory...
86     @staticmethod
87     def list_macros ():
88         names= list(sequences.keys())
89         names.sort()
90         return names
91
92 class TestMain:
93
94     default_config = [ 'default' ] 
95
96     default_build_url = "git://git.onelab.eu/tests"
97
98     def __init__(self):
99         self.path = os.path.dirname(sys.argv[0]) or "."
100         os.chdir(self.path)
101
102     def show_env(self, options, message):
103         if self.options.verbose:
104             utils.header(message)
105             utils.show_options("main options", options)
106
107     def init_steps(self):
108         self.steps_message  = ""
109         if not self.options.bonding_build:
110             self.steps_message += 20*'x' + " Defaut steps are\n" + \
111                                   TestPlc.printable_steps(TestPlc.default_steps)
112             self.steps_message += 20*'x' + " Other useful steps are\n" + \
113                                   TestPlc.printable_steps(TestPlc.other_steps)
114             self.steps_message += 20*'x' + " Macro steps are\n" + \
115                                   " ".join(Step.list_macros())
116         else:
117             self.steps_message += 20*'x' + " Default steps with bonding build are\n" + \
118                                   TestPlc.printable_steps(TestPlc.default_bonding_steps)
119
120     def list_steps(self):
121         if not self.options.verbose:
122             print(self.steps_message)
123         else:
124             # steps mentioned on the command line
125             if self.options.steps:
126                 scopes = [("Argument steps",self.options.steps)]
127             else:
128                 scopes = [("Default steps", TestPlc.default_steps)]
129                 if self.options.all_steps:
130                     scopes.append ( ("Other steps", TestPlc.other_steps) )
131                     # try to list macro steps as well
132                     scopes.append ( ("Macro steps", Step.list_macros()) )
133             for (scope, steps) in scopes:
134                 print('--------------------', scope)
135                 for step in [step for step in steps if TestPlc.valid_step(step)]:
136                     try:
137                         (step, qualifier) = step.split('@')
138                     except:
139                         pass
140                     stepname = step
141                     for special in ['force', 'ignore']:
142                         stepname = stepname.replace('_'+special, "")
143                     Step(stepname).print_doc()
144
145     def run (self):
146         usage = """usage: %%prog [options] steps
147 arch-rpms-url defaults to the last value used, as stored in arg-arch-rpms-url,
148    no default
149 config defaults to the last value used, as stored in arg-config,
150    or {}
151 ips_vnode, ips_vplc and ips_qemu defaults to the last value used, 
152    as stored in arg-ips-{{bplc,vplc,bnode,vnode}},
153    default is to use IP scanning
154 steps refer to a method in TestPlc or to a step_* module
155
156 run with -l to see a list of available steps
157 ===
158 """.format(TestMain.default_config)
159
160         parser = ArgumentParser(usage = usage)
161         parser.add_argument("-u", "--url", action="store",  dest="arch_rpms_url", 
162                             help="URL of the arch-dependent RPMS area - for locating what to test")
163         parser.add_argument("-b", "--build", action="store", dest="build_url", 
164                             help="ignored, for legacy only")
165         parser.add_argument("-c", "--config", action="append", dest="config", default=[],
166                             help="Config module - can be set multiple times, or use quotes")
167         parser.add_argument("-p", "--personality", action="store", dest="personality", 
168                             help="personality - as in vbuild-nightly")
169         parser.add_argument("-d", "--pldistro", action="store", dest="pldistro", 
170                             help="pldistro - as in vbuild-nightly")
171         parser.add_argument("-f", "--fcdistro", action="store", dest="fcdistro", 
172                             help="fcdistro - as in vbuild-nightly")
173         parser.add_argument("-e", "--exclude", action="append", dest="exclude", default=[],
174                             help="steps to exclude - can be set multiple times, or use quotes")
175         parser.add_argument("-i", "--ignore", action="append", dest="ignore", default=[],
176                             help="steps to run but ignore - can be set multiple times, or use quotes")
177         parser.add_argument("-a", "--all", action="store_true", dest="all_steps", default=False,
178                             help="Run all default steps")
179         parser.add_argument("-l", "--list", action="store_true", dest="list_steps", default=False,
180                             help="List known steps")
181         parser.add_argument("-V", "--vserver", action="append", dest="ips_bplc", default=[],
182                             help="Specify the set of hostnames for the boxes that host the plcs")
183         parser.add_argument("-P", "--plcs", action="append", dest="ips_vplc", default=[],
184                             help="Specify the set of hostname/IP's to use for vplcs")
185         parser.add_argument("-Q", "--qemus", action="append", dest="ips_bnode", default=[],
186                             help="Specify the set of hostnames for the boxes that host the nodes")
187         parser.add_argument("-N", "--nodes", action="append", dest="ips_vnode", default=[],
188                             help="Specify the set of hostname/IP's to use for vnodes")
189         parser.add_argument("-s", "--size", action="store", dest="size", default=1,
190                             type=int, 
191                             help="set test size in # of plcs - default is 1")
192         parser.add_argument("-q", "--qualifier", action="store", dest="qualifier", default=None,
193                             type=int, 
194                             help="run steps only on plc numbered <qualifier>, starting at 1")
195         parser.add_argument("-y", "--rspec-style", action="append", dest="rspec_styles", default=[],
196                             help="OBSOLETE - for compat only")
197         parser.add_argument("-k", "--keep-going", action="store", dest="keep_going", default=False,
198                             help="proceeds even if some steps are failing")
199         parser.add_argument("-D", "--dbname", action="store", dest="dbname", default=None,
200                             help="Used by plc_db_dump and plc_db_restore")
201         parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, 
202                             help="Run in verbose mode")
203         parser.add_argument("-I", "--interactive", action="store_true", dest="interactive", default=False,
204                             help="prompts before each step")
205         parser.add_argument("-n", "--dry-run", action="store_true", dest="dry_run", default=False,
206                             help="Show environment and exits")
207         parser.add_argument("-t", "--trace", action="store", dest="trace_file", default=None,
208                             help="Trace file location")
209         parser.add_argument("-g", "--bonding", action='store', dest='bonding_build', default=None,
210                             help="specify build to bond with")
211         # if we call symlink 'rung' instead of just run this is equivalent to run -G
212         bonding_default = 'rung' in sys.argv[0]
213         parser.add_argument("-G", "--bonding-env", action='store_true', dest='bonding_env', default=bonding_default,
214                             help="get bonding build from env. variable $bonding")
215         parser.add_argument("steps", nargs='*')
216         self.options = parser.parse_args()
217
218         # handle -G/-g options
219         if self.options.bonding_env:
220             if 'bonding' not in os.environ:
221                 print("env. variable $bonding must be set with --bonding-env")
222                 sys.exit(1)
223             self.options.bonding_build = os.environ['bonding']
224
225         if self.options.bonding_build:
226             ## allow to pass -g ../2015.03.15--f18 so we can use bash completion
227             self.options.bonding_build = os.path.basename(self.options.bonding_build)
228             if not os.path.isdir("../{}".format(self.options.bonding_build)):
229                 print("could not find test dir for bonding build {}".format(self.options.bonding_build))
230                 sys.exit(1)
231
232         # allow things like "run -c 'c1 c2' -c c3"
233         def flatten (x):
234             result = []
235             for el in x:
236                 if hasattr(el, "__iter__") and not isinstance(el, str):
237                     result.extend(flatten(el))
238                 else:
239                     result.append(el)
240             return result
241         # flatten relevant options
242         for optname in ['config', 'exclude', 'ignore', 'ips_bplc', 'ips_vplc', 'ips_bnode', 'ips_vnode']:
243             setattr(self.options, optname,
244                     flatten([arg.split() for arg in getattr(self.options, optname)]))
245
246         if self.options.rspec_styles:
247             print("WARNING: -y option is obsolete")
248
249         # handle defaults and option persistence
250         for recname, filename, default, need_reverse in (
251             ('build_url', 'arg-build-url', TestMain.default_build_url, None),
252             ('ips_bplc', 'arg-ips-bplc', [], True),
253             ('ips_vplc', 'arg-ips-vplc', [], True), 
254             ('ips_bnode', 'arg-ips-bnode', [], True),
255             ('ips_vnode', 'arg-ips-vnode', [], True), 
256             ('config', 'arg-config', TestMain.default_config, False), 
257             ('arch_rpms_url', 'arg-arch-rpms-url', "", None), 
258             ('personality', 'arg-personality', "linux64", None),
259             ('pldistro', 'arg-pldistro', "onelab", None),
260             ('fcdistro', 'arg-fcdistro', 'f14', None),
261             ):
262 #            print('handling', recname)
263             path = filename
264             is_list = isinstance(default, list)
265             is_bool = isinstance(default, bool)
266             if not getattr(self.options, recname):
267                 try:
268                     with open(path) as file:
269                         parsed = file.readlines()
270                     if is_list:         # lists
271                         parsed = [x.strip() for x in parsed]
272                     else:               # strings and booleans
273                         if len(parsed) != 1:
274                             print("{} - error when parsing {}".format(sys.argv[1], path))
275                             sys.exit(1)
276                         parsed = parsed[0].strip()
277                         if is_bool:
278                             parsed = parsed.lower() == 'true'
279                     setattr(self.options, recname, parsed)
280                 except  Exception as e:
281                     if default != "":
282                         setattr(self.options, recname, default)
283                     else:
284                         print("Cannot determine", recname, e)
285                         print("Run {} --help for help".format(sys.argv[0]))
286                         sys.exit(1)
287
288             # save for next run
289             fsave = open(path, "w")
290             if is_list:                 # lists
291                 for value in getattr(self.options, recname):
292                     fsave.write(value + "\n")
293             else:                       # strings and booleans - just call str()
294                 fsave.write(str(getattr(self.options, recname)) + "\n")
295             fsave.close()
296 #            utils.header('Saved {} into {}'.format(recname, filename))
297
298             # lists need be reversed
299             # I suspect this is useful for the various pools but for config, it's painful
300             if isinstance(getattr(self.options, recname), list) and need_reverse:
301                 getattr(self.options, recname).reverse()
302
303             if self.options.verbose:
304                 utils.header('* Using {} = {}'.format(recname, getattr(self.options, recname)))
305
306         # hack : if sfa is not among the published rpms, skip these tests
307         TestPlc.check_whether_build_has_sfa(self.options.arch_rpms_url)
308
309         # initialize steps
310         if not self.options.steps:
311             # defaults, depends on using bonding or not
312             if self.options.bonding_build:
313                 self.options.steps = TestPlc.default_bonding_steps
314             else:
315                 self.options.steps = TestPlc.default_steps
316
317         if self.options.list_steps:
318             self.init_steps()
319             self.list_steps()
320             return 'SUCCESS'
321
322         # rewrite '-' into '_' in step names
323         self.options.steps   = [ step.replace('-', '_') for step in self.options.steps ]
324         self.options.exclude = [ step.replace('-', '_') for step in self.options.exclude ]
325         self.options.ignore  = [ step.replace('-', '_') for step in self.options.ignore ]
326
327         # technicality, decorate known steps to produce the '_ignore' version
328         TestPlc.create_ignore_steps()
329
330         # exclude
331         selected = []
332         for step in self.options.steps:
333             keep = True
334             for exclude in self.options.exclude:
335                 if utils.match(step, exclude):
336                     keep = False
337                     break
338             if keep:
339                 selected.append(step)
340
341         # ignore
342         selected = [ step if step not in self.options.ignore else step + "_ignore"
343                      for step in selected ]
344
345         self.options.steps = selected
346
347         # this is useful when propagating on host boxes, to avoid conflicts
348         self.options.buildname = os.path.basename(os.path.abspath(self.path))
349
350         if self.options.verbose:
351             self.show_env(self.options, "Verbose")
352
353         # load configs
354         all_plc_specs = []
355         for config in self.options.config:
356             modulename = 'config_' + config
357             try:
358                 m = __import__(modulename)
359                 all_plc_specs = m.config(all_plc_specs, self.options)
360             except :
361                 traceback.print_exc()
362                 print('Cannot load config {} -- ignored'.format(modulename))
363                 raise
364
365         # provision on local substrate
366         all_plc_specs = LocalSubstrate.local_substrate.provision(all_plc_specs, self.options)
367
368         # remember substrate IP address(es) for next run
369         with open('arg-ips-bplc', 'w') as ips_bplc_file:
370             for plc_spec in all_plc_specs:
371                 ips_bplc_file.write("{}\n".format(plc_spec['host_box']))
372         with open('arg-ips-vplc', 'w') as ips_vplc_file:
373             for plc_spec in all_plc_specs:
374                 ips_vplc_file.write("{}\n".format(plc_spec['settings']['PLC_API_HOST']))
375         # ditto for nodes
376         with open('arg-ips-bnode', 'w') as ips_bnode_file:
377             for plc_spec in all_plc_specs:
378                 for site_spec in plc_spec['sites']:
379                     for node_spec in site_spec['nodes']:
380                         ips_bnode_file.write("{}\n".format(node_spec['host_box']))
381         with open('arg-ips-vnode','w') as ips_vnode_file:
382             for plc_spec in all_plc_specs:
383                 for site_spec in plc_spec['sites']:
384                     for node_spec in site_spec['nodes']:
385                         # back to normal (unqualified) form
386                         stripped = node_spec['node_fields']['hostname'].split('.')[0]
387                         ips_vnode_file.write("{}\n".format(stripped))
388
389         # build a TestPlc object from the result, passing options
390         for spec in all_plc_specs:
391             spec['failed_step'] = False
392         all_plcs = [ (x, TestPlc(x,self.options)) for x in all_plc_specs]
393
394         # pass options to utils as well
395         utils.init_options(self.options)
396
397         # populate TestBonding objects
398         # need to wait until here as we need all_plcs
399         if self.options.bonding_build:
400             # this will fail if ../{bonding_build} has not the right arg- files
401             for spec, test_plc in all_plcs:
402                 test_plc.test_bonding = TestBonding (test_plc,
403                                                      onelab_bonding_spec(self.options.bonding_build),
404                                                      LocalSubstrate.local_substrate,
405                                                      self.options)
406         
407         overall_result = 'SUCCESS'
408         all_step_infos = []
409         for step in self.options.steps:
410             if not TestPlc.valid_step(step):
411                 continue
412             # some steps need to be done regardless of the previous ones: we force them
413             force = False
414             if step.endswith("_force"):
415                 step = step.replace("_force", "")
416                 force = True
417             # allow for steps to specify an index like in 
418             # run checkslice@2
419             try:
420                 step, qualifier = step.split('@')
421             except:
422                 qualifier = self.options.qualifier
423
424             try:
425                 stepobj = Step (step)
426                 for substep, method in stepobj.tuples():
427                     # a cross step will run a method on TestPlc that has a signature like
428                     # def cross_foo (self, all_test_plcs)
429                     cross = False
430                     if substep.find("cross_") == 0:
431                         cross = True
432                     all_step_infos.append ( (substep, method, force, cross, qualifier, ) )
433             except :
434                 utils.header("********** FAILED step {} (NOT FOUND) -- won't be run".format(step))
435                 traceback.print_exc()
436                 overall_result = 'FAILURE'
437             
438         if self.options.dry_run:
439             self.show_env(self.options, "Dry run")
440         
441         # init & open trace file if provided
442         if self.options.trace_file and not self.options.dry_run:
443             # create dir if needed
444             trace_dir = os.path.dirname(self.options.trace_file)
445             if trace_dir and not os.path.isdir(trace_dir):
446                 os.makedirs(trace_dir)
447             trace = open(self.options.trace_file,"w")
448
449         # do all steps on all plcs
450         TIME_FORMAT = "%H-%M-%S"
451         TRACE_FORMAT = "TRACE: {plc_counter:d} {begin}->{seconds}s={duration}s " + \
452                        "status={status} step={stepname} plc={plcname} force={force}\n"
453         for stepname, method, force, cross, qualifier in all_step_infos:
454             plc_counter = 0
455             for spec, plc_obj in all_plcs:
456                 plc_counter += 1
457                 # skip this step if we have specified a plc_explicit
458                 if qualifier and plc_counter != int(qualifier):
459                     continue
460
461                 plcname = spec['name']
462                 across_plcs = [ o for (s,o) in all_plcs if o!=plc_obj ]
463
464                 # run the step
465                 beg_time = datetime.now()
466                 begin = beg_time.strftime(TIME_FORMAT)
467                 if not spec['failed_step'] or force or self.options.interactive or self.options.keep_going:
468                     skip_step = False
469                     if self.options.interactive:
470                         prompting = True
471                         while prompting:
472                             msg="{:d} Run step {} on {} [r](un)/d(ry_run)/p(roceed)/s(kip)/q(uit) ? "\
473                                 .format(plc_counter, stepname, plcname)
474                             answer = input(msg).strip().lower() or "r"
475                             answer = answer[0]
476                             if answer in ['s','n']:     # skip/no/next
477                                 print('{} on {} skipped'.format(stepname, plcname))
478                                 prompting = False
479                                 skip_step = True
480                             elif answer in ['q','b']:   # quit/bye
481                                 print('Exiting')
482                                 return 'FAILURE'
483                             elif answer in ['d']:       # dry_run
484                                 dry_run = self.options.dry_run
485                                 self.options.dry_run = True
486                                 plc_obj.options.dry_run = True
487                                 plc_obj.apiserver.set_dry_run(True)
488                                 if not cross:
489                                     step_result=method(plc_obj)
490                                 else:
491                                     step_result=method(plc_obj, across_plcs)
492                                 print('dry_run step ->', step_result)
493                                 self.options.dry_run = dry_run
494                                 plc_obj.options.dry_run = dry_run
495                                 plc_obj.apiserver.set_dry_run(dry_run)
496                             elif answer in ['p']:
497                                 # take it as a yes and leave interactive mode
498                                 prompting = False
499                                 self.options.interactive = False
500                             elif answer in ['r','y']:   # run/yes
501                                 prompting = False
502                     if skip_step:
503                         continue
504                     try:
505                         force_msg = ""
506                         if force and spec['failed_step']:
507                             force_msg=" (forced after {} has failed)".format(spec['failed_step'])
508                         utils.header("********** {:d} RUNNING step {}{} on plc {}"\
509                                      .format(plc_counter, stepname, force_msg, plcname))
510                         if not cross:
511                             step_result = method(plc_obj)
512                         else:
513                             step_result = method(plc_obj, across_plcs)
514                         if isinstance (step_result, Ignored):
515                             step_result = step_result.result
516                             if step_result:
517                                 msg = "OK"
518                             else:
519                                 msg = "KO"
520                                 # do not overwrite if FAILURE
521                                 if overall_result == 'SUCCESS': 
522                                     overall_result = 'IGNORED'
523                             utils.header('********** {} IGNORED ({}) step {} on {}'\
524                                          .format(plc_counter, msg, stepname, plcname))
525                             status="{}[I]".format(msg)
526                         elif step_result:
527                             utils.header('********** {:d} SUCCESSFUL step {} on {}'\
528                                          .format(plc_counter, stepname, plcname))
529                             status = "OK"
530                         else:
531                             overall_result = 'FAILURE'
532                             spec['failed_step'] = stepname
533                             utils.header('********** {:d} FAILED step {} on {} (discarded from further steps)'\
534                                          .format(plc_counter, stepname, plcname))
535                             status = "KO"
536                     except:
537                         overall_result = 'FAILURE'
538                         spec['failed_step'] = stepname
539                         traceback.print_exc()
540                         utils.header ('********** {} FAILED (exception) step {} on {} (discarded from further steps)'\
541                                       .format(plc_counter, stepname, plcname))
542                         status = "KO"
543
544                 # do not run, just display it's skipped
545                 else:
546                     why = "has failed {}".format(spec['failed_step'])
547                     utils.header("********** {} SKIPPED Step {} on {} ({})"\
548                                  .format(plc_counter, stepname, plcname, why))
549                     status = "UNDEF"
550                 if not self.options.dry_run:
551                     delay = datetime.now()-beg_time
552                     seconds = int(delay.total_seconds())
553                     duration = str(delay)
554                     # always do this on stdout
555                     print(TRACE_FORMAT.format(**locals()))
556                     # duplicate on trace_file if provided
557                     if self.options.trace_file:
558                         trace.write(TRACE_FORMAT.format(**locals()))
559                         trace.flush()
560
561         if self.options.trace_file and not self.options.dry_run:
562             trace.close()
563
564         # free local substrate
565         LocalSubstrate.local_substrate.release(self.options)
566         
567         return overall_result
568
569     # wrapper to run, returns a shell-compatible result
570     # retcod:
571     # 0: SUCCESS
572     # 1: FAILURE
573     # 2: SUCCESS but some ignored steps failed
574     # 3: OTHER ERROR
575     def main(self):
576         try:
577             success = self.run()
578             if success == 'SUCCESS':
579                 return 0
580             elif success == 'IGNORED':
581                 return 2
582             else:
583                 return 1
584         except SystemExit:
585             print('Caught SystemExit')
586             return 3
587         except:
588             traceback.print_exc()
589             return 3
590
591 if __name__ == "__main__":
592     exit_code = TestMain().main()
593     print("TestMain exit code", exit_code)
594     sys.exit(exit_code)