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()]
43 print "macro step %s not found in macros.py (%s) - exiting" % (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= sequences.keys()
94 default_config = [ 'default' ]
95 # default_rspec_styles = [ 'pl', 'pg' ]
96 default_rspec_styles = [ 'pg' ]
98 default_build_url = "git://git.onelab.eu/tests"
101 self.path = os.path.dirname(sys.argv[0]) or "."
104 def show_env(self, options, message):
105 if self.options.verbose:
106 utils.header(message)
107 utils.show_options("main options", options)
109 def init_steps(self):
110 self.steps_message = ""
111 if not self.options.bonding:
112 self.steps_message += 20*'x' + " Defaut steps are\n" + \
113 TestPlc.printable_steps(TestPlc.default_steps)
114 self.steps_message += 20*'x' + " Other useful steps are\n" + \
115 TestPlc.printable_steps(TestPlc.other_steps)
116 self.steps_message += 20*'x' + " Macro steps are\n" + \
117 " ".join(Step.list_macros())
119 self.steps_message += 20*'x' + " Default steps with bonding are\n" + \
120 TestPlc.printable_steps(TestPlc.bonding_steps)
122 def list_steps(self):
123 if not self.options.verbose:
124 print self.steps_message
126 # steps mentioned on the command line
127 if self.options.args:
128 scopes = [("Argument steps",self.options.args)]
130 scopes = [("Default steps", TestPlc.default_steps)]
131 if self.options.all_steps:
132 scopes.append ( ("Other steps", TestPlc.other_steps) )
133 # try to list macro steps as well
134 scopes.append ( ("Macro steps", Step.list_macros()) )
135 for (scope, steps) in scopes:
136 print '--------------------', scope
137 for step in [step for step in steps if TestPlc.valid_step(step)]:
139 (step, qualifier) = step.split('@')
143 for special in ['force', 'ignore']:
144 stepname = stepname.replace('_'+special, "")
145 Step(stepname).print_doc()
148 usage = """usage: %%prog [options] steps
149 arch-rpms-url defaults to the last value used, as stored in arg-arch-rpms-url,
151 config defaults to the last value used, as stored in arg-config,
153 ips_vnode, ips_vplc and ips_qemu defaults to the last value used, as stored in arg-ips-{bplc,vplc,bnode,vnode},
154 default is to use IP scanning
155 steps refer to a method in TestPlc or to a step_* module
157 run with -l to see a list of available steps
159 """%(TestMain.default_config)
161 parser = ArgumentParser(usage = usage)
162 parser.add_argument("-u", "--url", action="store", dest="arch_rpms_url",
163 help="URL of the arch-dependent RPMS area - for locating what to test")
164 parser.add_argument("-b", "--build", action="store", dest="build_url",
165 help="ignored, for legacy only")
166 parser.add_argument("-c", "--config", action="append", dest="config", default=[],
167 help="Config module - can be set multiple times, or use quotes")
168 parser.add_argument("-p", "--personality", action="store", dest="personality",
169 help="personality - as in vbuild-nightly")
170 parser.add_argument("-d", "--pldistro", action="store", dest="pldistro",
171 help="pldistro - as in vbuild-nightly")
172 parser.add_argument("-f", "--fcdistro", action="store", dest="fcdistro",
173 help="fcdistro - as in vbuild-nightly")
174 parser.add_argument("-e", "--exclude", action="append", dest="exclude", default=[],
175 help="steps to exclude - can be set multiple times, or use quotes")
176 parser.add_argument("-i", "--ignore", action="append", dest="ignore", default=[],
177 help="steps to run but ignore - can be set multiple times, or use quotes")
178 parser.add_argument("-a", "--all", action="store_true", dest="all_steps", default=False,
179 help="Run all default steps")
180 parser.add_argument("-l", "--list", action="store_true", dest="list_steps", default=False,
181 help="List known steps")
182 parser.add_argument("-V", "--vserver", action="append", dest="ips_bplc", default=[],
183 help="Specify the set of hostnames for the boxes that host the plcs")
184 parser.add_argument("-P", "--plcs", action="append", dest="ips_vplc", default=[],
185 help="Specify the set of hostname/IP's to use for vplcs")
186 parser.add_argument("-Q", "--qemus", action="append", dest="ips_bnode", default=[],
187 help="Specify the set of hostnames for the boxes that host the nodes")
188 parser.add_argument("-N", "--nodes", action="append", dest="ips_vnode", default=[],
189 help="Specify the set of hostname/IP's to use for vnodes")
190 parser.add_argument("-s", "--size", action="store", dest="size", default=1,
192 help="set test size in # of plcs - default is 1")
193 parser.add_argument("-q", "--qualifier", action="store", dest="qualifier", default=None,
195 help="run steps only on plc numbered <qualifier>, starting at 1")
196 parser.add_argument("-y", "--rspec-style", action="append", dest="rspec_styles", default=[],
197 help="pl is for planetlab rspecs, pg is for protogeni")
198 parser.add_argument("-k", "--keep-going", action="store", dest="keep_going", default=False,
199 help="proceeds even if some steps are failing")
200 parser.add_argument("-D", "--dbname", action="store", dest="dbname", default=None,
201 help="Used by plc_db_dump and plc_db_restore")
202 parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False,
203 help="Run in verbose mode")
204 parser.add_argument("-I", "--interactive", action="store_true", dest="interactive", default=False,
205 help="prompts before each step")
206 parser.add_argument("-n", "--dry-run", action="store_true", dest="dry_run", default=False,
207 help="Show environment and exits")
208 parser.add_argument("-t", "--trace", action="store", dest="trace_file", default=None,
209 help="Trace file location")
210 parser.add_argument("-g", "--bonding", action='store', dest='bonding', default=None,
211 help="specify build to bond with")
212 parser.add_argument("steps", nargs='*')
213 self.options = parser.parse_args()
215 # allow things like "run -c 'c1 c2' -c c3"
219 if hasattr(el, "__iter__") and not isinstance(el, basestring):
220 result.extend(flatten(el))
224 # flatten relevant options
225 for optname in ['config', 'exclude', 'ignore', 'ips_bplc', 'ips_vplc', 'ips_bnode', 'ips_vnode']:
226 setattr(self.options, optname,
227 flatten([arg.split() for arg in getattr(self.options, optname)]))
229 if not self.options.rspec_styles:
230 self.options.rspec_styles = TestMain.default_rspec_styles
232 # handle defaults and option persistence
233 for recname, filename, default, need_reverse in (
234 ('build_url', 'arg-build-url', TestMain.default_build_url, None),
235 ('ips_bplc', 'arg-ips-bplc', [], True),
236 ('ips_vplc', 'arg-ips-vplc', [], True),
237 ('ips_bnode', 'arg-ips-bnode', [], True),
238 ('ips_vnode', 'arg-ips-vnode', [], True),
239 ('config', 'arg-config', TestMain.default_config, False),
240 ('arch_rpms_url', 'arg-arch-rpms-url', "", None),
241 ('personality', 'arg-personality', "linux64", None),
242 ('pldistro', 'arg-pldistro', "onelab", None),
243 ('fcdistro', 'arg-fcdistro', 'f14', None),
245 # print 'handling',recname
247 is_list = isinstance(default, list)
248 is_bool = isinstance(default, bool)
249 if not getattr(self.options, recname):
251 parsed = file(path).readlines()
253 parsed = [x.strip() for x in parsed]
254 else: # strings and booleans
256 print "%s - error when parsing %s" % (sys.argv[1],path)
258 parsed = parsed[0].strip()
260 parsed = parsed.lower() == 'true'
261 setattr(self.options, recname, parsed)
264 setattr(self.options, recname, default)
266 print "Cannot determine", recname
267 print "Run %s --help for help" % sys.argv[0]
271 fsave = open(path, "w")
273 for value in getattr(self.options, recname):
274 fsave.write(value + "\n")
275 else: # strings and booleans - just call str()
276 fsave.write(str(getattr(self.options, recname)) + "\n")
278 # utils.header('Saved %s into %s'%(recname,filename))
280 # lists need be reversed
281 # I suspect this is useful for the various pools but for config, it's painful
282 if isinstance(getattr(self.options, recname), list) and need_reverse:
283 getattr(self.options, recname).reverse()
285 if self.options.verbose:
286 utils.header('* Using %s = %s' % (recname, getattr(self.options, recname)))
288 # hack : if sfa is not among the published rpms, skip these tests
289 TestPlc.check_whether_build_has_sfa(self.options.arch_rpms_url)
292 if not self.options.steps:
293 # defaults, depends on using bonding or not
294 if self.options.bonding:
295 self.options.steps = TestPlc.bonding_steps
297 self.options.steps = TestPlc.default_steps
299 if self.options.list_steps:
304 # rewrite '-' into '_' in step names
305 self.options.steps = [ step.replace('-', '_') for step in self.options.steps ]
306 self.options.exclude = [ step.replace('-', '_') for step in self.options.exclude ]
307 self.options.ignore = [ step.replace('-', '_') for step in self.options.ignore ]
309 # technicality, decorate known steps to produce the '_ignore' version
310 TestPlc.create_ignore_steps()
314 for step in self.options.steps:
316 for exclude in self.options.exclude:
317 if utils.match(step, exclude):
321 selected.append(step)
324 selected = [ step if step not in self.options.ignore else step + "_ignore"
325 for step in selected ]
327 self.options.steps = selected
329 # this is useful when propagating on host boxes, to avoid conflicts
330 self.options.buildname = os.path.basename(os.path.abspath(self.path))
332 if self.options.verbose:
333 self.show_env(self.options, "Verbose")
337 for config in self.options.config:
338 modulename = 'config_' + config
340 m = __import__(modulename)
341 all_plc_specs = m.config(all_plc_specs, self.options)
343 traceback.print_exc()
344 print 'Cannot load config %s -- ignored' % modulename
347 # provision on local substrate
348 all_plc_specs = LocalSubstrate.local_substrate.provision(all_plc_specs, self.options)
350 # remember substrate IP address(es) for next run
351 ips_bplc_file = open('arg-ips-bplc', 'w')
352 for plc_spec in all_plc_specs:
353 ips_bplc_file.write("%s\n" % plc_spec['host_box'])
354 ips_bplc_file.close()
355 ips_vplc_file = open('arg-ips-vplc', 'w')
356 for plc_spec in all_plc_specs:
357 ips_vplc_file.write("%s\n" % plc_spec['settings']['PLC_API_HOST'])
358 ips_vplc_file.close()
360 ips_bnode_file = open('arg-ips-bnode', 'w')
361 for plc_spec in all_plc_specs:
362 for site_spec in plc_spec['sites']:
363 for node_spec in site_spec['nodes']:
364 ips_bnode_file.write("%s\n" % node_spec['host_box'])
365 ips_bnode_file.close()
366 ips_vnode_file = open('arg-ips-vnode','w')
367 for plc_spec in all_plc_specs:
368 for site_spec in plc_spec['sites']:
369 for node_spec in site_spec['nodes']:
370 # back to normal (unqualified) form
371 stripped = node_spec['node_fields']['hostname'].split('.')[0]
372 ips_vnode_file.write("%s\n" % stripped)
373 ips_vnode_file.close()
375 # build a TestPlc object from the result, passing options
376 for spec in all_plc_specs:
377 spec['failed_step'] = False
378 all_plcs = [ (x, TestPlc(x,self.options)) for x in all_plc_specs]
380 # pass options to utils as well
381 utils.init_options(self.options)
383 # populate TestBonding objects
384 # need to wait until here as we need all_plcs
385 if self.options.bonding:
386 ## allow to pass -g ../2015.03.15--f18 so we can use bash completion
387 self.options.bonding = os.path.basename(self.options.bonding)
388 # this will fail if ../{bonding} has not the right arg- files
389 for spec, test_plc in all_plcs:
390 test_plc.test_bonding = TestBonding (test_plc,
391 onelab_bonding_spec(self.options.bonding),
394 overall_result = 'SUCCESS'
396 for step in self.options.steps:
397 if not TestPlc.valid_step(step):
399 # some steps need to be done regardless of the previous ones: we force them
401 if step.endswith("_force"):
402 step = step.replace("_force", "")
404 # allow for steps to specify an index like in
407 step, qualifier = step.split('@')
409 qualifier = self.options.qualifier
412 stepobj = Step (step)
413 for substep, method in stepobj.tuples():
414 # a cross step will run a method on TestPlc that has a signature like
415 # def cross_foo (self, all_test_plcs)
417 if substep.find("cross_") == 0:
419 all_step_infos.append ( (substep, method, force, cross, qualifier, ) )
421 utils.header("********** FAILED step %s (NOT FOUND) -- won't be run" % step)
422 traceback.print_exc()
423 overall_result = 'FAILURE'
425 if self.options.dry_run:
426 self.show_env(self.options, "Dry run")
428 # init & open trace file if provided
429 if self.options.trace_file and not self.options.dry_run:
430 # create dir if needed
431 trace_dir = os.path.dirname(self.options.trace_file)
432 if trace_dir and not os.path.isdir(trace_dir):
433 os.makedirs(trace_dir)
434 trace = open(self.options.trace_file,"w")
436 # do all steps on all plcs
437 TIME_FORMAT = "%H-%M-%S"
438 TRACE_FORMAT = "TRACE: %(plc_counter)d %(begin)s->%(seconds)ss=%(duration)s " + \
439 "status=%(status)s step=%(stepname)s plc=%(plcname)s force=%(force)s\n"
440 for stepname, method, force, cross, qualifier in all_step_infos:
442 for spec, plc_obj in all_plcs:
444 # skip this step if we have specified a plc_explicit
445 if qualifier and plc_counter != int(qualifier):
448 plcname = spec['name']
449 across_plcs = [ o for (s,o) in all_plcs if o!=plc_obj ]
452 beg_time = datetime.now()
453 begin = beg_time.strftime(TIME_FORMAT)
454 if not spec['failed_step'] or force or self.options.interactive or self.options.keep_going:
456 if self.options.interactive:
459 msg="%d Run step %s on %s [r](un)/d(ry_run)/p(roceed)/s(kip)/q(uit) ? " % \
460 (plc_counter,stepname,plcname)
461 answer = raw_input(msg).strip().lower() or "r"
463 if answer in ['s','n']: # skip/no/next
464 print '%s on %s skipped' % (stepname, plcname)
467 elif answer in ['q','b']: # quit/bye
470 elif answer in ['d']: # dry_run
471 dry_run = self.options.dry_run
472 self.options.dry_run = True
473 plc_obj.options.dry_run = True
474 plc_obj.apiserver.set_dry_run(True)
476 step_result=method(plc_obj)
478 step_result=method(plc_obj, across_plcs)
479 print 'dry_run step ->', step_result
480 self.options.dry_run = dry_run
481 plc_obj.options.dry_run = dry_run
482 plc_obj.apiserver.set_dry_run(dry_run)
483 elif answer in ['p']:
484 # take it as a yes and leave interactive mode
486 self.options.interactive = False
487 elif answer in ['r','y']: # run/yes
493 if force and spec['failed_step']:
494 force_msg=" (forced after %s has failed)" % spec['failed_step']
495 utils.header("********** %d RUNNING step %s%s on plc %s" % \
496 (plc_counter, stepname, force_msg, plcname))
498 step_result = method(plc_obj)
500 step_result = method(plc_obj, across_plcs)
501 if isinstance (step_result, Ignored):
502 step_result = step_result.result
507 # do not overwrite if FAILURE
508 if overall_result == 'SUCCESS':
509 overall_result = 'IGNORED'
510 utils.header('********** %d IGNORED (%s) step %s on %s' % \
511 (plc_counter, msg, stepname, plcname))
514 utils.header('********** %d SUCCESSFUL step %s on %s' % \
515 (plc_counter, stepname, plcname))
518 overall_result = 'FAILURE'
519 spec['failed_step'] = stepname
520 utils.header('********** %d FAILED step %s on %s (discarded from further steps)' % \
521 (plc_counter, stepname, plcname))
524 overall_result = 'FAILURE'
525 spec['failed_step'] = stepname
526 traceback.print_exc()
527 utils.header ('********** %d FAILED (exception) step %s on %s (discarded from further steps)' % \
528 (plc_counter, stepname, plcname))
531 # do not run, just display it's skipped
533 why = "has failed %s" % spec['failed_step']
534 utils.header("********** %d SKIPPED Step %s on %s (%s)" % \
535 (plc_counter, stepname, plcname, why))
537 if not self.options.dry_run:
538 delay = datetime.now()-beg_time
539 seconds = int(delay.total_seconds())
540 duration = str(delay)
541 # always do this on stdout
542 print TRACE_FORMAT % locals()
543 # duplicate on trace_file if provided
544 if self.options.trace_file:
545 trace.write(TRACE_FORMAT % locals())
548 if self.options.trace_file and not self.options.dry_run:
551 # free local substrate
552 LocalSubstrate.local_substrate.release(self.options)
554 return overall_result
556 # wrapper to run, returns a shell-compatible result
560 # 2: SUCCESS but some ignored steps failed
565 if success == 'SUCCESS':
567 elif success == 'IGNORED':
572 print 'Caught SystemExit'
575 traceback.print_exc()
578 if __name__ == "__main__":
579 exit_code = TestMain().main()
580 print "TestMain exit code", exit_code