3 # Thierry Parmentelat <thierry.parmentelat@inria.fr>
4 # Copyright (C) 2010 INRIA
6 import sys, os, os.path
7 from argparse import ArgumentParser
11 from datetime import datetime
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
20 # add $HOME in PYTHONPATH so we can import LocalSubstrate.py
21 sys.path.append(os.environ['HOME'])
26 natives = TestPlc.__dict__
29 return self.name.replace('_', '-')
31 return self.name.replace('-', '_')
33 def __init__ (self, name):
35 # a native step is implemented as a method on TestPlc
36 self.native = self.internal() in Step.natives
38 self.method = Step.natives[self.internal()]
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))
46 def print_doc (self, level=0):
50 start = level*' ' + '* '
51 # 2 is the len of '* '
52 width = tab - level - 2
53 format = "%%-%ds" % width
54 line = start + format % self.display()
57 print(self.method.__doc__)
59 print("*** no doc found")
61 beg_start = level*' ' + '>>> '
62 end_start = level*' ' + '<<< '
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*'<'
70 for step in self.substeps:
71 Step(step).print_doc(level+1)
74 # return a list of (name, method) for all native steps involved
77 return [ (self.internal(), self.method,) ]
80 for substep in [ Step(name) for name in self.substeps ] :
81 result += substep.tuples()
84 # convenience for listing macros
85 # just do a listdir, hoping we're in the right directory...
88 names= list(sequences.keys())
94 default_config = [ 'default' ]
96 default_build_url = "git://git.onelab.eu/tests"
99 self.path = os.path.dirname(sys.argv[0]) or "."
102 def show_env(self, options, message):
103 if self.options.verbose:
104 utils.header(message)
105 utils.show_options("main options", options)
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())
117 self.steps_message += 20*'x' + " Default steps with bonding build are\n" + \
118 TestPlc.printable_steps(TestPlc.default_bonding_steps)
120 def list_steps(self):
121 if not self.options.verbose:
122 print(self.steps_message)
124 # steps mentioned on the command line
125 if self.options.steps:
126 scopes = [("Argument steps",self.options.steps)]
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)]:
137 (step, qualifier) = step.split('@')
141 for special in ['force', 'ignore']:
142 stepname = stepname.replace('_'+special, "")
143 Step(stepname).print_doc()
146 usage = """usage: %%prog [options] steps
147 arch-rpms-url defaults to the last value used, as stored in arg-arch-rpms-url,
149 config defaults to the last value used, as stored in arg-config,
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
156 run with -l to see a list of available steps
158 """.format(TestMain.default_config)
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,
191 help="set test size in # of plcs - default is 1")
192 parser.add_argument("-q", "--qualifier", action="store", dest="qualifier", default=None,
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()
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")
223 self.options.bonding_build = os.environ['bonding']
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))
232 # allow things like "run -c 'c1 c2' -c c3"
236 if hasattr(el, "__iter__") and not isinstance(el, str):
237 result.extend(flatten(el))
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)]))
246 if self.options.rspec_styles:
247 print("WARNING: -y option is obsolete")
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),
262 # print('handling', recname)
264 is_list = isinstance(default, list)
265 is_bool = isinstance(default, bool)
266 if not getattr(self.options, recname):
268 with open(path) as file:
269 parsed = file.readlines()
271 parsed = [x.strip() for x in parsed]
272 else: # strings and booleans
274 print("{} - error when parsing {}".format(sys.argv[1], path))
276 parsed = parsed[0].strip()
278 parsed = parsed.lower() == 'true'
279 setattr(self.options, recname, parsed)
280 except Exception as e:
282 setattr(self.options, recname, default)
284 print("Cannot determine", recname, e)
285 print("Run {} --help for help".format(sys.argv[0]))
289 fsave = open(path, "w")
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")
296 # utils.header('Saved {} into {}'.format(recname, filename))
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()
303 if self.options.verbose:
304 utils.header('* Using {} = {}'.format(recname, getattr(self.options, recname)))
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)
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
315 self.options.steps = TestPlc.default_steps
317 if self.options.list_steps:
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 ]
327 # technicality, decorate known steps to produce the '_ignore' version
328 TestPlc.create_ignore_steps()
332 for step in self.options.steps:
334 for exclude in self.options.exclude:
335 if utils.match(step, exclude):
339 selected.append(step)
342 selected = [ step if step not in self.options.ignore else step + "_ignore"
343 for step in selected ]
345 self.options.steps = selected
347 # this is useful when propagating on host boxes, to avoid conflicts
348 self.options.buildname = os.path.basename(os.path.abspath(self.path))
350 if self.options.verbose:
351 self.show_env(self.options, "Verbose")
355 for config in self.options.config:
356 modulename = 'config_' + config
358 m = __import__(modulename)
359 all_plc_specs = m.config(all_plc_specs, self.options)
361 traceback.print_exc()
362 print('Cannot load config {} -- ignored'.format(modulename))
365 # provision on local substrate
366 all_plc_specs = LocalSubstrate.local_substrate.provision(all_plc_specs, self.options)
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']))
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))
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]
394 # pass options to utils as well
395 utils.init_options(self.options)
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,
407 overall_result = 'SUCCESS'
409 for step in self.options.steps:
410 if not TestPlc.valid_step(step):
412 # some steps need to be done regardless of the previous ones: we force them
414 if step.endswith("_force"):
415 step = step.replace("_force", "")
417 # allow for steps to specify an index like in
420 step, qualifier = step.split('@')
422 qualifier = self.options.qualifier
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)
430 if substep.find("cross_") == 0:
432 all_step_infos.append ( (substep, method, force, cross, qualifier, ) )
434 utils.header("********** FAILED step {} (NOT FOUND) -- won't be run".format(step))
435 traceback.print_exc()
436 overall_result = 'FAILURE'
438 if self.options.dry_run:
439 self.show_env(self.options, "Dry run")
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")
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:
455 for spec, plc_obj in all_plcs:
457 # skip this step if we have specified a plc_explicit
458 if qualifier and plc_counter != int(qualifier):
461 plcname = spec['name']
462 across_plcs = [ o for (s,o) in all_plcs if o!=plc_obj ]
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:
469 if self.options.interactive:
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"
476 if answer in ['s','n']: # skip/no/next
477 print('{} on {} skipped'.format(stepname, plcname))
480 elif answer in ['q','b']: # quit/bye
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)
489 step_result=method(plc_obj)
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
499 self.options.interactive = False
500 elif answer in ['r','y']: # run/yes
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))
511 step_result = method(plc_obj)
513 step_result = method(plc_obj, across_plcs)
514 if isinstance (step_result, Ignored):
515 step_result = step_result.result
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)
527 utils.header('********** {:d} SUCCESSFUL step {} on {}'\
528 .format(plc_counter, stepname, plcname))
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))
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))
544 # do not run, just display it's skipped
546 why = "has failed {}".format(spec['failed_step'])
547 utils.header("********** {} SKIPPED Step {} on {} ({})"\
548 .format(plc_counter, stepname, plcname, why))
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()))
561 if self.options.trace_file and not self.options.dry_run:
564 # free local substrate
565 LocalSubstrate.local_substrate.release(self.options)
567 return overall_result
569 # wrapper to run, returns a shell-compatible result
573 # 2: SUCCESS but some ignored steps failed
578 if success == 'SUCCESS':
580 elif success == 'IGNORED':
585 print('Caught SystemExit')
588 traceback.print_exc()
591 if __name__ == "__main__":
592 exit_code = TestMain().main()
593 print("TestMain exit code", exit_code)