probe for boxes memory
[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 %s"%(self.fedora(),self.memory())
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         composite_command += [ ";" , "echo", Box.separator , ";" ]
294         composite_command += [ "grep", "MemTotal", "/proc/meminfo" ]
295
296         # due to colons and all, this is going wrong on the local box (typically testmaster)
297         # I am reluctant to change TestSsh as it might break all over the place, so
298         if self.test_ssh().is_local():
299             probe_argv = [ "bash", "-c", " ".join (composite_command) ]
300         else:
301             probe_argv=self.test_ssh().actual_argv(composite_command)
302         composite=self.backquote ( probe_argv, trash_err=True )
303         self._hostname = self._uptime = self._uname = self._fedora = self._memory = "** Unknown **"
304         if not composite: 
305             print "root@%s unreachable"%self.hostname
306             self._probed=''
307         else:
308             try:
309                 pieces = composite.split(Box.separator)
310                 pieces = [ x.strip() for x in pieces ]
311                 # get raw data
312                 [hostname, uptime, uname, fedora, memory] = pieces
313                 # customize
314                 self._hostname = hostname
315                 self._uptime = ', '.join([ x.strip() for x in uptime.split(',')[2:]]).replace("load average","load")
316                 self._uname = uname
317                 self._fedora = fedora.replace("Fedora release ","f").split(" ")[0]
318                 # translate into Mb
319                 self._memory = int(memory.split()[1])/(1024)
320             except:
321                 import traceback
322                 print 'BEG issue with pieces',pieces
323                 traceback.print_exc()
324                 print 'END issue with pieces',pieces
325             self._probed=self._hostname
326         return self._probed
327
328     # use argv=['bash','-c',"the command line"]
329     def uptime(self):
330         self.probe()
331         if hasattr(self,'_uptime') and self._uptime: return self._uptime
332         return '*unprobed* uptime'
333     def uname(self):
334         self.probe()
335         if hasattr(self,'_uname') and self._uname: return self._uname
336         return '*unprobed* uname'
337     def fedora(self):
338         self.probe()
339         if hasattr(self,'_fedora') and self._fedora: return self._fedora
340         return '*unprobed* fedora'
341     def memory(self):
342         self.probe()
343         if hasattr(self,'_memory') and self._memory: return "%s Mb"%self._memory
344         return '*unprobed* memory'
345
346     def run(self,argv,message=None,trash_err=False,dry_run=False):
347         if dry_run:
348             print 'DRY_RUN:',
349             print " ".join(argv)
350             return 0
351         else:
352             header(message)
353             if not trash_err:
354                 return subprocess.call(argv)
355             else:
356                 return subprocess.call(argv,stderr=file('/dev/null','w'))
357                 
358     def run_ssh (self, argv, message, trash_err=False, dry_run=False):
359         ssh_argv = self.test_ssh().actual_argv(argv)
360         result=self.run (ssh_argv, message, trash_err, dry_run=dry_run)
361         if result!=0:
362             print "WARNING: failed to run %s on %s"%(" ".join(argv),self.hostname)
363         return result
364
365     def backquote (self, argv, trash_err=False):
366         # print 'running backquote',argv
367         if not trash_err:
368             result= subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
369         else:
370             result= subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
371         return result
372
373     # if you have any shell-expanded arguments like *
374     # and if there's any chance the command is adressed to the local host
375     def backquote_ssh (self, argv, trash_err=False):
376         if not self.probe(): return ''
377         return self.backquote( self.test_ssh().actual_argv(argv), trash_err)
378
379 ############################################################
380 class BuildInstance:
381     def __init__ (self, buildname, pid, buildbox):
382         self.buildname=buildname
383         self.buildbox=buildbox
384         self.pids=[pid]
385
386     def add_pid(self,pid):
387         self.pids.append(pid)
388
389     def line (self):
390         return "== %s == (pids=%r)"%(self.buildname,self.pids)
391
392 class BuildBox (Box):
393     def __init__ (self,hostname):
394         Box.__init__(self,hostname)
395         self.build_instances=[]
396
397     def add_build (self,buildname,pid):
398         for build in self.build_instances:
399             if build.buildname==buildname: 
400                 build.add_pid(pid)
401                 return
402         self.build_instances.append(BuildInstance(buildname, pid, self))
403
404     def list(self, verbose=False):
405         if not self.build_instances: 
406             header ('No build process on %s (%s)'%(self.hostname_fedora(),self.uptime()))
407         else:
408             header ("Builds on %s (%s)"%(self.hostname_fedora(),self.uptime()))
409             for b in self.build_instances: 
410                 header (b.line(),banner=False)
411
412     def reboot (self, options):
413         if not options.soft:
414             Box.reboot(self,options)
415         else:
416             self.soft_reboot (options)
417
418 build_matcher=re.compile("\s*(?P<pid>[0-9]+).*-[bo]\s+(?P<buildname>[^\s]+)(\s|\Z)")
419 build_matcher_initvm=re.compile("\s*(?P<pid>[0-9]+).*initvm.*\s+(?P<buildname>[^\s]+)\s*\Z")
420
421 class BuildLxcBox (BuildBox):
422     def soft_reboot (self, options):
423             command=['pkill','lbuild']
424             self.run_ssh(command,"Terminating vbuild processes",dry_run=options.dry_run)
425
426     # inspect box and find currently running builds
427     def sense(self, options):
428         print 'xb',
429         pids=self.backquote_ssh(['pgrep','lbuild'],trash_err=True)
430         if not pids: return
431         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
432         ps_lines=self.backquote_ssh (command).split('\n')
433         for line in ps_lines:
434             if not line.strip() or line.find('PID')>=0: continue
435             m=build_matcher.match(line)
436             if m: 
437                 date=time.strftime('%Y-%m-%d',time.localtime(time.time()))
438                 buildname=m.group('buildname').replace('@DATE@',date)
439                 self.add_build (buildname,m.group('pid'))
440                 continue
441             m=build_matcher_initvm.match(line)
442             if m: 
443                 # buildname is expansed here
444                 self.add_build (buildname,m.group('pid'))
445                 continue
446             header('BuildLxcBox.sense: command %r returned line that failed to match'%command)
447             header(">>%s<<"%line)
448     
449 ############################################################
450 class PlcInstance:
451     def __init__ (self, plcbox):
452         self.plc_box=plcbox
453         # unknown yet
454         self.timestamp=0
455         
456     def set_timestamp (self,timestamp): self.timestamp=timestamp
457     def set_now (self): self.timestamp=int(time.time())
458     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
459
460 class PlcLxcInstance (PlcInstance):
461     # does lxc have a context id of any kind ?
462     def __init__ (self, plcbox, lxcname, pid):
463         PlcInstance.__init__(self, plcbox)
464         self.lxcname = lxcname
465         self.pid = pid
466
467     def vplcname (self):
468         return self.lxcname.split('-')[-1]
469     def buildname (self):
470         return self.lxcname.rsplit('-',2)[0]
471
472     def line (self):
473         msg="== %s =="%(self.vplcname())
474         msg += " [=%s]"%self.lxcname
475         if self.pid==-1:  msg+=" not (yet?) running"
476         else:              msg+=" (pid=%s)"%self.pid
477         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
478         else:              msg += " *unknown timestamp*"
479         return msg
480
481     def kill (self):
482         command="rsync lxc-driver.sh  %s:/root"%self.plc_box.hostname
483         commands.getstatusoutput(command)
484         msg="lxc container stopping %s on %s"%(self.lxcname,self.plc_box.hostname)
485         self.plc_box.run_ssh(['/root/lxc-driver.sh','-c','stop_lxc','-n',self.lxcname],msg)
486         self.plc_box.forget(self)
487
488 ##########
489 class PlcBox (Box):
490     def __init__ (self, hostname, max_plcs):
491         Box.__init__(self,hostname)
492         self.plc_instances=[]
493         self.max_plcs=max_plcs
494
495     def free_slots (self):
496         return self.max_plcs - len(self.plc_instances)
497
498     # fill one slot even though this one is not started yet
499     def add_dummy (self, plcname):
500         dummy=PlcLxcInstance(self,'dummy_'+plcname,0)
501         dummy.set_now()
502         self.plc_instances.append(dummy)
503
504     def forget (self, plc_instance):
505         self.plc_instances.remove(plc_instance)
506
507     def reboot (self, options):
508         if not options.soft:
509             Box.reboot(self,options)
510         else:
511             self.soft_reboot (options)
512
513     def list(self, verbose=False):
514         if not self.plc_instances: 
515             header ('No plc running on %s'%(self.line()))
516         else:
517             header ("Active plc VMs on %s"%self.line())
518             self.plc_instances.sort(timestamp_sort)
519             for p in self.plc_instances: 
520                 header (p.line(),banner=False)
521
522 ## we do not this at INRIA any more
523 class PlcLxcBox (PlcBox):
524
525     def add_lxc (self,lxcname,pid):
526         for plc in self.plc_instances:
527             if plc.lxcname==lxcname:
528                 header("WARNING, duplicate myplc %s running on %s"%\
529                            (lxcname,self.hostname),banner=False)
530                 return
531         self.plc_instances.append(PlcLxcInstance(self,lxcname,pid))    
532
533
534     # a line describing the box
535     def line(self): 
536         return "%s [max=%d,free=%d] (%s)"%(self.hostname_fedora(virt="lxc"), 
537                                            self.max_plcs,self.free_slots(),
538                                            self.uptime(),
539                                            )
540     
541     def plc_instance_by_lxcname (self, lxcname):
542         for p in self.plc_instances:
543             if p.lxcname==lxcname: return p
544         return None
545     
546     # essentially shutdown all running containers
547     def soft_reboot (self, options):
548         command="rsync lxc-driver.sh  %s:/root"%self.hostname
549         commands.getstatusoutput(command)
550         self.run_ssh(['/root/lxc-driver.sh','-c','stop_all'],"Stopping all running lxc containers on %s"%(self.hostname,),
551                      dry_run=options.dry_run)
552
553
554     # sense is expected to fill self.plc_instances with PlcLxcInstance's 
555     # to describe the currently running VM's
556     def sense (self, options):
557         print "xp",
558         command="rsync lxc-driver.sh  %s:/root"%self.hostname
559         commands.getstatusoutput(command)
560         command=['/root/lxc-driver.sh','-c','sense_all']
561         lxc_stat = self.backquote_ssh (command)
562         for lxc_line in lxc_stat.split("\n"):
563             if not lxc_line: continue
564             lxcname=lxc_line.split(";")[0]
565             pid=lxc_line.split(";")[1]
566             timestamp=lxc_line.split(";")[2]
567             self.add_lxc(lxcname,pid)
568             try: timestamp=int(timestamp)
569             except: timestamp=0
570             p=self.plc_instance_by_lxcname(lxcname)
571             if not p:
572                 print 'WARNING zombie plc',self.hostname,lxcname
573                 print '... was expecting',lxcname,'in',[i.lxcname for i in self.plc_instances]
574                 continue
575             p.set_timestamp(timestamp)
576
577 ############################################################
578 class QemuInstance: 
579     def __init__ (self, nodename, pid, qemubox):
580         self.nodename=nodename
581         self.pid=pid
582         self.qemu_box=qemubox
583         # not known yet
584         self.buildname=None
585         self.timestamp=0
586         
587     def set_buildname (self,buildname): self.buildname=buildname
588     def set_timestamp (self,timestamp): self.timestamp=timestamp
589     def set_now (self): self.timestamp=int(time.time())
590     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
591     
592     def line (self):
593         msg = "== %s =="%(short_hostname(self.nodename))
594         msg += " [=%s]"%self.buildname
595         if self.pid:       msg += " (pid=%s)"%self.pid
596         else:              msg += " not (yet?) running"
597         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
598         else:              msg += " *unknown timestamp*"
599         return msg
600     
601     def kill(self):
602         if self.pid==0: 
603             print "cannot kill qemu %s with pid==0"%self.nodename
604             return
605         msg="Killing qemu %s with pid=%s on box %s"%(self.nodename,self.pid,self.qemu_box.hostname)
606         self.qemu_box.run_ssh(['kill',"%s"%self.pid],msg)
607         self.qemu_box.forget(self)
608
609
610 class QemuBox (Box):
611     def __init__ (self, hostname, max_qemus):
612         Box.__init__(self,hostname)
613         self.qemu_instances=[]
614         self.max_qemus=max_qemus
615
616     def add_node (self,nodename,pid):
617         for qemu in self.qemu_instances:
618             if qemu.nodename==nodename: 
619                 header("WARNING, duplicate qemu %s running on %s"%\
620                            (nodename,self.hostname), banner=False)
621                 return
622         self.qemu_instances.append(QemuInstance(nodename,pid,self))
623
624     def node_names (self):
625         return [ qi.nodename for qi in self.qemu_instances ]
626
627     def forget (self, qemu_instance):
628         self.qemu_instances.remove(qemu_instance)
629
630     # fill one slot even though this one is not started yet
631     def add_dummy (self, nodename):
632         dummy=QemuInstance('dummy_'+nodename,0,self)
633         dummy.set_now()
634         self.qemu_instances.append(dummy)
635
636     def line (self):
637         return "%s [max=%d,free=%d] (%s) %s"%(
638             self.hostname_fedora(virt="qemu"), self.max_qemus,self.free_slots(),
639             self.uptime(),self.driver())
640
641     def list(self, verbose=False):
642         if not self.qemu_instances: 
643             header ('No qemu on %s'%(self.line()))
644         else:
645             header ("Qemus on %s"%(self.line()))
646             self.qemu_instances.sort(timestamp_sort)
647             for q in self.qemu_instances: 
648                 header (q.line(),banner=False)
649
650     def free_slots (self):
651         return self.max_qemus - len(self.qemu_instances)
652
653     def driver(self):
654         if hasattr(self,'_driver') and self._driver: return self._driver
655         return '*undef* driver'
656
657     def qemu_instance_by_pid (self,pid):
658         for q in self.qemu_instances:
659             if q.pid==pid: return q
660         return None
661
662     def qemu_instance_by_nodename_buildname (self,nodename,buildname):
663         for q in self.qemu_instances:
664             if q.nodename==nodename and q.buildname==buildname:
665                 return q
666         return None
667
668     def reboot (self, options):
669         if not options.soft:
670             Box.reboot(self,options)
671         else:
672             self.run_ssh(['pkill','qemu'],"Killing qemu instances",
673                          dry_run=options.dry_run)
674
675     matcher=re.compile("\s*(?P<pid>[0-9]+).*-cdrom\s+(?P<nodename>[^\s]+)\.iso")
676     def sense(self, options):
677         print 'qn',
678         modules=self.backquote_ssh(['lsmod']).split('\n')
679         self._driver='*NO kqemu/kvm_intel MODULE LOADED*'
680         for module in modules:
681             if module.find('kqemu')==0:
682                 self._driver='kqemu module loaded'
683             # kvm might be loaded without kvm_intel (we dont have AMD)
684             elif module.find('kvm_intel')==0:
685                 self._driver='kvm_intel OK'
686         ########## find out running pids
687         pids=self.backquote_ssh(['pgrep','qemu'])
688         if not pids: return
689         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
690         ps_lines = self.backquote_ssh (command).split("\n")
691         for line in ps_lines:
692             if not line.strip() or line.find('PID') >=0 : continue
693             m=QemuBox.matcher.match(line)
694             if m: 
695                 self.add_node (m.group('nodename'),m.group('pid'))
696                 continue
697             header('QemuBox.sense: command %r returned line that failed to match'%command)
698             header(">>%s<<"%line)
699         ########## retrieve alive instances and map to build
700         live_builds=[]
701         command=['grep','.','/vservers/*/*/qemu.pid','/dev/null']
702         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
703         for pid_line in pid_lines:
704             if not pid_line.strip(): continue
705             # expect <build>/<nodename>/qemu.pid:<pid>pid
706             try:
707                 (_,__,buildname,nodename,tail)=pid_line.split('/')
708                 (_,pid)=tail.split(':')
709                 q=self.qemu_instance_by_pid (pid)
710                 if not q: continue
711                 q.set_buildname(buildname)
712                 live_builds.append(buildname)
713             except: print 'WARNING, could not parse pid line',pid_line
714         # retrieve timestamps
715         if not live_builds: return
716         command=   ['grep','.']
717         command += ['/vservers/%s/*/timestamp'%b for b in live_builds]
718         command += ['/dev/null']
719         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
720         for ts_line in ts_lines:
721             if not ts_line.strip(): continue
722             # expect <build>/<nodename>/timestamp:<timestamp>
723             try:
724                 (_,__,buildname,nodename,tail)=ts_line.split('/')
725                 nodename=nodename.replace('qemu-','')
726                 (_,timestamp)=tail.split(':')
727                 timestamp=int(timestamp)
728                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
729                 if not q: 
730                     print 'WARNING zombie qemu',self.hostname,ts_line
731                     print '... was expecting (',short_hostname(nodename),buildname,') in',\
732                         [ (short_hostname(i.nodename),i.buildname) for i in self.qemu_instances ]
733                     continue
734                 q.set_timestamp(timestamp)
735             except:  print 'WARNING, could not parse ts line',ts_line
736
737 ####################
738 class TestInstance:
739     def __init__ (self, buildname, pid=0):
740         self.pids=[]
741         if pid!=0: self.pid.append(pid)
742         self.buildname=buildname
743         # latest trace line
744         self.trace=''
745         # has a KO test
746         self.broken_steps=[]
747         self.timestamp = 0
748
749     def set_timestamp (self,timestamp): self.timestamp=timestamp
750     def set_now (self): self.timestamp=int(time.time())
751     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
752
753     def is_running (self): return len(self.pids) != 0
754
755     def add_pid (self,pid):
756         self.pids.append(pid)
757     def set_broken (self, plcindex, step): 
758         self.broken_steps.append ( (plcindex, step,) )
759
760     def second_letter (self):
761         if not self.broken_steps: return '='
762         else:
763             really_broken = [ step for (i,step) in self.broken_steps if '_ignore' not in step ]
764             # W is for warning like what's in the build mail
765             if len(really_broken)==0: return 'W'
766             else: return 'B'
767
768     def line (self):
769         # make up a 2-letter sign
770         # first letter : '=', unless build is running : '*'
771         double = '*' if self.pids else '='
772         # second letter : '=' if fine, 'W' for warnings (only ignored steps) 'B' for broken
773         letter2 = self.second_letter()
774         double += letter2
775         msg = " %s %s =="%(double,self.buildname)
776         if not self.pids:       pass
777         elif len(self.pids)==1: msg += " (pid=%s)"%self.pids[0]
778         else:                   msg += " !!!pids=%s!!!"%self.pids
779         msg += " @%s"%self.pretty_timestamp()
780         if letter2 != '=':
781             msg2 = ( ' BROKEN' if letter2 == 'B' else ' WARNING' )
782             # sometimes we have an empty plcindex
783             msg += " [%s="%msg2 + " ".join( [ "%s@%s"%(s,i) if i else s for (i,s) in self.broken_steps ] ) + "]"
784         return msg
785
786 class TestBox (Box):
787     def __init__ (self,hostname):
788         Box.__init__(self,hostname)
789         self.starting_ips=[]
790         self.test_instances=[]
791
792     def reboot (self, options):
793         # can't reboot a vserver VM
794         self.run_ssh (['pkill','run_log'],"Terminating current runs",
795                       dry_run=options.dry_run)
796         self.run_ssh (['rm','-f',Starting.location],"Cleaning %s"%Starting.location,
797                       dry_run=options.dry_run)
798
799     def get_test (self, buildname):
800         for i in self.test_instances:
801             if i.buildname==buildname: return i
802
803     # we scan ALL remaining test results, even the ones not running
804     def add_timestamp (self, buildname, timestamp):
805         i=self.get_test(buildname)
806         if i:   
807             i.set_timestamp(timestamp)
808         else:   
809             i=TestInstance(buildname,0)
810             i.set_timestamp(timestamp)
811             self.test_instances.append(i)
812
813     def add_running_test (self, pid, buildname):
814         i=self.get_test(buildname)
815         if not i:
816             self.test_instances.append (TestInstance (buildname,pid))
817             return
818         if i.pids:
819             print "WARNING: 2 concurrent tests run on same build %s"%buildname
820         i.add_pid (pid)
821
822     def add_broken (self, buildname, plcindex, step):
823         i=self.get_test(buildname)
824         if not i:
825             i=TestInstance(buildname)
826             self.test_instances.append(i)
827         i.set_broken(plcindex, step)
828
829     matcher_proc=re.compile (".*/proc/(?P<pid>[0-9]+)/cwd.*/root/(?P<buildname>[^/]+)$")
830     matcher_grep=re.compile ("/root/(?P<buildname>[^/]+)/logs/trace.*:TRACE:\s*(?P<plcindex>[0-9]+).*step=(?P<step>\S+).*")
831     matcher_grep_missing=re.compile ("grep: /root/(?P<buildname>[^/]+)/logs/trace: No such file or directory")
832     def sense (self, options):
833         print 'tm',
834         self.starting_ips=[x for x in self.backquote_ssh(['cat',Starting.location], trash_err=True).strip().split('\n') if x]
835
836         # scan timestamps on all tests
837         # this is likely to not invoke ssh so we need to be a bit smarter to get * expanded
838         # xxx would make sense above too
839         command=['bash','-c',"grep . /root/*/timestamp /dev/null"]
840         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
841         for ts_line in ts_lines:
842             if not ts_line.strip(): continue
843             # expect /root/<buildname>/timestamp:<timestamp>
844             try:
845                 (ts_file,timestamp)=ts_line.split(':')
846                 ts_file=os.path.dirname(ts_file)
847                 buildname=os.path.basename(ts_file)
848                 timestamp=int(timestamp)
849                 t=self.add_timestamp(buildname,timestamp)
850             except:  print 'WARNING, could not parse ts line',ts_line
851
852         # let's try to be robust here -- tests that fail very early like e.g.
853         # "Cannot make space for a PLC instance: vplc IP pool exhausted", that occurs as part of provision
854         # will result in a 'trace' symlink to an inexisting 'trace-<>.txt' because no step has gone through
855         # simple 'trace' should exist though as it is created by run_log
856         command=['bash','-c',"grep KO /root/*/logs/trace /dev/null 2>&1" ]
857         trace_lines=self.backquote_ssh (command).split('\n')
858         for line in trace_lines:
859             if not line.strip(): continue
860             m=TestBox.matcher_grep_missing.match(line)
861             if m:
862                 buildname=m.group('buildname')
863                 self.add_broken(buildname,'','NO STEP DONE')
864                 continue
865             m=TestBox.matcher_grep.match(line)
866             if m: 
867                 buildname=m.group('buildname')
868                 plcindex=m.group('plcindex')
869                 step=m.group('step')
870                 self.add_broken(buildname,plcindex, step)
871                 continue
872             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
873             header(">>%s<<"%line)
874
875         pids = self.backquote_ssh (['pgrep','run_log'],trash_err=True)
876         if not pids: return
877         command=['ls','-ld'] + ["/proc/%s/cwd"%pid for pid in pids.split("\n") if pid]
878         ps_lines=self.backquote_ssh (command).split('\n')
879         for line in ps_lines:
880             if not line.strip(): continue
881             m=TestBox.matcher_proc.match(line)
882             if m: 
883                 pid=m.group('pid')
884                 buildname=m.group('buildname')
885                 self.add_running_test(pid, buildname)
886                 continue
887             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
888             header(">>%s<<"%line)
889         
890         
891     def line (self):
892         return self.hostname_fedora()
893
894     def list (self, verbose=False):
895         # verbose shows all tests
896         if verbose:
897             instances = self.test_instances
898             msg="tests"
899         else:
900             instances = [ i for i in self.test_instances if i.is_running() ]
901             msg="running tests"
902
903         if not instances:
904             header ("No %s on %s"%(msg,self.line()))
905         else:
906             header ("%s on %s"%(msg,self.line()))
907             instances.sort(timestamp_sort)
908             for i in instances: print i.line()
909         # show 'starting' regardless of verbose
910         if self.starting_ips:
911             header ("Starting IP addresses on %s"%self.line())
912             self.starting_ips.sort()
913             for starting in self.starting_ips: print starting
914         else:
915             header ("Empty 'starting' on %s"%self.line())
916
917 ############################################################
918 class Options: pass
919
920 class Substrate:
921
922     def __init__ (self):
923         self.options=Options()
924         self.options.dry_run=False
925         self.options.verbose=False
926         self.options.reboot=False
927         self.options.soft=False
928         self.test_box = TestBox (self.test_box_spec())
929         self.build_lxc_boxes = [ BuildLxcBox(h) for h in self.build_lxc_boxes_spec() ]
930         self.plc_lxc_boxes = [ PlcLxcBox (h,m) for (h,m) in self.plc_lxc_boxes_spec ()]
931         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
932         self._sensed=False
933
934         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs",self)
935         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes",self)
936         
937         self.build_boxes = self.build_lxc_boxes
938         self.plc_boxes = self.plc_lxc_boxes
939         self.default_boxes = self.plc_boxes + self.qemu_boxes
940         self.all_boxes = self.build_boxes + [ self.test_box ] + self.plc_boxes + self.qemu_boxes
941
942     def summary_line (self):
943         msg  = "["
944         msg += " %d xp"%len(self.plc_lxc_boxes)
945         msg += " %d tried plc boxes"%len(self.plc_boxes)
946         msg += "]"
947         return msg
948
949     def fqdn (self, hostname):
950         if hostname.find('.')<0: return "%s.%s"%(hostname,self.domain())
951         return hostname
952
953     # return True if actual sensing takes place
954     def sense (self,force=False):
955         if self._sensed and not force: return False
956         print 'Sensing local substrate...',
957         for b in self.default_boxes: b.sense(self.options)
958         print 'Done'
959         self._sensed=True
960         return True
961
962     def list (self, verbose=False):
963         for b in self.default_boxes:
964             b.list()
965
966     def add_dummy_plc (self, plc_boxname, plcname):
967         for pb in self.plc_boxes:
968             if pb.hostname==plc_boxname:
969                 pb.add_dummy(plcname)
970                 return True
971     def add_dummy_qemu (self, qemu_boxname, qemuname):
972         for qb in self.qemu_boxes:
973             if qb.hostname==qemu_boxname:
974                 qb.add_dummy(qemuname)
975                 return True
976
977     def add_starting_dummy (self, bname, vname):
978         return self.add_dummy_plc (bname, vname) or self.add_dummy_qemu (bname, vname)
979
980     ########## 
981     def provision (self,plcs,options):
982         try:
983             # attach each plc to a plc box and an IP address
984             plcs = [ self.provision_plc (plc,options) for plc in plcs ]
985             # attach each node/qemu to a qemu box with an IP address
986             plcs = [ self.provision_qemus (plc,options) for plc in plcs ]
987             # update the SFA spec accordingly
988             plcs = [ self.localize_sfa_rspec(plc,options) for plc in plcs ]
989             self.list()
990             return plcs
991         except Exception, e:
992             print '* Could not provision this test on current substrate','--',e,'--','exiting'
993             traceback.print_exc()
994             sys.exit(1)
995
996     # it is expected that a couple of options like ips_bplc and ips_vplc 
997     # are set or unset together
998     @staticmethod
999     def check_options (x,y):
1000         if not x and not y: return True
1001         return len(x)==len(y)
1002
1003     # find an available plc box (or make space)
1004     # and a free IP address (using options if present)
1005     def provision_plc (self, plc, options):
1006         
1007         assert Substrate.check_options (options.ips_bplc, options.ips_vplc)
1008
1009         #### let's find an IP address for that plc
1010         # look in options 
1011         if options.ips_vplc:
1012             # this is a rerun
1013             # we don't check anything here, 
1014             # it is the caller's responsability to cleanup and make sure this makes sense
1015             plc_boxname = options.ips_bplc.pop()
1016             vplc_hostname=options.ips_vplc.pop()
1017         else:
1018             if self.sense(): self.list()
1019             plc_boxname=None
1020             vplc_hostname=None
1021             # try to find an available IP 
1022             self.vplc_pool.sense()
1023             couple=self.vplc_pool.next_free()
1024             if couple:
1025                 (vplc_hostname,unused)=couple
1026             #### we need to find one plc box that still has a slot
1027             max_free=0
1028             # use the box that has max free spots for load balancing
1029             for pb in self.plc_boxes:
1030                 free=pb.free_slots()
1031                 if free>max_free:
1032                     plc_boxname=pb.hostname
1033                     max_free=free
1034             # if there's no available slot in the plc_boxes, or we need a free IP address
1035             # make space by killing the oldest running instance
1036             if not plc_boxname or not vplc_hostname:
1037                 # find the oldest of all our instances
1038                 all_plc_instances=reduce(lambda x, y: x+y, 
1039                                          [ pb.plc_instances for pb in self.plc_boxes ],
1040                                          [])
1041                 all_plc_instances.sort(timestamp_sort)
1042                 try:
1043                     plc_instance_to_kill=all_plc_instances[0]
1044                 except:
1045                     msg=""
1046                     if not plc_boxname: msg += " PLC boxes are full"
1047                     if not vplc_hostname: msg += " vplc IP pool exhausted"
1048                     msg += " %s"%self.summary_line()
1049                     raise Exception,"Cannot make space for a PLC instance:"+msg
1050                 freed_plc_boxname=plc_instance_to_kill.plc_box.hostname
1051                 freed_vplc_hostname=plc_instance_to_kill.vplcname()
1052                 message='killing oldest plc instance = %s on %s'%(plc_instance_to_kill.line(),
1053                                                                   freed_plc_boxname)
1054                 plc_instance_to_kill.kill()
1055                 # use this new plcbox if that was the problem
1056                 if not plc_boxname:
1057                     plc_boxname=freed_plc_boxname
1058                 # ditto for the IP address
1059                 if not vplc_hostname:
1060                     vplc_hostname=freed_vplc_hostname
1061                     # record in pool as mine
1062                     self.vplc_pool.set_mine(vplc_hostname)
1063
1064         # 
1065         self.add_dummy_plc(plc_boxname,plc['name'])
1066         vplc_ip = self.vplc_pool.get_ip(vplc_hostname)
1067         self.vplc_pool.add_starting(vplc_hostname, plc_boxname)
1068
1069         #### compute a helpful vserver name
1070         # remove domain in hostname
1071         vplc_short = short_hostname(vplc_hostname)
1072         vservername = "%s-%d-%s" % (options.buildname,plc['index'],vplc_short)
1073         plc_name = "%s_%s"%(plc['name'],vplc_short)
1074
1075         utils.header( 'PROVISION plc %s in box %s at IP %s as %s'%\
1076                           (plc['name'],plc_boxname,vplc_hostname,vservername))
1077
1078         #### apply in the plc_spec
1079         # # informative
1080         # label=options.personality.replace("linux","")
1081         mapper = {'plc': [ ('*' , {'host_box':plc_boxname,
1082                                    # 'name':'%s-'+label,
1083                                    'name': plc_name,
1084                                    'vservername':vservername,
1085                                    'vserverip':vplc_ip,
1086 #                                   'settings': {
1087                                    'settings:PLC_DB_HOST':vplc_hostname,
1088                                    'settings:PLC_API_HOST':vplc_hostname,
1089                                    'settings:PLC_BOOT_HOST':vplc_hostname,
1090                                    'settings:PLC_WWW_HOST':vplc_hostname,
1091                                    'settings:PLC_NET_DNS1' : self.network_settings() [ 'interface_fields:dns1' ],
1092                                    'settings:PLC_NET_DNS2' : self.network_settings() [ 'interface_fields:dns2' ],
1093 #                                      }
1094                                    } ) ]
1095                   }
1096
1097
1098         # mappers only work on a list of plcs
1099         return TestMapper([plc],options).map(mapper)[0]
1100
1101     ##########
1102     def provision_qemus (self, plc, options):
1103
1104         assert Substrate.check_options (options.ips_bnode, options.ips_vnode)
1105
1106         test_mapper = TestMapper ([plc], options)
1107         nodenames = test_mapper.node_names()
1108         maps=[]
1109         for nodename in nodenames:
1110
1111             if options.ips_vnode:
1112                 # as above, it's a rerun, take it for granted
1113                 qemu_boxname=options.ips_bnode.pop()
1114                 vnode_hostname=options.ips_vnode.pop()
1115             else:
1116                 if self.sense(): self.list()
1117                 qemu_boxname=None
1118                 vnode_hostname=None
1119                 # try to find an available IP 
1120                 self.vnode_pool.sense()
1121                 couple=self.vnode_pool.next_free()
1122                 if couple:
1123                     (vnode_hostname,unused)=couple
1124                 # find a physical box
1125                 max_free=0
1126                 # use the box that has max free spots for load balancing
1127                 for qb in self.qemu_boxes:
1128                     free=qb.free_slots()
1129                     if free>max_free:
1130                         qemu_boxname=qb.hostname
1131                         max_free=free
1132                 # if we miss the box or the IP, kill the oldest instance
1133                 if not qemu_boxname or not vnode_hostname:
1134                 # find the oldest of all our instances
1135                     all_qemu_instances=reduce(lambda x, y: x+y, 
1136                                               [ qb.qemu_instances for qb in self.qemu_boxes ],
1137                                               [])
1138                     all_qemu_instances.sort(timestamp_sort)
1139                     try:
1140                         qemu_instance_to_kill=all_qemu_instances[0]
1141                     except:
1142                         msg=""
1143                         if not qemu_boxname: msg += " QEMU boxes are full"
1144                         if not vnode_hostname: msg += " vnode IP pool exhausted" 
1145                         msg += " %s"%self.summary_line()
1146                         raise Exception,"Cannot make space for a QEMU instance:"+msg
1147                     freed_qemu_boxname=qemu_instance_to_kill.qemu_box.hostname
1148                     freed_vnode_hostname=short_hostname(qemu_instance_to_kill.nodename)
1149                     # kill it
1150                     message='killing oldest qemu node = %s on %s'%(qemu_instance_to_kill.line(),
1151                                                                    freed_qemu_boxname)
1152                     qemu_instance_to_kill.kill()
1153                     # use these freed resources where needed
1154                     if not qemu_boxname:
1155                         qemu_boxname=freed_qemu_boxname
1156                     if not vnode_hostname:
1157                         vnode_hostname=freed_vnode_hostname
1158                         self.vnode_pool.set_mine(vnode_hostname)
1159
1160             self.add_dummy_qemu (qemu_boxname,vnode_hostname)
1161             mac=self.vnode_pool.retrieve_userdata(vnode_hostname)
1162             ip=self.vnode_pool.get_ip (vnode_hostname)
1163             self.vnode_pool.add_starting(vnode_hostname,qemu_boxname)
1164
1165             vnode_fqdn = self.fqdn(vnode_hostname)
1166             nodemap={'host_box':qemu_boxname,
1167                      'node_fields:hostname':vnode_fqdn,
1168                      'interface_fields:ip':ip, 
1169                      'ipaddress_fields:ip_addr':ip, 
1170                      'interface_fields:mac':mac,
1171                      }
1172             nodemap.update(self.network_settings())
1173             maps.append ( (nodename, nodemap) )
1174
1175             utils.header("PROVISION node %s in box %s at IP %s with MAC %s"%\
1176                              (nodename,qemu_boxname,vnode_hostname,mac))
1177
1178         return test_mapper.map({'node':maps})[0]
1179
1180     def localize_sfa_rspec (self,plc,options):
1181        
1182         plc['sfa']['settings']['SFA_REGISTRY_HOST'] = plc['settings']['PLC_DB_HOST']
1183         plc['sfa']['settings']['SFA_AGGREGATE_HOST'] = plc['settings']['PLC_DB_HOST']
1184         plc['sfa']['settings']['SFA_SM_HOST'] = plc['settings']['PLC_DB_HOST']
1185         plc['sfa']['settings']['SFA_DB_HOST'] = plc['settings']['PLC_DB_HOST']
1186         plc['sfa']['settings']['SFA_PLC_URL'] = 'https://%s:443/PLCAPI/' % plc['settings']['PLC_API_HOST']
1187         return plc
1188
1189     #################### release:
1190     def release (self,options):
1191         self.vplc_pool.release_my_starting()
1192         self.vnode_pool.release_my_starting()
1193         pass
1194
1195     #################### show results for interactive mode
1196     def get_box (self,boxname):
1197         for b in self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box] :
1198             if b.shortname()==boxname:                          return b
1199             try:
1200                 if b.shortname()==boxname.split('.')[0]:        return b
1201             except: pass
1202         print "Could not find box %s"%boxname
1203         return None
1204
1205     # deal with the mix of boxes and names and stores the current focus 
1206     # as a list of Box instances in self.focus_all
1207     def normalize (self, box_or_names):
1208         self.focus_all=[]
1209         for box in box_or_names:
1210             if not isinstance(box,Box): box=self.get_box(box)
1211             if not box: 
1212                 print 'Warning - could not handle box',box
1213             self.focus_all.append(box)
1214         # elaborate by type
1215         self.focus_build = [ x for x in self.focus_all if isinstance(x,BuildBox) ]
1216         self.focus_plc = [ x for x in self.focus_all if isinstance(x,PlcBox) ]
1217         self.focus_qemu = [ x for x in self.focus_all if isinstance(x,QemuBox) ]
1218                              
1219     def list_boxes(self):
1220         print 'Sensing',
1221         for box in self.focus_all:
1222             box.sense(self.options)
1223         print 'Done'
1224         for box in self.focus_all:
1225             box.list(self.options.verbose)
1226
1227     def reboot_boxes(self):
1228         for box in self.focus_all:
1229             box.reboot(self.options)
1230
1231     def sanity_check (self):
1232         print 'Sanity check'
1233         self.sanity_check_plc()
1234         self.sanity_check_qemu()
1235
1236     def sanity_check_plc (self):
1237         pass
1238
1239     def sanity_check_qemu (self):
1240         all_nodes=[]
1241         for box in self.focus_qemu:
1242             all_nodes += box.node_names()
1243         hash={}
1244         for node in all_nodes:
1245             if node not in hash: hash[node]=0
1246             hash[node]+=1
1247         for (node,count) in hash.items():
1248             if count!=1: print 'WARNING - duplicate node',node
1249         
1250
1251     ####################
1252     # can be run as a utility to probe/display/manage the local infrastructure
1253     def main (self):
1254         parser=OptionParser()
1255         parser.add_option ('-r',"--reboot",action='store_true',dest='reboot',default=False,
1256                            help='reboot mode (use shutdown -r)')
1257         parser.add_option ('-s',"--soft",action='store_true',dest='soft',default=False,
1258                            help='soft mode for reboot (terminates processes)')
1259         parser.add_option ('-t',"--testbox",action='store_true',dest='testbox',default=False,
1260                            help='add test box') 
1261         parser.add_option ('-b',"--build",action='store_true',dest='builds',default=False,
1262                            help='add build boxes')
1263         parser.add_option ('-p',"--plc",action='store_true',dest='plcs',default=False,
1264                            help='add plc boxes')
1265         parser.add_option ('-q',"--qemu",action='store_true',dest='qemus',default=False,
1266                            help='add qemu boxes') 
1267         parser.add_option ('-a',"--all",action='store_true',dest='all',default=False,
1268                            help='address all known  boxes, like -b -t -p -q')
1269         parser.add_option ('-v',"--verbose",action='store_true',dest='verbose',default=False,
1270                            help='verbose mode')
1271         parser.add_option ('-n',"--dry_run",action='store_true',dest='dry_run',default=False,
1272                            help='dry run mode')
1273         (self.options,args)=parser.parse_args()
1274
1275         boxes=args
1276         if self.options.testbox: boxes += [self.test_box]
1277         if self.options.builds: boxes += self.build_boxes
1278         if self.options.plcs: boxes += self.plc_boxes
1279         if self.options.qemus: boxes += self.qemu_boxes
1280         if self.options.all: boxes += self.all_boxes
1281         
1282         global verbose
1283         verbose=self.options.verbose
1284         # default scope is -b -p -q -t
1285         if not boxes:
1286             boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box]
1287
1288         self.normalize (boxes)
1289
1290         if self.options.reboot:
1291             self.reboot_boxes ()
1292         else:
1293             self.list_boxes ()
1294             self.sanity_check ()