this is how TestMapper deals with subfields
[tests.git] / system / Substrate.py
1 #
2 # Thierry Parmentelat <thierry.parmentelat@inria.fr>
3 # Copyright (C) 2010 INRIA 
4 #
5 # #################### history
6 #
7 # see also Substrate.readme
8 #
9 # This is a complete rewrite of TestResources/Tracker/Pool
10 # we don't use trackers anymore and just probe/sense the running 
11 # boxes to figure out where we are
12 # in order to implement some fairness in the round-robin allocation scheme
13 # we need an indication of the 'age' of each running entity, 
14 # hence the 'timestamp-*' steps in TestPlc
15
16 # this should be much more flexible:
17 # * supports several plc boxes 
18 # * supports several qemu guests per host
19 # * no need to worry about tracker being in sync or not
20 #
21 # #################### howto use
22 #
23 # each site is to write its own LocalSubstrate.py, 
24 # (see e.g. LocalSubstrate.inria.py)
25 # LocalSubstrate.py is expected to be in /root on the testmaster box
26 # and needs to define
27 # MYPLCs
28 # . the vserver-capable boxes used for hosting myplcs
29 # .  and their admissible load (max # of myplcs)
30 # . the pool of DNS-names and IP-addresses available for myplcs
31 # QEMU nodes
32 # . the kvm-qemu capable boxes to host qemu instances
33 # .  and their admissible load (max # of myplcs)
34 # . the pool of DNS-names and IP-addresses available for nodes
35
36 # #################### implem. note
37
38 # this model relies on 'sensing' the substrate, 
39 # i.e. probing all the boxes for their running instances of vservers and qemu
40 # this is how we get rid of tracker inconsistencies 
41 # however there is a 'black hole' between the time where a given address is 
42 # allocated and when it actually gets used/pingable
43 # this is why we still need a shared knowledge among running tests
44 # in a file named /root/starting
45 # this is connected to the Pool class 
46
47 # ####################
48
49 import os.path, sys
50 import time
51 import re
52 import traceback
53 import subprocess
54 import commands
55 import socket
56 from optparse import OptionParser
57
58 import utils
59 from TestSsh import TestSsh
60 from TestMapper import TestMapper
61
62 # too painful to propagate this cleanly
63 verbose=None
64
65 def header (message,banner=True):
66     if not message: return
67     if banner: print "===============",
68     print message
69     sys.stdout.flush()
70
71 def timestamp_sort(o1,o2): return o1.timestamp-o2.timestamp
72
73 def short_hostname (hostname):
74     return hostname.split('.')[0]
75
76 ####################
77 # the place were other test instances tell about their not-yet-started
78 # instances, that go undetected through sensing
79 class Starting:
80
81     location='/root/starting'
82     def __init__ (self):
83         self.tuples=[]
84
85     def load (self):
86         try:    self.tuples=[line.strip().split('@') 
87                              for line in file(Starting.location).readlines()]
88         except: self.tuples=[]
89
90     def vnames (self) : 
91         self.load()
92         return [ x for (x,_) in self.tuples ]
93
94     def add (self, vname, bname):
95         if not vname in self.vnames():
96             file(Starting.location,'a').write("%s@%s\n"%(vname,bname))
97             
98     def delete_vname (self, vname):
99         self.load()
100         if vname in self.vnames():
101             f=file(Starting.location,'w')
102             for (v,b) in self.tuples: 
103                 if v != vname: f.write("%s@%s\n"%(v,b))
104             f.close()
105     
106 ####################
107 # pool class
108 # allows to pick an available IP among a pool
109 # input is expressed as a list of tuples (hostname,ip,user_data)
110 # that can be searched iteratively for a free slot
111 # e.g.
112 # pool = [ (hostname1,user_data1),  
113 #          (hostname2,user_data2),  
114 #          (hostname3,user_data2),  
115 #          (hostname4,user_data4) ]
116 # assuming that ip1 and ip3 are taken (pingable), then we'd get
117 # pool=Pool(pool)
118 # pool.next_free() -> entry2
119 # pool.next_free() -> entry4
120 # pool.next_free() -> None
121 # that is, even if ip2 is not busy/pingable when the second next_free() is issued
122
123 class PoolItem:
124     def __init__ (self,hostname,userdata):
125         self.hostname=hostname
126         self.userdata=userdata
127         # slot holds 'busy' or 'free' or 'mine' or 'starting' or None
128         # 'mine' is for our own stuff, 'starting' from the concurrent tests
129         self.status=None
130         self.ip=None
131
132     def line(self):
133         return "Pooled %s (%s) -> %s"%(self.hostname,self.userdata, self.status)
134
135     def char (self):
136         if   self.status==None:       return '?'
137         elif self.status=='busy':     return '+'
138         elif self.status=='free':     return '-'
139         elif self.status=='mine':     return 'M'
140         elif self.status=='starting': return 'S'
141
142     def get_ip(self):
143         if self.ip: return self.ip
144         ip=socket.gethostbyname(self.hostname)
145         self.ip=ip
146         return ip
147
148 class Pool:
149
150     def __init__ (self, tuples,message, substrate):
151         self.pool_items= [ PoolItem (hostname,userdata) for (hostname,userdata) in tuples ] 
152         self.message=message
153         # where to send notifications upon load_starting
154         self.substrate=substrate
155
156     def list (self, verbose=False):
157         for i in self.pool_items: print i.line()
158
159     def line (self):
160         line=self.message
161         for i in self.pool_items: line += ' ' + i.char()
162         return line
163
164     def _item (self, hostname):
165         for i in self.pool_items: 
166             if i.hostname==hostname: return i
167         raise Exception ("Could not locate hostname %s in pool %s"%(hostname,self.message))
168
169     def retrieve_userdata (self, hostname): 
170         return self._item(hostname).userdata
171
172     def get_ip (self, hostname):
173         try:    return self._item(hostname).get_ip()
174         except: return socket.gethostbyname(hostname)
175         
176     def set_mine (self, hostname):
177         try:
178             self._item(hostname).status='mine'
179         except:
180             print 'WARNING: host %s not found in IP pool %s'%(hostname,self.message)
181
182     def next_free (self):
183         for i in self.pool_items:
184             if i.status == 'free':
185                 i.status='mine'
186                 return (i.hostname,i.userdata)
187         return None
188
189     ####################
190     # we have a starting instance of our own
191     def add_starting (self, vname, bname):
192         Starting().add(vname,bname)
193         for i in self.pool_items:
194             if i.hostname==vname: i.status='mine'
195
196     # load the starting instances from the common file
197     # remember that might be ours
198     # return the list of (vname,bname) that are not ours
199     def load_starting (self):
200         starting=Starting()
201         starting.load()
202         new_tuples=[]
203         for (v,b) in starting.tuples:
204             for i in self.pool_items:
205                 if i.hostname==v and i.status=='free':
206                     i.status='starting'
207                     new_tuples.append( (v,b,) )
208         return new_tuples
209
210     def release_my_starting (self):
211         for i in self.pool_items:
212             if i.status=='mine':
213                 Starting().delete_vname (i.hostname)
214                 i.status=None
215
216
217     ##########
218     def _sense (self):
219         for item in self.pool_items:
220             if item.status is not None: 
221                 print item.char(),
222                 continue
223             if self.check_ping (item.hostname): 
224                 item.status='busy'
225                 print '*',
226             else:
227                 item.status='free'
228                 print '.',
229     
230     def sense (self):
231         print 'Sensing IP pool',self.message,
232         self._sense()
233         print 'Done'
234         for (vname,bname) in self.load_starting():
235             self.substrate.add_starting_dummy (bname, vname)
236         print "After having loaded 'starting': IP pool"
237         print self.line()
238     # OS-dependent ping option (support for macos, for convenience)
239     ping_timeout_option = None
240     # returns True when a given hostname/ip responds to ping
241     def check_ping (self,hostname):
242         if not Pool.ping_timeout_option:
243             (status,osname) = commands.getstatusoutput("uname -s")
244             if status != 0:
245                 raise Exception, "TestPool: Cannot figure your OS name"
246             if osname == "Linux":
247                 Pool.ping_timeout_option="-w"
248             elif osname == "Darwin":
249                 Pool.ping_timeout_option="-t"
250
251         command="ping -c 1 %s 1 %s"%(Pool.ping_timeout_option,hostname)
252         (status,output) = commands.getstatusoutput(command)
253         return status == 0
254
255 ####################
256 class Box:
257     def __init__ (self,hostname):
258         self.hostname=hostname
259         self._probed=None
260     def shortname (self):
261         return short_hostname(self.hostname)
262     def test_ssh (self): return TestSsh(self.hostname,username='root',unknown_host=False)
263     def reboot (self, options):
264         self.test_ssh().run("shutdown -r now",message="Rebooting %s"%self.hostname,
265                             dry_run=options.dry_run)
266
267     def hostname_fedora (self,virt=None):
268         result = "%s {"%self.hostname
269         if virt: result += "%s-"%virt
270         result += "%s"%self.fedora()
271         # too painful to propagate this cleanly
272         global verbose
273         if verbose:
274             result += "-%s" % self.uname()
275         result += "}"
276         return result
277
278     separator = "===composite==="
279
280     # probe the ssh link
281     # take this chance to gather useful stuff
282     def probe (self):
283         # try it only once
284         if self._probed is not None: return self._probed
285         composite_command = [ ]
286         composite_command += [ "hostname" ]
287         composite_command += [ ";" , "echo", Box.separator , ";" ]
288         composite_command += [ "uptime" ]
289         composite_command += [ ";" , "echo", Box.separator , ";" ]
290         composite_command += [ "uname", "-r"]
291         composite_command += [ ";" , "echo", Box.separator , ";" ]
292         composite_command += [ "cat" , "/etc/fedora-release" ]
293
294         # due to colons and all, this is going wrong on the local box (typically testmaster)
295         # I am reluctant to change TestSsh as it might break all over the place, so
296         if self.test_ssh().is_local():
297             probe_argv = [ "bash", "-c", " ".join (composite_command) ]
298         else:
299             probe_argv=self.test_ssh().actual_argv(composite_command)
300         composite=self.backquote ( probe_argv, trash_err=True )
301         self._hostname = self._uptime = self._uname = self._fedora = "** Unknown **"
302         if not composite: 
303             print "root@%s unreachable"%self.hostname
304             self._probed=''
305         else:
306             try:
307                 pieces = composite.split(Box.separator)
308                 pieces = [ x.strip() for x in pieces ]
309                 [self._hostname, self._uptime, self._uname, self._fedora] = pieces
310                 # customize
311                 self._uptime = ', '.join([ x.strip() for x in self._uptime.split(',')[2:]])
312                 self._fedora = self._fedora.replace("Fedora release ","f").split(" ")[0]
313             except:
314                 import traceback
315                 print 'BEG issue with pieces',pieces
316                 traceback.print_exc()
317                 print 'END issue with pieces',pieces
318             self._probed=self._hostname
319         return self._probed
320
321     # use argv=['bash','-c',"the command line"]
322     def uptime(self):
323         self.probe()
324         if hasattr(self,'_uptime') and self._uptime: return self._uptime
325         return '*unprobed* uptime'
326     def uname(self):
327         self.probe()
328         if hasattr(self,'_uname') and self._uname: return self._uname
329         return '*unprobed* uname'
330     def fedora(self):
331         self.probe()
332         if hasattr(self,'_fedora') and self._fedora: return self._fedora
333         return '*unprobed* fedora'
334
335     def run(self,argv,message=None,trash_err=False,dry_run=False):
336         if dry_run:
337             print 'DRY_RUN:',
338             print " ".join(argv)
339             return 0
340         else:
341             header(message)
342             if not trash_err:
343                 return subprocess.call(argv)
344             else:
345                 return subprocess.call(argv,stderr=file('/dev/null','w'))
346                 
347     def run_ssh (self, argv, message, trash_err=False, dry_run=False):
348         ssh_argv = self.test_ssh().actual_argv(argv)
349         result=self.run (ssh_argv, message, trash_err, dry_run=dry_run)
350         if result!=0:
351             print "WARNING: failed to run %s on %s"%(" ".join(argv),self.hostname)
352         return result
353
354     def backquote (self, argv, trash_err=False):
355         # print 'running backquote',argv
356         if not trash_err:
357             result= subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
358         else:
359             result= subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
360         return result
361
362     # if you have any shell-expanded arguments like *
363     # and if there's any chance the command is adressed to the local host
364     def backquote_ssh (self, argv, trash_err=False):
365         if not self.probe(): return ''
366         return self.backquote( self.test_ssh().actual_argv(argv), trash_err)
367
368 ############################################################
369 class BuildInstance:
370     def __init__ (self, buildname, pid, buildbox):
371         self.buildname=buildname
372         self.buildbox=buildbox
373         self.pids=[pid]
374
375     def add_pid(self,pid):
376         self.pids.append(pid)
377
378     def line (self):
379         return "== %s == (pids=%r)"%(self.buildname,self.pids)
380
381 class BuildBox (Box):
382     def __init__ (self,hostname):
383         Box.__init__(self,hostname)
384         self.build_instances=[]
385
386     def add_build (self,buildname,pid):
387         for build in self.build_instances:
388             if build.buildname==buildname: 
389                 build.add_pid(pid)
390                 return
391         self.build_instances.append(BuildInstance(buildname, pid, self))
392
393     def list(self, verbose=False):
394         if not self.build_instances: 
395             header ('No build process on %s (%s)'%(self.hostname_fedora(),self.uptime()))
396         else:
397             header ("Builds on %s (%s)"%(self.hostname_fedora(),self.uptime()))
398             for b in self.build_instances: 
399                 header (b.line(),banner=False)
400
401     def reboot (self, options):
402         if not options.soft:
403             Box.reboot(self,options)
404         else:
405             self.soft_reboot (options)
406
407 build_matcher=re.compile("\s*(?P<pid>[0-9]+).*-[bo]\s+(?P<buildname>[^\s]+)(\s|\Z)")
408 build_matcher_initvm=re.compile("\s*(?P<pid>[0-9]+).*initvm.*\s+(?P<buildname>[^\s]+)\s*\Z")
409
410 class BuildLxcBox (BuildBox):
411     def soft_reboot (self, options):
412             command=['pkill','lbuild']
413             self.run_ssh(command,"Terminating vbuild processes",dry_run=options.dry_run)
414
415     # inspect box and find currently running builds
416     def sense(self, options):
417         print 'xb',
418         pids=self.backquote_ssh(['pgrep','lbuild'],trash_err=True)
419         if not pids: return
420         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
421         ps_lines=self.backquote_ssh (command).split('\n')
422         for line in ps_lines:
423             if not line.strip() or line.find('PID')>=0: continue
424             m=build_matcher.match(line)
425             if m: 
426                 date=time.strftime('%Y-%m-%d',time.localtime(time.time()))
427                 buildname=m.group('buildname').replace('@DATE@',date)
428                 self.add_build (buildname,m.group('pid'))
429                 continue
430             m=build_matcher_initvm.match(line)
431             if m: 
432                 # buildname is expansed here
433                 self.add_build (buildname,m.group('pid'))
434                 continue
435             header('BuildLxcBox.sense: command %r returned line that failed to match'%command)
436             header(">>%s<<"%line)
437     
438 ############################################################
439 class PlcInstance:
440     def __init__ (self, plcbox):
441         self.plc_box=plcbox
442         # unknown yet
443         self.timestamp=0
444         
445     def set_timestamp (self,timestamp): self.timestamp=timestamp
446     def set_now (self): self.timestamp=int(time.time())
447     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
448
449 class PlcLxcInstance (PlcInstance):
450     # does lxc have a context id of any kind ?
451     def __init__ (self, plcbox, lxcname, pid):
452         PlcInstance.__init__(self, plcbox)
453         self.lxcname = lxcname
454         self.pid = pid
455
456     def vplcname (self):
457         return self.lxcname.split('-')[-1]
458     def buildname (self):
459         return self.lxcname.rsplit('-',2)[0]
460
461     def line (self):
462         msg="== %s =="%(self.vplcname())
463         msg += " [=%s]"%self.lxcname
464         if self.pid==-1:  msg+=" not (yet?) running"
465         else:              msg+=" (pid=%s)"%self.pid
466         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
467         else:              msg += " *unknown timestamp*"
468         return msg
469
470     def kill (self):
471         command="rsync lxc-driver.sh  %s:/root"%self.plc_box.hostname
472         commands.getstatusoutput(command)
473         msg="lxc container stopping %s on %s"%(self.lxcname,self.plc_box.hostname)
474         self.plc_box.run_ssh(['/root/lxc-driver.sh','-c','stop_lxc','-n',self.lxcname],msg)
475         self.plc_box.forget(self)
476
477 ##########
478 class PlcBox (Box):
479     def __init__ (self, hostname, max_plcs):
480         Box.__init__(self,hostname)
481         self.plc_instances=[]
482         self.max_plcs=max_plcs
483
484     def free_slots (self):
485         return self.max_plcs - len(self.plc_instances)
486
487     # fill one slot even though this one is not started yet
488     def add_dummy (self, plcname):
489         dummy=PlcLxcInstance(self,'dummy_'+plcname,0)
490         dummy.set_now()
491         self.plc_instances.append(dummy)
492
493     def forget (self, plc_instance):
494         self.plc_instances.remove(plc_instance)
495
496     def reboot (self, options):
497         if not options.soft:
498             Box.reboot(self,options)
499         else:
500             self.soft_reboot (options)
501
502     def list(self, verbose=False):
503         if not self.plc_instances: 
504             header ('No plc running on %s'%(self.line()))
505         else:
506             header ("Active plc VMs on %s"%self.line())
507             self.plc_instances.sort(timestamp_sort)
508             for p in self.plc_instances: 
509                 header (p.line(),banner=False)
510
511 ## we do not this at INRIA any more
512 class PlcLxcBox (PlcBox):
513
514     def add_lxc (self,lxcname,pid):
515         for plc in self.plc_instances:
516             if plc.lxcname==lxcname:
517                 header("WARNING, duplicate myplc %s running on %s"%\
518                            (lxcname,self.hostname),banner=False)
519                 return
520         self.plc_instances.append(PlcLxcInstance(self,lxcname,pid))    
521
522
523     # a line describing the box
524     def line(self): 
525         return "%s [max=%d,free=%d] (%s)"%(self.hostname_fedora(virt="lxc"), 
526                                            self.max_plcs,self.free_slots(),
527                                            self.uptime(),
528                                            )
529     
530     def plc_instance_by_lxcname (self, lxcname):
531         for p in self.plc_instances:
532             if p.lxcname==lxcname: return p
533         return None
534     
535     # essentially shutdown all running containers
536     def soft_reboot (self, options):
537         command="rsync lxc-driver.sh  %s:/root"%self.hostname
538         commands.getstatusoutput(command)
539         self.run_ssh(['/root/lxc-driver.sh','-c','stop_all'],"Stopping all running lxc containers on %s"%(self.hostname,),
540                      dry_run=options.dry_run)
541
542
543     # sense is expected to fill self.plc_instances with PlcLxcInstance's 
544     # to describe the currently running VM's
545     def sense (self, options):
546         print "xp",
547         command="rsync lxc-driver.sh  %s:/root"%self.hostname
548         commands.getstatusoutput(command)
549         command=['/root/lxc-driver.sh','-c','sense_all']
550         lxc_stat = self.backquote_ssh (command)
551         for lxc_line in lxc_stat.split("\n"):
552             if not lxc_line: continue
553             lxcname=lxc_line.split(";")[0]
554             pid=lxc_line.split(";")[1]
555             timestamp=lxc_line.split(";")[2]
556             self.add_lxc(lxcname,pid)
557             try: timestamp=int(timestamp)
558             except: timestamp=0
559             p=self.plc_instance_by_lxcname(lxcname)
560             if not p:
561                 print 'WARNING zombie plc',self.hostname,lxcname
562                 print '... was expecting',lxcname,'in',[i.lxcname for i in self.plc_instances]
563                 continue
564             p.set_timestamp(timestamp)
565
566 ############################################################
567 class QemuInstance: 
568     def __init__ (self, nodename, pid, qemubox):
569         self.nodename=nodename
570         self.pid=pid
571         self.qemu_box=qemubox
572         # not known yet
573         self.buildname=None
574         self.timestamp=0
575         
576     def set_buildname (self,buildname): self.buildname=buildname
577     def set_timestamp (self,timestamp): self.timestamp=timestamp
578     def set_now (self): self.timestamp=int(time.time())
579     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
580     
581     def line (self):
582         msg = "== %s =="%(short_hostname(self.nodename))
583         msg += " [=%s]"%self.buildname
584         if self.pid:       msg += " (pid=%s)"%self.pid
585         else:              msg += " not (yet?) running"
586         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
587         else:              msg += " *unknown timestamp*"
588         return msg
589     
590     def kill(self):
591         if self.pid==0: 
592             print "cannot kill qemu %s with pid==0"%self.nodename
593             return
594         msg="Killing qemu %s with pid=%s on box %s"%(self.nodename,self.pid,self.qemu_box.hostname)
595         self.qemu_box.run_ssh(['kill',"%s"%self.pid],msg)
596         self.qemu_box.forget(self)
597
598
599 class QemuBox (Box):
600     def __init__ (self, hostname, max_qemus):
601         Box.__init__(self,hostname)
602         self.qemu_instances=[]
603         self.max_qemus=max_qemus
604
605     def add_node (self,nodename,pid):
606         for qemu in self.qemu_instances:
607             if qemu.nodename==nodename: 
608                 header("WARNING, duplicate qemu %s running on %s"%\
609                            (nodename,self.hostname), banner=False)
610                 return
611         self.qemu_instances.append(QemuInstance(nodename,pid,self))
612
613     def node_names (self):
614         return [ qi.nodename for qi in self.qemu_instances ]
615
616     def forget (self, qemu_instance):
617         self.qemu_instances.remove(qemu_instance)
618
619     # fill one slot even though this one is not started yet
620     def add_dummy (self, nodename):
621         dummy=QemuInstance('dummy_'+nodename,0,self)
622         dummy.set_now()
623         self.qemu_instances.append(dummy)
624
625     def line (self):
626         return "%s [max=%d,free=%d] (%s) %s"%(
627             self.hostname_fedora(virt="qemu"), self.max_qemus,self.free_slots(),
628             self.uptime(),self.driver())
629
630     def list(self, verbose=False):
631         if not self.qemu_instances: 
632             header ('No qemu on %s'%(self.line()))
633         else:
634             header ("Qemus on %s"%(self.line()))
635             self.qemu_instances.sort(timestamp_sort)
636             for q in self.qemu_instances: 
637                 header (q.line(),banner=False)
638
639     def free_slots (self):
640         return self.max_qemus - len(self.qemu_instances)
641
642     def driver(self):
643         if hasattr(self,'_driver') and self._driver: return self._driver
644         return '*undef* driver'
645
646     def qemu_instance_by_pid (self,pid):
647         for q in self.qemu_instances:
648             if q.pid==pid: return q
649         return None
650
651     def qemu_instance_by_nodename_buildname (self,nodename,buildname):
652         for q in self.qemu_instances:
653             if q.nodename==nodename and q.buildname==buildname:
654                 return q
655         return None
656
657     def reboot (self, options):
658         if not options.soft:
659             Box.reboot(self,options)
660         else:
661             self.run_ssh(['pkill','qemu'],"Killing qemu instances",
662                          dry_run=options.dry_run)
663
664     matcher=re.compile("\s*(?P<pid>[0-9]+).*-cdrom\s+(?P<nodename>[^\s]+)\.iso")
665     def sense(self, options):
666         print 'qn',
667         modules=self.backquote_ssh(['lsmod']).split('\n')
668         self._driver='*NO kqemu/kvm_intel MODULE LOADED*'
669         for module in modules:
670             if module.find('kqemu')==0:
671                 self._driver='kqemu module loaded'
672             # kvm might be loaded without kvm_intel (we dont have AMD)
673             elif module.find('kvm_intel')==0:
674                 self._driver='kvm_intel OK'
675         ########## find out running pids
676         pids=self.backquote_ssh(['pgrep','qemu'])
677         if not pids: return
678         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
679         ps_lines = self.backquote_ssh (command).split("\n")
680         for line in ps_lines:
681             if not line.strip() or line.find('PID') >=0 : continue
682             m=QemuBox.matcher.match(line)
683             if m: 
684                 self.add_node (m.group('nodename'),m.group('pid'))
685                 continue
686             header('QemuBox.sense: command %r returned line that failed to match'%command)
687             header(">>%s<<"%line)
688         ########## retrieve alive instances and map to build
689         live_builds=[]
690         command=['grep','.','/vservers/*/*/qemu.pid','/dev/null']
691         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
692         for pid_line in pid_lines:
693             if not pid_line.strip(): continue
694             # expect <build>/<nodename>/qemu.pid:<pid>pid
695             try:
696                 (_,__,buildname,nodename,tail)=pid_line.split('/')
697                 (_,pid)=tail.split(':')
698                 q=self.qemu_instance_by_pid (pid)
699                 if not q: continue
700                 q.set_buildname(buildname)
701                 live_builds.append(buildname)
702             except: print 'WARNING, could not parse pid line',pid_line
703         # retrieve timestamps
704         if not live_builds: return
705         command=   ['grep','.']
706         command += ['/vservers/%s/*/timestamp'%b for b in live_builds]
707         command += ['/dev/null']
708         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
709         for ts_line in ts_lines:
710             if not ts_line.strip(): continue
711             # expect <build>/<nodename>/timestamp:<timestamp>
712             try:
713                 (_,__,buildname,nodename,tail)=ts_line.split('/')
714                 nodename=nodename.replace('qemu-','')
715                 (_,timestamp)=tail.split(':')
716                 timestamp=int(timestamp)
717                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
718                 if not q: 
719                     print 'WARNING zombie qemu',self.hostname,ts_line
720                     print '... was expecting (',short_hostname(nodename),buildname,') in',\
721                         [ (short_hostname(i.nodename),i.buildname) for i in self.qemu_instances ]
722                     continue
723                 q.set_timestamp(timestamp)
724             except:  print 'WARNING, could not parse ts line',ts_line
725
726 ####################
727 class TestInstance:
728     def __init__ (self, buildname, pid=0):
729         self.pids=[]
730         if pid!=0: self.pid.append(pid)
731         self.buildname=buildname
732         # latest trace line
733         self.trace=''
734         # has a KO test
735         self.broken_steps=[]
736         self.timestamp = 0
737
738     def set_timestamp (self,timestamp): self.timestamp=timestamp
739     def set_now (self): self.timestamp=int(time.time())
740     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
741
742     def is_running (self): return len(self.pids) != 0
743
744     def add_pid (self,pid):
745         self.pids.append(pid)
746     def set_broken (self, plcindex, step): 
747         self.broken_steps.append ( (plcindex, step,) )
748
749     def second_letter (self):
750         if not self.broken_steps: return '='
751         else:
752             really_broken = [ step for (i,step) in self.broken_steps if '_ignore' not in step ]
753             # W is for warning like what's in the build mail
754             if len(really_broken)==0: return 'W'
755             else: return 'B'
756
757     def line (self):
758         # make up a 2-letter sign
759         # first letter : '=', unless build is running : '*'
760         double = '*' if self.pids else '='
761         # second letter : '=' if fine, 'W' for warnings (only ignored steps) 'B' for broken
762         letter2 = self.second_letter()
763         double += letter2
764         msg = " %s %s =="%(double,self.buildname)
765         if not self.pids:       pass
766         elif len(self.pids)==1: msg += " (pid=%s)"%self.pids[0]
767         else:                   msg += " !!!pids=%s!!!"%self.pids
768         msg += " @%s"%self.pretty_timestamp()
769         if letter2 != '=':
770             msg2 = ( ' BROKEN' if letter2 == 'B' else ' WARNING' )
771             # sometimes we have an empty plcindex
772             msg += " [%s="%msg2 + " ".join( [ "%s@%s"%(s,i) if i else s for (i,s) in self.broken_steps ] ) + "]"
773         return msg
774
775 class TestBox (Box):
776     def __init__ (self,hostname):
777         Box.__init__(self,hostname)
778         self.starting_ips=[]
779         self.test_instances=[]
780
781     def reboot (self, options):
782         # can't reboot a vserver VM
783         self.run_ssh (['pkill','run_log'],"Terminating current runs",
784                       dry_run=options.dry_run)
785         self.run_ssh (['rm','-f',Starting.location],"Cleaning %s"%Starting.location,
786                       dry_run=options.dry_run)
787
788     def get_test (self, buildname):
789         for i in self.test_instances:
790             if i.buildname==buildname: return i
791
792     # we scan ALL remaining test results, even the ones not running
793     def add_timestamp (self, buildname, timestamp):
794         i=self.get_test(buildname)
795         if i:   
796             i.set_timestamp(timestamp)
797         else:   
798             i=TestInstance(buildname,0)
799             i.set_timestamp(timestamp)
800             self.test_instances.append(i)
801
802     def add_running_test (self, pid, buildname):
803         i=self.get_test(buildname)
804         if not i:
805             self.test_instances.append (TestInstance (buildname,pid))
806             return
807         if i.pids:
808             print "WARNING: 2 concurrent tests run on same build %s"%buildname
809         i.add_pid (pid)
810
811     def add_broken (self, buildname, plcindex, step):
812         i=self.get_test(buildname)
813         if not i:
814             i=TestInstance(buildname)
815             self.test_instances.append(i)
816         i.set_broken(plcindex, step)
817
818     matcher_proc=re.compile (".*/proc/(?P<pid>[0-9]+)/cwd.*/root/(?P<buildname>[^/]+)$")
819     matcher_grep=re.compile ("/root/(?P<buildname>[^/]+)/logs/trace.*:TRACE:\s*(?P<plcindex>[0-9]+).*step=(?P<step>\S+).*")
820     matcher_grep_missing=re.compile ("grep: /root/(?P<buildname>[^/]+)/logs/trace: No such file or directory")
821     def sense (self, options):
822         print 'tm',
823         self.starting_ips=[x for x in self.backquote_ssh(['cat',Starting.location], trash_err=True).strip().split('\n') if x]
824
825         # scan timestamps on all tests
826         # this is likely to not invoke ssh so we need to be a bit smarter to get * expanded
827         # xxx would make sense above too
828         command=['bash','-c',"grep . /root/*/timestamp /dev/null"]
829         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
830         for ts_line in ts_lines:
831             if not ts_line.strip(): continue
832             # expect /root/<buildname>/timestamp:<timestamp>
833             try:
834                 (ts_file,timestamp)=ts_line.split(':')
835                 ts_file=os.path.dirname(ts_file)
836                 buildname=os.path.basename(ts_file)
837                 timestamp=int(timestamp)
838                 t=self.add_timestamp(buildname,timestamp)
839             except:  print 'WARNING, could not parse ts line',ts_line
840
841         # let's try to be robust here -- tests that fail very early like e.g.
842         # "Cannot make space for a PLC instance: vplc IP pool exhausted", that occurs as part of provision
843         # will result in a 'trace' symlink to an inexisting 'trace-<>.txt' because no step has gone through
844         # simple 'trace' should exist though as it is created by run_log
845         command=['bash','-c',"grep KO /root/*/logs/trace /dev/null 2>&1" ]
846         trace_lines=self.backquote_ssh (command).split('\n')
847         for line in trace_lines:
848             if not line.strip(): continue
849             m=TestBox.matcher_grep_missing.match(line)
850             if m:
851                 buildname=m.group('buildname')
852                 self.add_broken(buildname,'','NO STEP DONE')
853                 continue
854             m=TestBox.matcher_grep.match(line)
855             if m: 
856                 buildname=m.group('buildname')
857                 plcindex=m.group('plcindex')
858                 step=m.group('step')
859                 self.add_broken(buildname,plcindex, step)
860                 continue
861             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
862             header(">>%s<<"%line)
863
864         pids = self.backquote_ssh (['pgrep','run_log'],trash_err=True)
865         if not pids: return
866         command=['ls','-ld'] + ["/proc/%s/cwd"%pid for pid in pids.split("\n") if pid]
867         ps_lines=self.backquote_ssh (command).split('\n')
868         for line in ps_lines:
869             if not line.strip(): continue
870             m=TestBox.matcher_proc.match(line)
871             if m: 
872                 pid=m.group('pid')
873                 buildname=m.group('buildname')
874                 self.add_running_test(pid, buildname)
875                 continue
876             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
877             header(">>%s<<"%line)
878         
879         
880     def line (self):
881         return self.hostname_fedora()
882
883     def list (self, verbose=False):
884         # verbose shows all tests
885         if verbose:
886             instances = self.test_instances
887             msg="tests"
888         else:
889             instances = [ i for i in self.test_instances if i.is_running() ]
890             msg="running tests"
891
892         if not instances:
893             header ("No %s on %s"%(msg,self.line()))
894         else:
895             header ("%s on %s"%(msg,self.line()))
896             instances.sort(timestamp_sort)
897             for i in instances: print i.line()
898         # show 'starting' regardless of verbose
899         if self.starting_ips:
900             header ("Starting IP addresses on %s"%self.line())
901             self.starting_ips.sort()
902             for starting in self.starting_ips: print starting
903         else:
904             header ("Empty 'starting' on %s"%self.line())
905
906 ############################################################
907 class Options: pass
908
909 class Substrate:
910
911     def __init__ (self):
912         self.options=Options()
913         self.options.dry_run=False
914         self.options.verbose=False
915         self.options.reboot=False
916         self.options.soft=False
917         self.test_box = TestBox (self.test_box_spec())
918         self.build_lxc_boxes = [ BuildLxcBox(h) for h in self.build_lxc_boxes_spec() ]
919         self.plc_lxc_boxes = [ PlcLxcBox (h,m) for (h,m) in self.plc_lxc_boxes_spec ()]
920         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
921         self._sensed=False
922
923         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs",self)
924         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes",self)
925         
926         self.build_boxes = self.build_lxc_boxes
927         self.plc_boxes = self.plc_lxc_boxes
928         self.default_boxes = self.plc_boxes + self.qemu_boxes
929         self.all_boxes = self.build_boxes + [ self.test_box ] + self.plc_boxes + self.qemu_boxes
930
931     def summary_line (self):
932         msg  = "["
933         msg += " %d xp"%len(self.plc_lxc_boxes)
934         msg += " %d tried plc boxes"%len(self.plc_boxes)
935         msg += "]"
936         return msg
937
938     def fqdn (self, hostname):
939         if hostname.find('.')<0: return "%s.%s"%(hostname,self.domain())
940         return hostname
941
942     # return True if actual sensing takes place
943     def sense (self,force=False):
944         if self._sensed and not force: return False
945         print 'Sensing local substrate...',
946         for b in self.default_boxes: b.sense(self.options)
947         print 'Done'
948         self._sensed=True
949         return True
950
951     def list (self, verbose=False):
952         for b in self.default_boxes:
953             b.list()
954
955     def add_dummy_plc (self, plc_boxname, plcname):
956         for pb in self.plc_boxes:
957             if pb.hostname==plc_boxname:
958                 pb.add_dummy(plcname)
959                 return True
960     def add_dummy_qemu (self, qemu_boxname, qemuname):
961         for qb in self.qemu_boxes:
962             if qb.hostname==qemu_boxname:
963                 qb.add_dummy(qemuname)
964                 return True
965
966     def add_starting_dummy (self, bname, vname):
967         return self.add_dummy_plc (bname, vname) or self.add_dummy_qemu (bname, vname)
968
969     ########## 
970     def provision (self,plcs,options):
971         try:
972             # attach each plc to a plc box and an IP address
973             plcs = [ self.provision_plc (plc,options) for plc in plcs ]
974             # attach each node/qemu to a qemu box with an IP address
975             plcs = [ self.provision_qemus (plc,options) for plc in plcs ]
976             # update the SFA spec accordingly
977             plcs = [ self.localize_sfa_rspec(plc,options) for plc in plcs ]
978             self.list()
979             return plcs
980         except Exception, e:
981             print '* Could not provision this test on current substrate','--',e,'--','exiting'
982             traceback.print_exc()
983             sys.exit(1)
984
985     # it is expected that a couple of options like ips_bplc and ips_vplc 
986     # are set or unset together
987     @staticmethod
988     def check_options (x,y):
989         if not x and not y: return True
990         return len(x)==len(y)
991
992     # find an available plc box (or make space)
993     # and a free IP address (using options if present)
994     def provision_plc (self, plc, options):
995         
996         assert Substrate.check_options (options.ips_bplc, options.ips_vplc)
997
998         #### let's find an IP address for that plc
999         # look in options 
1000         if options.ips_vplc:
1001             # this is a rerun
1002             # we don't check anything here, 
1003             # it is the caller's responsability to cleanup and make sure this makes sense
1004             plc_boxname = options.ips_bplc.pop()
1005             vplc_hostname=options.ips_vplc.pop()
1006         else:
1007             if self.sense(): self.list()
1008             plc_boxname=None
1009             vplc_hostname=None
1010             # try to find an available IP 
1011             self.vplc_pool.sense()
1012             couple=self.vplc_pool.next_free()
1013             if couple:
1014                 (vplc_hostname,unused)=couple
1015             #### we need to find one plc box that still has a slot
1016             max_free=0
1017             # use the box that has max free spots for load balancing
1018             for pb in self.plc_boxes:
1019                 free=pb.free_slots()
1020                 if free>max_free:
1021                     plc_boxname=pb.hostname
1022                     max_free=free
1023             # if there's no available slot in the plc_boxes, or we need a free IP address
1024             # make space by killing the oldest running instance
1025             if not plc_boxname or not vplc_hostname:
1026                 # find the oldest of all our instances
1027                 all_plc_instances=reduce(lambda x, y: x+y, 
1028                                          [ pb.plc_instances for pb in self.plc_boxes ],
1029                                          [])
1030                 all_plc_instances.sort(timestamp_sort)
1031                 try:
1032                     plc_instance_to_kill=all_plc_instances[0]
1033                 except:
1034                     msg=""
1035                     if not plc_boxname: msg += " PLC boxes are full"
1036                     if not vplc_hostname: msg += " vplc IP pool exhausted"
1037                     msg += " %s"%self.summary_line()
1038                     raise Exception,"Cannot make space for a PLC instance:"+msg
1039                 freed_plc_boxname=plc_instance_to_kill.plc_box.hostname
1040                 freed_vplc_hostname=plc_instance_to_kill.vplcname()
1041                 message='killing oldest plc instance = %s on %s'%(plc_instance_to_kill.line(),
1042                                                                   freed_plc_boxname)
1043                 plc_instance_to_kill.kill()
1044                 # use this new plcbox if that was the problem
1045                 if not plc_boxname:
1046                     plc_boxname=freed_plc_boxname
1047                 # ditto for the IP address
1048                 if not vplc_hostname:
1049                     vplc_hostname=freed_vplc_hostname
1050                     # record in pool as mine
1051                     self.vplc_pool.set_mine(vplc_hostname)
1052
1053         # 
1054         self.add_dummy_plc(plc_boxname,plc['name'])
1055         vplc_ip = self.vplc_pool.get_ip(vplc_hostname)
1056         self.vplc_pool.add_starting(vplc_hostname, plc_boxname)
1057
1058         #### compute a helpful vserver name
1059         # remove domain in hostname
1060         vplc_short = short_hostname(vplc_hostname)
1061         vservername = "%s-%d-%s" % (options.buildname,plc['index'],vplc_short)
1062         plc_name = "%s_%s"%(plc['name'],vplc_short)
1063
1064         utils.header( 'PROVISION plc %s in box %s at IP %s as %s'%\
1065                           (plc['name'],plc_boxname,vplc_hostname,vservername))
1066
1067         #### apply in the plc_spec
1068         # # informative
1069         # label=options.personality.replace("linux","")
1070         mapper = {'plc': [ ('*' , {'host_box':plc_boxname,
1071                                    # 'name':'%s-'+label,
1072                                    'name': plc_name,
1073                                    'vservername':vservername,
1074                                    'vserverip':vplc_ip,
1075 #                                   'settings': {
1076                                    'settings:PLC_DB_HOST':vplc_hostname,
1077                                    'settings:PLC_API_HOST':vplc_hostname,
1078                                    'settings:PLC_BOOT_HOST':vplc_hostname,
1079                                    'settings:PLC_WWW_HOST':vplc_hostname,
1080                                    'settings:PLC_NET_DNS1' : self.network_settings() [ 'interface_fields:dns1' ],
1081                                    'settings:PLC_NET_DNS2' : self.network_settings() [ 'interface_fields:dns2' ],
1082 #                                      }
1083                                    } ) ]
1084                   }
1085
1086
1087         # mappers only work on a list of plcs
1088         return TestMapper([plc],options).map(mapper)[0]
1089
1090     ##########
1091     def provision_qemus (self, plc, options):
1092
1093         assert Substrate.check_options (options.ips_bnode, options.ips_vnode)
1094
1095         test_mapper = TestMapper ([plc], options)
1096         nodenames = test_mapper.node_names()
1097         maps=[]
1098         for nodename in nodenames:
1099
1100             if options.ips_vnode:
1101                 # as above, it's a rerun, take it for granted
1102                 qemu_boxname=options.ips_bnode.pop()
1103                 vnode_hostname=options.ips_vnode.pop()
1104             else:
1105                 if self.sense(): self.list()
1106                 qemu_boxname=None
1107                 vnode_hostname=None
1108                 # try to find an available IP 
1109                 self.vnode_pool.sense()
1110                 couple=self.vnode_pool.next_free()
1111                 if couple:
1112                     (vnode_hostname,unused)=couple
1113                 # find a physical box
1114                 max_free=0
1115                 # use the box that has max free spots for load balancing
1116                 for qb in self.qemu_boxes:
1117                     free=qb.free_slots()
1118                     if free>max_free:
1119                         qemu_boxname=qb.hostname
1120                         max_free=free
1121                 # if we miss the box or the IP, kill the oldest instance
1122                 if not qemu_boxname or not vnode_hostname:
1123                 # find the oldest of all our instances
1124                     all_qemu_instances=reduce(lambda x, y: x+y, 
1125                                               [ qb.qemu_instances for qb in self.qemu_boxes ],
1126                                               [])
1127                     all_qemu_instances.sort(timestamp_sort)
1128                     try:
1129                         qemu_instance_to_kill=all_qemu_instances[0]
1130                     except:
1131                         msg=""
1132                         if not qemu_boxname: msg += " QEMU boxes are full"
1133                         if not vnode_hostname: msg += " vnode IP pool exhausted" 
1134                         msg += " %s"%self.summary_line()
1135                         raise Exception,"Cannot make space for a QEMU instance:"+msg
1136                     freed_qemu_boxname=qemu_instance_to_kill.qemu_box.hostname
1137                     freed_vnode_hostname=short_hostname(qemu_instance_to_kill.nodename)
1138                     # kill it
1139                     message='killing oldest qemu node = %s on %s'%(qemu_instance_to_kill.line(),
1140                                                                    freed_qemu_boxname)
1141                     qemu_instance_to_kill.kill()
1142                     # use these freed resources where needed
1143                     if not qemu_boxname:
1144                         qemu_boxname=freed_qemu_boxname
1145                     if not vnode_hostname:
1146                         vnode_hostname=freed_vnode_hostname
1147                         self.vnode_pool.set_mine(vnode_hostname)
1148
1149             self.add_dummy_qemu (qemu_boxname,vnode_hostname)
1150             mac=self.vnode_pool.retrieve_userdata(vnode_hostname)
1151             ip=self.vnode_pool.get_ip (vnode_hostname)
1152             self.vnode_pool.add_starting(vnode_hostname,qemu_boxname)
1153
1154             vnode_fqdn = self.fqdn(vnode_hostname)
1155             nodemap={'host_box':qemu_boxname,
1156                      'node_fields:hostname':vnode_fqdn,
1157                      'interface_fields:ip':ip, 
1158                      'ipaddress_fields:ip_addr':ip, 
1159                      'interface_fields:mac':mac,
1160                      }
1161             nodemap.update(self.network_settings())
1162             maps.append ( (nodename, nodemap) )
1163
1164             utils.header("PROVISION node %s in box %s at IP %s with MAC %s"%\
1165                              (nodename,qemu_boxname,vnode_hostname,mac))
1166
1167         return test_mapper.map({'node':maps})[0]
1168
1169     def localize_sfa_rspec (self,plc,options):
1170        
1171         plc['sfa']['settings']['SFA_REGISTRY_HOST'] = plc['settings']['PLC_DB_HOST']
1172         plc['sfa']['settings']['SFA_AGGREGATE_HOST'] = plc['settings']['PLC_DB_HOST']
1173         plc['sfa']['settings']['SFA_SM_HOST'] = plc['settings']['PLC_DB_HOST']
1174         plc['sfa']['settings']['SFA_DB_HOST'] = plc['settings']['PLC_DB_HOST']
1175         plc['sfa']['settings']['SFA_PLC_URL'] = 'https://%s:443/PLCAPI/' % plc['settings']['PLC_API_HOST']
1176         return plc
1177
1178     #################### release:
1179     def release (self,options):
1180         self.vplc_pool.release_my_starting()
1181         self.vnode_pool.release_my_starting()
1182         pass
1183
1184     #################### show results for interactive mode
1185     def get_box (self,boxname):
1186         for b in self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box] :
1187             if b.shortname()==boxname:                          return b
1188             try:
1189                 if b.shortname()==boxname.split('.')[0]:        return b
1190             except: pass
1191         print "Could not find box %s"%boxname
1192         return None
1193
1194     # deal with the mix of boxes and names and stores the current focus 
1195     # as a list of Box instances in self.focus_all
1196     def normalize (self, box_or_names):
1197         self.focus_all=[]
1198         for box in box_or_names:
1199             if not isinstance(box,Box): box=self.get_box(box)
1200             if not box: 
1201                 print 'Warning - could not handle box',box
1202             self.focus_all.append(box)
1203         # elaborate by type
1204         self.focus_build = [ x for x in self.focus_all if isinstance(x,BuildBox) ]
1205         self.focus_plc = [ x for x in self.focus_all if isinstance(x,PlcBox) ]
1206         self.focus_qemu = [ x for x in self.focus_all if isinstance(x,QemuBox) ]
1207                              
1208     def list_boxes(self):
1209         print 'Sensing',
1210         for box in self.focus_all:
1211             box.sense(self.options)
1212         print 'Done'
1213         for box in self.focus_all:
1214             box.list(self.options.verbose)
1215
1216     def reboot_boxes(self):
1217         for box in self.focus_all:
1218             box.reboot(self.options)
1219
1220     def sanity_check (self):
1221         print 'Sanity check'
1222         self.sanity_check_plc()
1223         self.sanity_check_qemu()
1224
1225     def sanity_check_plc (self):
1226         pass
1227
1228     def sanity_check_qemu (self):
1229         all_nodes=[]
1230         for box in self.focus_qemu:
1231             all_nodes += box.node_names()
1232         hash={}
1233         for node in all_nodes:
1234             if node not in hash: hash[node]=0
1235             hash[node]+=1
1236         for (node,count) in hash.items():
1237             if count!=1: print 'WARNING - duplicate node',node
1238         
1239
1240     ####################
1241     # can be run as a utility to probe/display/manage the local infrastructure
1242     def main (self):
1243         parser=OptionParser()
1244         parser.add_option ('-r',"--reboot",action='store_true',dest='reboot',default=False,
1245                            help='reboot mode (use shutdown -r)')
1246         parser.add_option ('-s',"--soft",action='store_true',dest='soft',default=False,
1247                            help='soft mode for reboot (terminates processes)')
1248         parser.add_option ('-t',"--testbox",action='store_true',dest='testbox',default=False,
1249                            help='add test box') 
1250         parser.add_option ('-b',"--build",action='store_true',dest='builds',default=False,
1251                            help='add build boxes')
1252         parser.add_option ('-p',"--plc",action='store_true',dest='plcs',default=False,
1253                            help='add plc boxes')
1254         parser.add_option ('-q',"--qemu",action='store_true',dest='qemus',default=False,
1255                            help='add qemu boxes') 
1256         parser.add_option ('-a',"--all",action='store_true',dest='all',default=False,
1257                            help='address all known  boxes, like -b -t -p -q')
1258         parser.add_option ('-v',"--verbose",action='store_true',dest='verbose',default=False,
1259                            help='verbose mode')
1260         parser.add_option ('-n',"--dry_run",action='store_true',dest='dry_run',default=False,
1261                            help='dry run mode')
1262         (self.options,args)=parser.parse_args()
1263
1264         boxes=args
1265         if self.options.testbox: boxes += [self.test_box]
1266         if self.options.builds: boxes += self.build_boxes
1267         if self.options.plcs: boxes += self.plc_boxes
1268         if self.options.qemus: boxes += self.qemu_boxes
1269         if self.options.all: boxes += self.all_boxes
1270         
1271         global verbose
1272         verbose=self.options.verbose
1273         # default scope is -b -p -q -t
1274         if not boxes:
1275             boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box]
1276
1277         self.normalize (boxes)
1278
1279         if self.options.reboot:
1280             self.reboot_boxes ()
1281         else:
1282             self.list_boxes ()
1283             self.sanity_check ()