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