improve formatting of run -lv for nested macros
[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
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']:
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("-a","--all",action="store_true",dest="all_steps", default=False,
163                           help="Run all default steps")
164         parser.add_option("-l","--list",action="store_true",dest="list_steps", default=False,
165                           help="List known steps")
166         parser.add_option("-V","--vserver",action="append", dest="ips_bplc", default=[],
167                           help="Specify the set of hostnames for the boxes that host the plcs")
168         parser.add_option("-P","--plcs",action="append", dest="ips_vplc", default=[],
169                           help="Specify the set of hostname/IP's to use for vplcs")
170         parser.add_option("-Q","--qemus",action="append", dest="ips_bnode", default=[],
171                           help="Specify the set of hostnames for the boxes that host the nodes")
172         parser.add_option("-N","--nodes",action="append", dest="ips_vnode", default=[],
173                           help="Specify the set of hostname/IP's to use for vnodes")
174         parser.add_option ('-X', "--lxc",action='store_true',dest='plcs_use_lxc',
175                            help='use lxc-enabled plc boxes instead of vs-enabled ones')
176         parser.add_option("-s","--size",action="store",type="int",dest="size",default=1,
177                           help="sets test size in # of plcs - default is 1")
178         parser.add_option("-q","--qualifier",action="store",type="int",dest="qualifier",default=None,
179                           help="run steps only on plc numbered <qualifier>, starting at 1")
180         parser.add_option("-y","--rspec-style",action="append",dest="rspec_styles",default=[],
181                           help="pl is for planetlab rspecs, pg is for protogeni")
182         parser.add_option("-k","--keep-going",action="store",dest="keep_going",default=False,
183                           help="proceeds even if some steps are failing")
184         parser.add_option("-D","--dbname",action="store",dest="dbname",default=None,
185                            help="Used by plc_db_dump and plc_db_restore")
186         parser.add_option("-v","--verbose", action="store_true", dest="verbose", default=False, 
187                           help="Run in verbose mode")
188         parser.add_option("-i","--interactive",action="store_true",dest="interactive",default=False,
189                           help="prompts before each step")
190         parser.add_option("-n","--dry-run", action="store_true", dest="dry_run", default=False,
191                           help="Show environment and exits")
192         parser.add_option("-r","--restart-nm", action="store_true", dest="forcenm", default=False, 
193                           help="Force the NM to restart in ssh_slices step")
194         parser.add_option("-t","--trace", action="store", dest="trace_file", default=None,
195                           help="Trace file location")
196         (self.options, self.args) = parser.parse_args()
197
198         # allow things like "run -c 'c1 c2' -c c3"
199         def flatten (x):
200             result = []
201             for el in x:
202                 if hasattr(el, "__iter__") and not isinstance(el, basestring):
203                     result.extend(flatten(el))
204                 else:
205                     result.append(el)
206             return result
207         # flatten relevant options
208         for optname in ['config','exclude','ips_bplc','ips_vplc','ips_bnode','ips_vnode']:
209             setattr(self.options,optname, flatten ( [ arg.split() for arg in getattr(self.options,optname) ] ))
210
211         if not self.options.rspec_styles:
212             self.options.rspec_styles=TestMain.default_rspec_styles
213
214         # handle defaults and option persistence
215         for (recname,filename,default,need_reverse) in (
216             ('build_url','arg-build-url',TestMain.default_build_url,None) ,
217             ('ips_bplc','arg-ips-bplc',[],True),
218             ('ips_vplc','arg-ips-vplc',[],True) , 
219             ('ips_bnode','arg-ips-bnode',[],True),
220             ('ips_vnode','arg-ips-vnode',[],True) , 
221             ('config','arg-config',TestMain.default_config,False) , 
222             ('arch_rpms_url','arg-arch-rpms-url',"",None) , 
223             ('personality','arg-personality',"linux64",None),
224             ('pldistro','arg-pldistro',"onelab",None),
225             ('fcdistro','arg-fcdistro','f14',None),
226             ('plcs_use_lxc','arg-plcs-use-lxc',False,None),
227             ) :
228 #            print 'handling',recname
229             path=filename
230             is_list = isinstance(default,list)
231             is_bool = isinstance(default,bool)
232             if not getattr(self.options,recname):
233                 try:
234                     parsed=file(path).readlines()
235                     if is_list:         # lists
236                         parsed=[x.strip() for x in parsed]
237                     else:               # strings and booleans
238                         if len(parsed) != 1:
239                             print "%s - error when parsing %s"%(sys.argv[1],path)
240                             sys.exit(1)
241                         parsed=parsed[0].strip()
242                         if is_bool:
243                             parsed = parsed.lower()=='true'
244                     setattr(self.options,recname,parsed)
245                 except:
246                     if default != "":
247                         setattr(self.options,recname,default)
248                     else:
249                         print "Cannot determine",recname
250                         print "Run %s --help for help"%sys.argv[0]                        
251                         sys.exit(1)
252
253             # save for next run
254             fsave=open(path,"w")
255             if is_list:                 # lists
256                 for value in getattr(self.options,recname):
257                     fsave.write(value + "\n")
258             else:                       # strings and booleans - just call str()
259                 fsave.write(str(getattr(self.options,recname)) + "\n")
260             fsave.close()
261 #            utils.header('Saved %s into %s'%(recname,filename))
262
263             # lists need be reversed
264             # I suspect this is useful for the various pools but for config, it's painful
265             if isinstance(getattr(self.options,recname),list) and need_reverse:
266                 getattr(self.options,recname).reverse()
267
268             if self.options.verbose:
269                 utils.header('* Using %s = %s'%(recname,getattr(self.options,recname)))
270
271         # hack : if sfa is not among the published rpms, skip these tests
272         TestPlc.check_whether_build_has_sfa(self.options.arch_rpms_url)
273
274         # no step specified
275         self.options.args = self.args
276         if len(self.args) == 0:
277             self.options.steps=TestPlc.default_steps
278         else:
279             self.options.steps = self.args
280
281         if self.options.list_steps:
282             self.init_steps()
283             self.list_steps()
284             return True
285
286         # steps
287         if not self.options.steps:
288             #default (all) steps
289             #self.options.steps=['dump','clean','install','populate']
290             self.options.steps=TestPlc.default_steps
291
292         # rewrite '-' into '_' in step names
293         self.options.steps = [ step.replace('-','_') for step in self.options.steps ]
294
295         # exclude
296         selected=[]
297         for step in self.options.steps:
298             keep=True
299             for exclude in self.options.exclude:
300                 if utils.match(step,exclude):
301                     keep=False
302                     break
303             if keep: selected.append(step)
304         self.options.steps=selected
305
306         # this is useful when propagating on host boxes, to avoid conflicts
307         self.options.buildname = os.path.basename (os.path.abspath (self.path))
308
309         if self.options.verbose:
310             self.show_env(self.options,"Verbose")
311
312         # load configs
313         all_plc_specs = []
314         for config in self.options.config:
315             modulename='config_'+config
316             try:
317                 m = __import__(modulename)
318                 all_plc_specs = m.config(all_plc_specs,self.options)
319             except :
320                 traceback.print_exc()
321                 print 'Cannot load config %s -- ignored'%modulename
322                 raise
323
324         # provision on local substrate
325         if self.options.plcs_use_lxc: LocalSubstrate.local_substrate.rescope (plcs_on_vs=False, plcs_on_lxc=True)
326         all_plc_specs = LocalSubstrate.local_substrate.provision(all_plc_specs,self.options)
327
328         # remember substrate IP address(es) for next run
329         ips_bplc_file=open('arg-ips-bplc','w')
330         for plc_spec in all_plc_specs:
331             ips_bplc_file.write("%s\n"%plc_spec['host_box'])
332         ips_bplc_file.close()
333         ips_vplc_file=open('arg-ips-vplc','w')
334         for plc_spec in all_plc_specs:
335             ips_vplc_file.write("%s\n"%plc_spec['PLC_API_HOST'])
336         ips_vplc_file.close()
337         # ditto for nodes
338         ips_bnode_file=open('arg-ips-bnode','w')
339         for plc_spec in all_plc_specs:
340             for site_spec in plc_spec['sites']:
341                 for node_spec in site_spec['nodes']:
342                     ips_bnode_file.write("%s\n"%node_spec['host_box'])
343         ips_bnode_file.close()
344         ips_vnode_file=open('arg-ips-vnode','w')
345         for plc_spec in all_plc_specs:
346             for site_spec in plc_spec['sites']:
347                 for node_spec in site_spec['nodes']:
348                     # back to normal (unqualified) form
349                     stripped=node_spec['node_fields']['hostname'].split('.')[0]
350                     ips_vnode_file.write("%s\n"%stripped)
351         ips_vnode_file.close()
352
353         # build a TestPlc object from the result, passing options
354         for spec in all_plc_specs:
355             spec['failed_step'] = False
356         all_plcs = [ (x, TestPlc(x,self.options)) for x in all_plc_specs]
357
358         # pass options to utils as well
359         utils.init_options(self.options)
360
361         overall_result = True
362         all_step_infos=[]
363         for step in self.options.steps:
364             if not TestPlc.valid_step(step):
365                 continue
366             # some steps need to be done regardless of the previous ones: we force them
367             force=False
368             if step.find("force_") == 0:
369                 step=step.replace("force_","")
370                 force=True
371             # allow for steps to specify an index like in 
372             # run checkslice@2
373             try:        (step,qualifier)=step.split('@')
374             except:     qualifier=self.options.qualifier
375
376             try:
377                 stepobj = Step (step)
378                 for (substep, method) in stepobj.tuples():
379                     # a cross step will run a method on TestPlc that has a signature like
380                     # def cross_foo (self, all_test_plcs)
381                     cross=False
382                     if substep.find("cross_") == 0:
383                         cross=True
384                     all_step_infos.append ( (substep, method, force, cross, qualifier, ) )
385             except :
386                 utils.header("********** FAILED step %s (NOT FOUND) -- won't be run"%step)
387                 traceback.print_exc()
388                 overall_result = False
389             
390         if self.options.dry_run:
391             self.show_env(self.options,"Dry run")
392         
393         # init & open trace file if provided
394         if self.options.trace_file and not self.options.dry_run:
395             # create dir if needed
396             trace_dir=os.path.dirname(self.options.trace_file)
397             if trace_dir and not os.path.isdir(trace_dir):
398                 os.makedirs(trace_dir)
399             trace=open(self.options.trace_file,"w")
400
401         # do all steps on all plcs
402         TIME_FORMAT="%H-%M-%S"
403         TRACE_FORMAT="TRACE: %(plc_counter)d %(beg)s->%(end)s status=%(status)s step=%(stepname)s plc=%(plcname)s force=%(force)s\n"
404         for (stepname,method,force,cross,qualifier) in all_step_infos:
405             plc_counter=0
406             for (spec,plc_obj) in all_plcs:
407                 plc_counter+=1
408                 # skip this step if we have specified a plc_explicit
409                 if qualifier and plc_counter!=int(qualifier): continue
410
411                 plcname=spec['name']
412                 across_plcs = [ o for (s,o) in all_plcs if o!=plc_obj ]
413
414                 # run the step
415                 beg=strftime(TIME_FORMAT)
416                 if not spec['failed_step'] or force or self.options.interactive or self.options.keep_going:
417                     skip_step=False
418                     if self.options.interactive:
419                         prompting=True
420                         while prompting:
421                             msg="%d Run step %s on %s [r](un)/d(ry_run)/p(roceed)/s(kip)/q(uit) ? "%(plc_counter,stepname,plcname)
422                             answer=raw_input(msg).strip().lower() or "r"
423                             answer=answer[0]
424                             if answer in ['s','n']:     # skip/no/next
425                                 print '%s on %s skipped'%(stepname,plcname)
426                                 prompting=False
427                                 skip_step=True
428                             elif answer in ['q','b']:   # quit/bye
429                                 print 'Exiting'
430                                 return
431                             elif answer in ['d']:       # dry_run
432                                 dry_run=self.options.dry_run
433                                 self.options.dry_run=True
434                                 plc_obj.options.dry_run=True
435                                 plc_obj.apiserver.set_dry_run(True)
436                                 if not cross:   step_result=method(plc_obj)
437                                 else:           step_result=method(plc_obj,across_plcs)
438                                 print 'dry_run step ->',step_result
439                                 self.options.dry_run=dry_run
440                                 plc_obj.options.dry_run=dry_run
441                                 plc_obj.apiserver.set_dry_run(dry_run)
442                             elif answer in ['p']:
443                                 # take it as a yes and leave interactive mode
444                                 prompting=False
445                                 self.options.interactive=False
446                             elif answer in ['r','y']:   # run/yes
447                                 prompting=False
448                     if skip_step:
449                         continue
450                     try:
451                         force_msg=""
452                         if force and spec['failed_step']: force_msg=" (forced after %s has failed)"%spec['failed_step']
453                         utils.header("********** %d RUNNING step %s%s on plc %s"%(plc_counter,stepname,force_msg,plcname))
454                         if not cross:   step_result = method(plc_obj)
455                         else:           step_result = method(plc_obj,across_plcs)
456                         if step_result:
457                             utils.header('********** %d SUCCESSFUL step %s on %s'%(plc_counter,stepname,plcname))
458                             status="OK"
459                         else:
460                             overall_result = False
461                             spec['failed_step'] = stepname
462                             utils.header('********** %d FAILED Step %s on %s (discarded from further steps)'\
463                                              %(plc_counter,stepname,plcname))
464                             status="KO"
465                     except:
466                         overall_result=False
467                         spec['failed_step'] = stepname
468                         traceback.print_exc()
469                         utils.header ('********** %d FAILED (exception) Step %s on %s (discarded from further steps)'\
470                                           %(plc_counter,stepname,plcname))
471                         status="KO"
472
473                 # do not run, just display it's skipped
474                 else:
475                     why="has failed %s"%spec['failed_step']
476                     utils.header("********** %d SKIPPED Step %s on %s (%s)"%(plc_counter,stepname,plcname,why))
477                     status="UNDEF"
478                 if not self.options.dry_run:
479                     end=strftime(TIME_FORMAT)
480                     # always do this on stdout
481                     print TRACE_FORMAT%locals()
482                     # duplicate on trace_file if provided
483                     if self.options.trace_file:
484                         trace.write(TRACE_FORMAT%locals())
485                         trace.flush()
486
487         if self.options.trace_file and not self.options.dry_run:
488             trace.close()
489
490         # free local substrate
491         LocalSubstrate.local_substrate.release(self.options)
492         
493         return overall_result
494
495     # wrapper to run, returns a shell-compatible result
496     def main(self):
497         try:
498             success=self.run()
499             if success:
500                 return 0
501             else:
502                 return 1 
503         except SystemExit:
504             print 'Caught SystemExit'
505             raise
506         except:
507             traceback.print_exc()
508             return 2
509
510 if __name__ == "__main__":
511     sys.exit(TestMain().main())