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 TestSite import TestSite
16 from TestNode import TestNode
17 from macros import sequences
19 # add $HOME in PYTHONPATH so we can import LocalSubstrate.py
20 sys.path.append(os.environ['HOME'])
25 natives = TestPlc.__dict__
28 return self.name.replace('_', '-')
30 return self.name.replace('-', '_')
32 def __init__ (self, name):
34 # a native step is implemented as a method on TestPlc
35 self.native = self.internal() in Step.natives
37 self.method = Step.natives[self.internal()]
40 self.substeps = sequences[self.internal()]
42 print "macro step %s not found in macros.py (%s) - exiting" % (self.display(),e)
45 def print_doc (self, level=0):
49 start = level*' ' + '* '
50 # 2 is the len of '* '
51 width = tab - level - 2
52 format = "%%-%ds" % width
53 line = start + format % self.display()
56 print self.method.__doc__
58 print "*** no doc found"
60 beg_start = level*' ' + '>>> '
61 end_start = level*' ' + '<<< '
63 # 4 is the len of '>>> '
64 width = tab - level - 4 - trail
65 format = "%%-%ds" % width
66 beg_line = beg_start + format % self.display() + trail*'>'
67 end_line = end_start + format % self.display() + trail*'<'
69 for step in self.substeps:
70 Step(step).print_doc(level+1)
73 # return a list of (name, method) for all native steps involved
76 return [ (self.internal(), self.method,) ]
79 for substep in [ Step(name) for name in self.substeps ] :
80 result += substep.tuples()
83 # convenience for listing macros
84 # just do a listdir, hoping we're in the right directory...
87 names= sequences.keys()
93 default_config = [ 'default' ]
94 # default_rspec_styles = [ 'pl', 'pg' ]
95 default_rspec_styles = [ 'pg' ]
97 default_build_url = "git://git.onelab.eu/tests"
100 self.path = os.path.dirname(sys.argv[0]) or "."
103 def show_env(self, options, message):
104 if self.options.verbose:
105 utils.header(message)
106 utils.show_options("main options", options)
108 def init_steps(self):
109 self.steps_message = 20*'x' + " Defaut steps are\n" + \
110 TestPlc.printable_steps(TestPlc.default_steps)
111 self.steps_message += 20*'x' + " Other useful steps are\n" + \
112 TestPlc.printable_steps(TestPlc.other_steps)
113 self.steps_message += 20*'x' + " Macro steps are\n" + \
114 " ".join(Step.list_macros())
116 def list_steps(self):
117 if not self.options.verbose:
118 print self.steps_message
120 # steps mentioned on the command line
121 if self.options.args:
122 scopes = [("Argument steps",self.options.args)]
124 scopes = [("Default steps", TestPlc.default_steps)]
125 if self.options.all_steps:
126 scopes.append ( ("Other steps", TestPlc.other_steps) )
127 # try to list macro steps as well
128 scopes.append ( ("Macro steps", Step.list_macros()) )
129 for (scope, steps) in scopes:
130 print '--------------------', scope
131 for step in [step for step in steps if TestPlc.valid_step(step)]:
133 (step, qualifier) = step.split('@')
137 for special in ['force', 'ignore']:
138 stepname = stepname.replace('_'+special, "")
139 Step(stepname).print_doc()
143 usage = """usage: %%prog [options] steps
144 arch-rpms-url defaults to the last value used, as stored in arg-arch-rpms-url,
146 config defaults to the last value used, as stored in arg-config,
148 ips_vnode, ips_vplc and ips_qemu defaults to the last value used, as stored in arg-ips-{bplc,vplc,bnode,vnode},
149 default is to use IP scanning
150 steps refer to a method in TestPlc or to a step_* module
152 """%(TestMain.default_config)
153 usage += self.steps_message
155 parser = ArgumentParser(usage = usage)
156 parser.add_argument("-u", "--url", action="store", dest="arch_rpms_url",
157 help="URL of the arch-dependent RPMS area - for locating what to test")
158 parser.add_argument("-b", "--build", action="store", dest="build_url",
159 help="ignored, for legacy only")
160 parser.add_argument("-c", "--config", action="append", dest="config", default=[],
161 help="Config module - can be set multiple times, or use quotes")
162 parser.add_argument("-p", "--personality", action="store", dest="personality",
163 help="personality - as in vbuild-nightly")
164 parser.add_argument("-d", "--pldistro", action="store", dest="pldistro",
165 help="pldistro - as in vbuild-nightly")
166 parser.add_argument("-f", "--fcdistro", action="store", dest="fcdistro",
167 help="fcdistro - as in vbuild-nightly")
168 parser.add_argument("-e", "--exclude", action="append", dest="exclude", default=[],
169 help="steps to exclude - can be set multiple times, or use quotes")
170 parser.add_argument("-i", "--ignore", action="append", dest="ignore", default=[],
171 help="steps to run but ignore - can be set multiple times, or use quotes")
172 parser.add_argument("-a", "--all", action="store_true", dest="all_steps", default=False,
173 help="Run all default steps")
174 parser.add_argument("-l", "--list", action="store_true", dest="list_steps", default=False,
175 help="List known steps")
176 parser.add_argument("-V", "--vserver", action="append", dest="ips_bplc", default=[],
177 help="Specify the set of hostnames for the boxes that host the plcs")
178 parser.add_argument("-P", "--plcs", action="append", dest="ips_vplc", default=[],
179 help="Specify the set of hostname/IP's to use for vplcs")
180 parser.add_argument("-Q", "--qemus", action="append", dest="ips_bnode", default=[],
181 help="Specify the set of hostnames for the boxes that host the nodes")
182 parser.add_argument("-N", "--nodes", action="append", dest="ips_vnode", default=[],
183 help="Specify the set of hostname/IP's to use for vnodes")
184 parser.add_argument("-s", "--size", action="store", dest="size", default=1,
186 help="set test size in # of plcs - default is 1")
187 parser.add_argument("-q", "--qualifier", action="store", dest="qualifier", default=None,
189 help="run steps only on plc numbered <qualifier>, starting at 1")
190 parser.add_argument("-y", "--rspec-style", action="append", dest="rspec_styles", default=[],
191 help="pl is for planetlab rspecs, pg is for protogeni")
192 parser.add_argument("-k", "--keep-going", action="store", dest="keep_going", default=False,
193 help="proceeds even if some steps are failing")
194 parser.add_argument("-D", "--dbname", action="store", dest="dbname", default=None,
195 help="Used by plc_db_dump and plc_db_restore")
196 parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False,
197 help="Run in verbose mode")
198 parser.add_argument("-I", "--interactive", action="store_true", dest="interactive", default=False,
199 help="prompts before each step")
200 parser.add_argument("-n", "--dry-run", action="store_true", dest="dry_run", default=False,
201 help="Show environment and exits")
202 parser.add_argument("-t", "--trace", action="store", dest="trace_file", default=None,
203 help="Trace file location")
204 # parser.add_argument("-g", "--bonding", action='store', dest='bonding', default=None,
205 # help="specify build to bond with")
206 parser.add_argument("steps", nargs='*')
207 self.options = parser.parse_args()
209 # allow things like "run -c 'c1 c2' -c c3"
213 if hasattr(el, "__iter__") and not isinstance(el, basestring):
214 result.extend(flatten(el))
218 # flatten relevant options
219 for optname in ['config', 'exclude', 'ignore', 'ips_bplc', 'ips_vplc', 'ips_bnode', 'ips_vnode']:
220 setattr(self.options, optname,
221 flatten([arg.split() for arg in getattr(self.options, optname)]))
223 if not self.options.rspec_styles:
224 self.options.rspec_styles = TestMain.default_rspec_styles
226 # handle defaults and option persistence
227 for recname, filename, default, need_reverse in (
228 ('build_url', 'arg-build-url', TestMain.default_build_url, None),
229 ('ips_bplc', 'arg-ips-bplc', [], True),
230 ('ips_vplc', 'arg-ips-vplc', [], True),
231 ('ips_bnode', 'arg-ips-bnode', [], True),
232 ('ips_vnode', 'arg-ips-vnode', [], True),
233 ('config', 'arg-config', TestMain.default_config, False),
234 ('arch_rpms_url', 'arg-arch-rpms-url', "", None),
235 ('personality', 'arg-personality', "linux64", None),
236 ('pldistro', 'arg-pldistro', "onelab", None),
237 ('fcdistro', 'arg-fcdistro', 'f14', None),
239 # print 'handling',recname
241 is_list = isinstance(default, list)
242 is_bool = isinstance(default, bool)
243 if not getattr(self.options, recname):
245 parsed = file(path).readlines()
247 parsed = [x.strip() for x in parsed]
248 else: # strings and booleans
250 print "%s - error when parsing %s" % (sys.argv[1],path)
252 parsed = parsed[0].strip()
254 parsed = parsed.lower() == 'true'
255 setattr(self.options, recname, parsed)
258 setattr(self.options, recname, default)
260 print "Cannot determine", recname
261 print "Run %s --help for help" % sys.argv[0]
265 fsave = open(path, "w")
267 for value in getattr(self.options, recname):
268 fsave.write(value + "\n")
269 else: # strings and booleans - just call str()
270 fsave.write(str(getattr(self.options, recname)) + "\n")
272 # utils.header('Saved %s into %s'%(recname,filename))
274 # lists need be reversed
275 # I suspect this is useful for the various pools but for config, it's painful
276 if isinstance(getattr(self.options, recname), list) and need_reverse:
277 getattr(self.options, recname).reverse()
279 if self.options.verbose:
280 utils.header('* Using %s = %s' % (recname, getattr(self.options, recname)))
282 # hack : if sfa is not among the published rpms, skip these tests
283 TestPlc.check_whether_build_has_sfa(self.options.arch_rpms_url)
285 # use the default list of steps if unspecified
286 if len(self.options.steps) == 0:
287 self.options.steps = TestPlc.default_steps
289 if self.options.list_steps:
295 if not self.options.steps:
297 #self.options.steps=['dump','clean','install','populate']
298 self.options.steps = TestPlc.default_steps
300 # rewrite '-' into '_' in step names
301 self.options.steps = [ step.replace('-', '_') for step in self.options.steps ]
302 self.options.exclude = [ step.replace('-', '_') for step in self.options.exclude ]
303 self.options.ignore = [ step.replace('-', '_') for step in self.options.ignore ]
305 # technicality, decorate known steps to produce the '_ignore' version
306 TestPlc.create_ignore_steps()
310 for step in self.options.steps:
312 for exclude in self.options.exclude:
313 if utils.match(step, exclude):
317 selected.append(step)
320 selected = [ step if step not in self.options.ignore else step + "_ignore"
321 for step in selected ]
323 self.options.steps = selected
325 # this is useful when propagating on host boxes, to avoid conflicts
326 self.options.buildname = os.path.basename(os.path.abspath(self.path))
328 if self.options.verbose:
329 self.show_env(self.options, "Verbose")
333 for config in self.options.config:
334 modulename = 'config_' + config
336 m = __import__(modulename)
337 all_plc_specs = m.config(all_plc_specs, self.options)
339 traceback.print_exc()
340 print 'Cannot load config %s -- ignored' % modulename
343 # provision on local substrate
344 all_plc_specs = LocalSubstrate.local_substrate.provision(all_plc_specs, self.options)
346 # remember substrate IP address(es) for next run
347 ips_bplc_file = open('arg-ips-bplc', 'w')
348 for plc_spec in all_plc_specs:
349 ips_bplc_file.write("%s\n" % plc_spec['host_box'])
350 ips_bplc_file.close()
351 ips_vplc_file = open('arg-ips-vplc', 'w')
352 for plc_spec in all_plc_specs:
353 ips_vplc_file.write("%s\n" % plc_spec['settings']['PLC_API_HOST'])
354 ips_vplc_file.close()
356 ips_bnode_file = open('arg-ips-bnode', 'w')
357 for plc_spec in all_plc_specs:
358 for site_spec in plc_spec['sites']:
359 for node_spec in site_spec['nodes']:
360 ips_bnode_file.write("%s\n" % node_spec['host_box'])
361 ips_bnode_file.close()
362 ips_vnode_file = open('arg-ips-vnode','w')
363 for plc_spec in all_plc_specs:
364 for site_spec in plc_spec['sites']:
365 for node_spec in site_spec['nodes']:
366 # back to normal (unqualified) form
367 stripped = node_spec['node_fields']['hostname'].split('.')[0]
368 ips_vnode_file.write("%s\n" % stripped)
369 ips_vnode_file.close()
371 # build a TestPlc object from the result, passing options
372 for spec in all_plc_specs:
373 spec['failed_step'] = False
374 all_plcs = [ (x, TestPlc(x,self.options)) for x in all_plc_specs]
376 # pass options to utils as well
377 utils.init_options(self.options)
379 overall_result = 'SUCCESS'
381 for step in self.options.steps:
382 if not TestPlc.valid_step(step):
384 # some steps need to be done regardless of the previous ones: we force them
386 if step.endswith("_force"):
387 step = step.replace("_force", "")
389 # allow for steps to specify an index like in
392 step, qualifier = step.split('@')
394 qualifier = self.options.qualifier
397 stepobj = Step (step)
398 for substep, method in stepobj.tuples():
399 # a cross step will run a method on TestPlc that has a signature like
400 # def cross_foo (self, all_test_plcs)
402 if substep.find("cross_") == 0:
404 all_step_infos.append ( (substep, method, force, cross, qualifier, ) )
406 utils.header("********** FAILED step %s (NOT FOUND) -- won't be run" % step)
407 traceback.print_exc()
408 overall_result = 'FAILURE'
410 if self.options.dry_run:
411 self.show_env(self.options, "Dry run")
413 # init & open trace file if provided
414 if self.options.trace_file and not self.options.dry_run:
415 # create dir if needed
416 trace_dir = os.path.dirname(self.options.trace_file)
417 if trace_dir and not os.path.isdir(trace_dir):
418 os.makedirs(trace_dir)
419 trace = open(self.options.trace_file,"w")
421 # do all steps on all plcs
422 TIME_FORMAT = "%H-%M-%S"
423 TRACE_FORMAT = "TRACE: %(plc_counter)d %(begin)s->%(seconds)ss=%(duration)s " + \
424 "status=%(status)s step=%(stepname)s plc=%(plcname)s force=%(force)s\n"
425 for stepname, method, force, cross, qualifier in all_step_infos:
427 for spec, plc_obj in all_plcs:
429 # skip this step if we have specified a plc_explicit
430 if qualifier and plc_counter != int(qualifier):
433 plcname = spec['name']
434 across_plcs = [ o for (s,o) in all_plcs if o!=plc_obj ]
437 beg_time = datetime.now()
438 begin = beg_time.strftime(TIME_FORMAT)
439 if not spec['failed_step'] or force or self.options.interactive or self.options.keep_going:
441 if self.options.interactive:
444 msg="%d Run step %s on %s [r](un)/d(ry_run)/p(roceed)/s(kip)/q(uit) ? " % \
445 (plc_counter,stepname,plcname)
446 answer = raw_input(msg).strip().lower() or "r"
448 if answer in ['s','n']: # skip/no/next
449 print '%s on %s skipped' % (stepname, plcname)
452 elif answer in ['q','b']: # quit/bye
455 elif answer in ['d']: # dry_run
456 dry_run = self.options.dry_run
457 self.options.dry_run = True
458 plc_obj.options.dry_run = True
459 plc_obj.apiserver.set_dry_run(True)
461 step_result=method(plc_obj)
463 step_result=method(plc_obj, across_plcs)
464 print 'dry_run step ->', step_result
465 self.options.dry_run = dry_run
466 plc_obj.options.dry_run = dry_run
467 plc_obj.apiserver.set_dry_run(dry_run)
468 elif answer in ['p']:
469 # take it as a yes and leave interactive mode
471 self.options.interactive = False
472 elif answer in ['r','y']: # run/yes
478 if force and spec['failed_step']:
479 force_msg=" (forced after %s has failed)" % spec['failed_step']
480 utils.header("********** %d RUNNING step %s%s on plc %s" % \
481 (plc_counter, stepname, force_msg, plcname))
483 step_result = method(plc_obj)
485 step_result = method(plc_obj, across_plcs)
486 if isinstance (step_result, Ignored):
487 step_result = step_result.result
492 # do not overwrite if FAILURE
493 if overall_result == 'SUCCESS':
494 overall_result = 'IGNORED'
495 utils.header('********** %d IGNORED (%s) step %s on %s' % \
496 (plc_counter, msg, stepname, plcname))
499 utils.header('********** %d SUCCESSFUL step %s on %s' % \
500 (plc_counter, stepname, plcname))
503 overall_result = 'FAILURE'
504 spec['failed_step'] = stepname
505 utils.header('********** %d FAILED step %s on %s (discarded from further steps)' % \
506 (plc_counter, stepname, plcname))
509 overall_result = 'FAILURE'
510 spec['failed_step'] = stepname
511 traceback.print_exc()
512 utils.header ('********** %d FAILED (exception) step %s on %s (discarded from further steps)' % \
513 (plc_counter, stepname, plcname))
516 # do not run, just display it's skipped
518 why = "has failed %s" % spec['failed_step']
519 utils.header("********** %d SKIPPED Step %s on %s (%s)" % \
520 (plc_counter, stepname, plcname, why))
522 if not self.options.dry_run:
523 delay = datetime.now()-beg_time
524 seconds = int(delay.total_seconds())
525 duration = str(delay)
526 # always do this on stdout
527 print TRACE_FORMAT % locals()
528 # duplicate on trace_file if provided
529 if self.options.trace_file:
530 trace.write(TRACE_FORMAT % locals())
533 if self.options.trace_file and not self.options.dry_run:
536 # free local substrate
537 LocalSubstrate.local_substrate.release(self.options)
539 return overall_result
541 # wrapper to run, returns a shell-compatible result
545 # 2: SUCCESS but some ignored steps failed
550 if success == 'SUCCESS':
552 elif success == 'IGNORED':
557 print 'Caught SystemExit'
560 traceback.print_exc()
563 if __name__ == "__main__":
564 exit_code = TestMain().main()
565 print "TestMain exit code", exit_code