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