substrate sensing and /root vs /vservers on kvm boxes
[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         ########## find out running pids
719         pids=self.backquote_ssh(['pgrep','qemu'])
720         if not pids: return
721         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
722         ps_lines = self.backquote_ssh (command).split("\n")
723         for line in ps_lines:
724             if not line.strip() or line.find('PID') >=0 : continue
725             m=QemuBox.matcher.match(line)
726             if m: 
727                 self.add_node (m.group('nodename'),m.group('pid'))
728                 continue
729             header('QemuBox.sense: command %r returned line that failed to match'%command)
730             header(">>%s<<"%line)
731         ########## retrieve alive instances and map to build
732         live_builds=[]
733         command=['grep','.','*/*/qemu.pid','/dev/null']
734         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
735         for pid_line in pid_lines:
736             if not pid_line.strip(): continue
737             # expect <build>/<nodename>/qemu.pid:<pid>pid
738             try:
739                 (buildname,nodename,tail)=pid_line.split('/')
740                 (_,pid)=tail.split(':')
741                 q=self.qemu_instance_by_pid (pid)
742                 if not q: continue
743                 q.set_buildname(buildname)
744                 live_builds.append(buildname)
745             except: print 'WARNING, could not parse pid line',pid_line
746         # retrieve timestamps
747         if not live_builds: return
748         command=   ['grep','.']
749         command += ['%s/*/timestamp'%b for b in live_builds]
750         command += ['/dev/null']
751         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
752         for ts_line in ts_lines:
753             if not ts_line.strip(): continue
754             # expect <build>/<nodename>/timestamp:<timestamp>
755             try:
756                 (buildname,nodename,tail)=ts_line.split('/')
757                 nodename=nodename.replace('qemu-','')
758                 (_,timestamp)=tail.split(':')
759                 timestamp=int(timestamp)
760                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
761                 if not q: 
762                     print 'WARNING zombie qemu',self.hostname,ts_line
763                     print '... was expecting (',short_hostname(nodename),buildname,') in',\
764                         [ (short_hostname(i.nodename),i.buildname) for i in self.qemu_instances ]
765                     continue
766                 q.set_timestamp(timestamp)
767             except:  print 'WARNING, could not parse ts line',ts_line
768
769 ####################
770 class TestInstance:
771     def __init__ (self, buildname, pid=0):
772         self.pids=[]
773         if pid!=0: self.pid.append(pid)
774         self.buildname=buildname
775         # latest trace line
776         self.trace=''
777         # has a KO test
778         self.broken_steps=[]
779         self.timestamp = 0
780
781     def set_timestamp (self,timestamp): self.timestamp=timestamp
782     def set_now (self): self.timestamp=int(time.time())
783     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
784
785     def is_running (self): return len(self.pids) != 0
786
787     def add_pid (self,pid):
788         self.pids.append(pid)
789     def set_broken (self, plcindex, step): 
790         self.broken_steps.append ( (plcindex, step,) )
791
792     def line (self):
793         double='=='
794         if self.pids: double='*'+double[1]
795         if self.broken_steps: double=double[0]+'B'
796         msg = " %s %s =="%(double,self.buildname)
797         if not self.pids:       pass
798         elif len(self.pids)==1: msg += " (pid=%s)"%self.pids[0]
799         else:                   msg += " !!!pids=%s!!!"%self.pids
800         msg += " @%s"%self.pretty_timestamp()
801         if self.broken_steps:
802             msg += " [BROKEN=" + " ".join( [ "%s@%s"%(s,i) for (i,s) in self.broken_steps ] ) + "]"
803         return msg
804
805 class TestBox (Box):
806     def __init__ (self,hostname):
807         Box.__init__(self,hostname)
808         self.starting_ips=[]
809         self.test_instances=[]
810
811     def reboot (self, options):
812         # can't reboot a vserver VM
813         self.run_ssh (['pkill','run_log'],"Terminating current runs",
814                       dry_run=options.dry_run)
815         self.run_ssh (['rm','-f',Starting.location],"Cleaning %s"%Starting.location,
816                       dry_run=options.dry_run)
817
818     def get_test (self, buildname):
819         for i in self.test_instances:
820             if i.buildname==buildname: return i
821
822     # we scan ALL remaining test results, even the ones not running
823     def add_timestamp (self, buildname, timestamp):
824         i=self.get_test(buildname)
825         if i:   
826             i.set_timestamp(timestamp)
827         else:   
828             i=TestInstance(buildname,0)
829             i.set_timestamp(timestamp)
830             self.test_instances.append(i)
831
832     def add_running_test (self, pid, buildname):
833         i=self.get_test(buildname)
834         if not i:
835             self.test_instances.append (TestInstance (buildname,pid))
836             return
837         if i.pids:
838             print "WARNING: 2 concurrent tests run on same build %s"%buildname
839         i.add_pid (pid)
840
841     def add_broken (self, buildname, plcindex, step):
842         i=self.get_test(buildname)
843         if not i:
844             i=TestInstance(buildname)
845             self.test_instances.append(i)
846         i.set_broken(plcindex, step)
847
848     matcher_proc=re.compile (".*/proc/(?P<pid>[0-9]+)/cwd.*/vservers/(?P<buildname>[^/]+)$")
849     matcher_grep=re.compile ("/vservers/(?P<buildname>[^/]+)/logs/trace.*:TRACE:\s*(?P<plcindex>[0-9]+).*step=(?P<step>\S+).*")
850     def sense (self, options):
851         print 'tm',
852         self.sense_uptime()
853         self.starting_ips=[x for x in self.backquote_ssh(['cat',Starting.location], trash_err=True).strip().split('\n') if x]
854
855         # scan timestamps on all tests
856         # this is likely to not invoke ssh so we need to be a bit smarter to get * expanded
857         # xxx would make sense above too
858         command=['bash','-c',"grep . /vservers/*/timestamp /dev/null"]
859         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
860         for ts_line in ts_lines:
861             if not ts_line.strip(): continue
862             # expect /vservers/<buildname>/timestamp:<timestamp>
863             try:
864                 (ts_file,timestamp)=ts_line.split(':')
865                 ts_file=os.path.dirname(ts_file)
866                 buildname=os.path.basename(ts_file)
867                 timestamp=int(timestamp)
868                 t=self.add_timestamp(buildname,timestamp)
869             except:  print 'WARNING, could not parse ts line',ts_line
870
871         command=['bash','-c',"grep KO /vservers/*/logs/trace-* /dev/null" ]
872         trace_lines=self.backquote_ssh (command).split('\n')
873         for line in trace_lines:
874             if not line.strip(): continue
875             m=TestBox.matcher_grep.match(line)
876             if m: 
877                 buildname=m.group('buildname')
878                 plcindex=m.group('plcindex')
879                 step=m.group('step')
880                 self.add_broken(buildname,plcindex, step)
881                 continue
882             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
883             header(">>%s<<"%line)
884
885         pids = self.backquote_ssh (['pgrep','run_log'],trash_err=True)
886         if not pids: return
887         command=['ls','-ld'] + ["/proc/%s/cwd"%pid for pid in pids.split("\n") if pid]
888         ps_lines=self.backquote_ssh (command).split('\n')
889         for line in ps_lines:
890             if not line.strip(): continue
891             m=TestBox.matcher_proc.match(line)
892             if m: 
893                 pid=m.group('pid')
894                 buildname=m.group('buildname')
895                 self.add_running_test(pid, buildname)
896                 continue
897             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
898             header(">>%s<<"%line)
899         
900         
901     def line (self):
902         return "%s (%s)"%(self.hostname,self.uptime())
903
904     def list (self, verbose=False):
905         # verbose shows all tests
906         if verbose:
907             instances = self.test_instances
908             msg="knwown tests"
909         else:
910             instances = [ i for i in self.test_instances if i.is_running() ]
911             msg="known running tests"
912
913         if not instances:
914             header ("No %s on %s"%(msg,self.line()))
915         else:
916             header ("%s on %s"%(msg,self.line()))
917             instances.sort(timestamp_sort)
918             for i in instances: print i.line()
919         # show 'starting' regardless of verbose
920         if self.starting_ips:
921             header ("Starting IP addresses on %s"%self.line())
922             self.starting_ips.sort()
923             for starting in self.starting_ips: print starting
924         else:
925             header ("Empty 'starting' on %s"%self.line())
926
927 ############################################################
928 class Options: pass
929
930 class Substrate:
931
932     def __init__ (self, plcs_on_vs=True, plcs_on_lxc=False):
933         self.options=Options()
934         self.options.dry_run=False
935         self.options.verbose=False
936         self.options.reboot=False
937         self.options.soft=False
938         self.test_box = TestBox (self.test_box_spec())
939         self.build_boxes = [ BuildBox(h) for h in self.build_boxes_spec() ]
940         # for compat with older LocalSubstrate
941         try:
942             self.plc_vs_boxes = [ PlcVsBox (h,m) for (h,m) in self.plc_vs_boxes_spec ()]
943             self.plc_lxc_boxes = [ PlcLxcBox (h,m) for (h,m) in self.plc_lxc_boxes_spec ()]
944         except:
945             self.plc_vs_boxes = [ PlcVsBox (h,m) for (h,m) in self.plc_boxes_spec ()]
946             self.plc_lxc_boxes = [ ]
947         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
948         self._sensed=False
949
950         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs",self)
951         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes",self)
952         
953         self.rescope (plcs_on_vs=plcs_on_vs, plcs_on_lxc=plcs_on_lxc)
954
955     # which plc boxes are we interested in ?
956     def rescope (self, plcs_on_vs, plcs_on_lxc):
957         self.plc_boxes=[]
958         if plcs_on_vs: self.plc_boxes += self.plc_vs_boxes
959         if plcs_on_lxc: self.plc_boxes += self.plc_lxc_boxes
960         self.default_boxes = self.plc_boxes + self.qemu_boxes
961         self.all_boxes = self.build_boxes + [ self.test_box ] + self.plc_boxes + self.qemu_boxes
962
963     def summary_line (self):
964         msg  = "["
965         msg += " %d vp"%len(self.plc_vs_boxes)
966         msg += " %d xp"%len(self.plc_lxc_boxes)
967         msg += " %d tried plc boxes"%len(self.plc_boxes)
968         msg += "]"
969         return msg
970
971     def fqdn (self, hostname):
972         if hostname.find('.')<0: return "%s.%s"%(hostname,self.domain())
973         return hostname
974
975     # return True if actual sensing takes place
976     def sense (self,force=False):
977         if self._sensed and not force: return False
978         print 'Sensing local substrate...',
979         for b in self.default_boxes: b.sense(self.options)
980         print 'Done'
981         self._sensed=True
982         return True
983
984     def list (self, verbose=False):
985         for b in self.default_boxes:
986             b.list()
987
988     def add_dummy_plc (self, plc_boxname, plcname):
989         for pb in self.plc_boxes:
990             if pb.hostname==plc_boxname:
991                 pb.add_dummy(plcname)
992                 return True
993     def add_dummy_qemu (self, qemu_boxname, qemuname):
994         for qb in self.qemu_boxes:
995             if qb.hostname==qemu_boxname:
996                 qb.add_dummy(qemuname)
997                 return True
998
999     def add_starting_dummy (self, bname, vname):
1000         return self.add_dummy_plc (bname, vname) or self.add_dummy_qemu (bname, vname)
1001
1002     ########## 
1003     def provision (self,plcs,options):
1004         try:
1005             # attach each plc to a plc box and an IP address
1006             plcs = [ self.provision_plc (plc,options) for plc in plcs ]
1007             # attach each node/qemu to a qemu box with an IP address
1008             plcs = [ self.provision_qemus (plc,options) for plc in plcs ]
1009             # update the SFA spec accordingly
1010             plcs = [ self.localize_sfa_rspec(plc,options) for plc in plcs ]
1011             self.list()
1012             return plcs
1013         except Exception, e:
1014             print '* Could not provision this test on current substrate','--',e,'--','exiting'
1015             traceback.print_exc()
1016             sys.exit(1)
1017
1018     # it is expected that a couple of options like ips_bplc and ips_vplc 
1019     # are set or unset together
1020     @staticmethod
1021     def check_options (x,y):
1022         if not x and not y: return True
1023         return len(x)==len(y)
1024
1025     # find an available plc box (or make space)
1026     # and a free IP address (using options if present)
1027     def provision_plc (self, plc, options):
1028         
1029         assert Substrate.check_options (options.ips_bplc, options.ips_vplc)
1030
1031         #### let's find an IP address for that plc
1032         # look in options 
1033         if options.ips_vplc:
1034             # this is a rerun
1035             # we don't check anything here, 
1036             # it is the caller's responsability to cleanup and make sure this makes sense
1037             plc_boxname = options.ips_bplc.pop()
1038             vplc_hostname=options.ips_vplc.pop()
1039         else:
1040             if self.sense(): self.list()
1041             plc_boxname=None
1042             vplc_hostname=None
1043             # try to find an available IP 
1044             self.vplc_pool.sense()
1045             couple=self.vplc_pool.next_free()
1046             if couple:
1047                 (vplc_hostname,unused)=couple
1048             #### we need to find one plc box that still has a slot
1049             max_free=0
1050             # use the box that has max free spots for load balancing
1051             for pb in self.plc_boxes:
1052                 free=pb.free_slots()
1053                 if free>max_free:
1054                     plc_boxname=pb.hostname
1055                     max_free=free
1056             # if there's no available slot in the plc_boxes, or we need a free IP address
1057             # make space by killing the oldest running instance
1058             if not plc_boxname or not vplc_hostname:
1059                 # find the oldest of all our instances
1060                 all_plc_instances=reduce(lambda x, y: x+y, 
1061                                          [ pb.plc_instances for pb in self.plc_boxes ],
1062                                          [])
1063                 all_plc_instances.sort(timestamp_sort)
1064                 try:
1065                     plc_instance_to_kill=all_plc_instances[0]
1066                 except:
1067                     msg=""
1068                     if not plc_boxname: msg += " PLC boxes are full"
1069                     if not vplc_hostname: msg += " vplc IP pool exhausted"
1070                     msg += " %s"%self.summary_line()
1071                     raise Exception,"Cannot make space for a PLC instance:"+msg
1072                 freed_plc_boxname=plc_instance_to_kill.plc_box.hostname
1073                 freed_vplc_hostname=plc_instance_to_kill.vplcname()
1074                 message='killing oldest plc instance = %s on %s'%(plc_instance_to_kill.line(),
1075                                                                   freed_plc_boxname)
1076                 plc_instance_to_kill.kill()
1077                 # use this new plcbox if that was the problem
1078                 if not plc_boxname:
1079                     plc_boxname=freed_plc_boxname
1080                 # ditto for the IP address
1081                 if not vplc_hostname:
1082                     vplc_hostname=freed_vplc_hostname
1083                     # record in pool as mine
1084                     self.vplc_pool.set_mine(vplc_hostname)
1085
1086         # 
1087         self.add_dummy_plc(plc_boxname,plc['name'])
1088         vplc_ip = self.vplc_pool.get_ip(vplc_hostname)
1089         self.vplc_pool.add_starting(vplc_hostname, plc_boxname)
1090
1091         #### compute a helpful vserver name
1092         # remove domain in hostname
1093         vplc_short = short_hostname(vplc_hostname)
1094         vservername = "%s-%d-%s" % (options.buildname,plc['index'],vplc_short)
1095         plc_name = "%s_%s"%(plc['name'],vplc_short)
1096
1097         utils.header( 'PROVISION plc %s in box %s at IP %s as %s'%\
1098                           (plc['name'],plc_boxname,vplc_hostname,vservername))
1099
1100         #### apply in the plc_spec
1101         # # informative
1102         # label=options.personality.replace("linux","")
1103         mapper = {'plc': [ ('*' , {'host_box':plc_boxname,
1104                                    # 'name':'%s-'+label,
1105                                    'name': plc_name,
1106                                    'vservername':vservername,
1107                                    'vserverip':vplc_ip,
1108                                    'PLC_DB_HOST':vplc_hostname,
1109                                    'PLC_API_HOST':vplc_hostname,
1110                                    'PLC_BOOT_HOST':vplc_hostname,
1111                                    'PLC_WWW_HOST':vplc_hostname,
1112                                    'PLC_NET_DNS1' : self.network_settings() [ 'interface_fields:dns1' ],
1113                                    'PLC_NET_DNS2' : self.network_settings() [ 'interface_fields:dns2' ],
1114                                    } ) ]
1115                   }
1116
1117
1118         # mappers only work on a list of plcs
1119         return TestMapper([plc],options).map(mapper)[0]
1120
1121     ##########
1122     def provision_qemus (self, plc, options):
1123
1124         assert Substrate.check_options (options.ips_bnode, options.ips_vnode)
1125
1126         test_mapper = TestMapper ([plc], options)
1127         nodenames = test_mapper.node_names()
1128         maps=[]
1129         for nodename in nodenames:
1130
1131             if options.ips_vnode:
1132                 # as above, it's a rerun, take it for granted
1133                 qemu_boxname=options.ips_bnode.pop()
1134                 vnode_hostname=options.ips_vnode.pop()
1135             else:
1136                 if self.sense(): self.list()
1137                 qemu_boxname=None
1138                 vnode_hostname=None
1139                 # try to find an available IP 
1140                 self.vnode_pool.sense()
1141                 couple=self.vnode_pool.next_free()
1142                 if couple:
1143                     (vnode_hostname,unused)=couple
1144                 # find a physical box
1145                 max_free=0
1146                 # use the box that has max free spots for load balancing
1147                 for qb in self.qemu_boxes:
1148                     free=qb.free_slots()
1149                     if free>max_free:
1150                         qemu_boxname=qb.hostname
1151                         max_free=free
1152                 # if we miss the box or the IP, kill the oldest instance
1153                 if not qemu_boxname or not vnode_hostname:
1154                 # find the oldest of all our instances
1155                     all_qemu_instances=reduce(lambda x, y: x+y, 
1156                                               [ qb.qemu_instances for qb in self.qemu_boxes ],
1157                                               [])
1158                     all_qemu_instances.sort(timestamp_sort)
1159                     try:
1160                         qemu_instance_to_kill=all_qemu_instances[0]
1161                     except:
1162                         msg=""
1163                         if not qemu_boxname: msg += " QEMU boxes are full"
1164                         if not vnode_hostname: msg += " vnode IP pool exhausted" 
1165                         msg += " %s"%self.summary_line()
1166                         raise Exception,"Cannot make space for a QEMU instance:"+msg
1167                     freed_qemu_boxname=qemu_instance_to_kill.qemu_box.hostname
1168                     freed_vnode_hostname=short_hostname(qemu_instance_to_kill.nodename)
1169                     # kill it
1170                     message='killing oldest qemu node = %s on %s'%(qemu_instance_to_kill.line(),
1171                                                                    freed_qemu_boxname)
1172                     qemu_instance_to_kill.kill()
1173                     # use these freed resources where needed
1174                     if not qemu_boxname:
1175                         qemu_boxname=freed_qemu_boxname
1176                     if not vnode_hostname:
1177                         vnode_hostname=freed_vnode_hostname
1178                         self.vnode_pool.set_mine(vnode_hostname)
1179
1180             self.add_dummy_qemu (qemu_boxname,vnode_hostname)
1181             mac=self.vnode_pool.retrieve_userdata(vnode_hostname)
1182             ip=self.vnode_pool.get_ip (vnode_hostname)
1183             self.vnode_pool.add_starting(vnode_hostname,qemu_boxname)
1184
1185             vnode_fqdn = self.fqdn(vnode_hostname)
1186             nodemap={'host_box':qemu_boxname,
1187                      'node_fields:hostname':vnode_fqdn,
1188                      'interface_fields:ip':ip, 
1189                      'ipaddress_fields:ip_addr':ip, 
1190                      'interface_fields:mac':mac,
1191                      }
1192             nodemap.update(self.network_settings())
1193             maps.append ( (nodename, nodemap) )
1194
1195             utils.header("PROVISION node %s in box %s at IP %s with MAC %s"%\
1196                              (nodename,qemu_boxname,vnode_hostname,mac))
1197
1198         return test_mapper.map({'node':maps})[0]
1199
1200     def localize_sfa_rspec (self,plc,options):
1201        
1202         plc['sfa']['SFA_REGISTRY_HOST'] = plc['PLC_DB_HOST']
1203         plc['sfa']['SFA_AGGREGATE_HOST'] = plc['PLC_DB_HOST']
1204         plc['sfa']['SFA_SM_HOST'] = plc['PLC_DB_HOST']
1205         plc['sfa']['SFA_DB_HOST'] = plc['PLC_DB_HOST']
1206         plc['sfa']['SFA_PLC_URL'] = 'https://' + plc['PLC_API_HOST'] + ':443/PLCAPI/' 
1207         return plc
1208
1209     #################### release:
1210     def release (self,options):
1211         self.vplc_pool.release_my_starting()
1212         self.vnode_pool.release_my_starting()
1213         pass
1214
1215     #################### show results for interactive mode
1216     def get_box (self,boxname):
1217         for b in self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box] :
1218             if b.shortname()==boxname:
1219                 return b
1220         print "Could not find box %s"%boxname
1221         return None
1222
1223     def list_boxes(self,box_or_names):
1224         print 'Sensing',
1225         for box in box_or_names:
1226             if not isinstance(box,Box): box=self.get_box(box)
1227             if not box: continue
1228             box.sense(self.options)
1229         print 'Done'
1230         for box in box_or_names:
1231             if not isinstance(box,Box): box=self.get_box(box)
1232             if not box: continue
1233             box.list(self.options.verbose)
1234
1235     def reboot_boxes(self,box_or_names):
1236         for box in box_or_names:
1237             if not isinstance(box,Box): box=self.get_box(box)
1238             if not box: continue
1239             box.reboot(self.options)
1240
1241     ####################
1242     # can be run as a utility to probe/display/manage the local infrastructure
1243     def main (self):
1244         parser=OptionParser()
1245         parser.add_option ('-r',"--reboot",action='store_true',dest='reboot',default=False,
1246                            help='reboot mode (use shutdown -r)')
1247         parser.add_option ('-s',"--soft",action='store_true',dest='soft',default=False,
1248                            help='soft mode for reboot (vserver stop or kill qemus)')
1249         parser.add_option ('-t',"--testbox",action='store_true',dest='testbox',default=False,
1250                            help='add test box') 
1251         parser.add_option ('-b',"--build",action='store_true',dest='builds',default=False,
1252                            help='add build boxes')
1253         parser.add_option ('-p',"--plc",action='store_true',dest='plcs',default=False,
1254                            help='add plc boxes')
1255         parser.add_option ('-q',"--qemu",action='store_true',dest='qemus',default=False,
1256                            help='add qemu boxes') 
1257         parser.add_option ('-a',"--all",action='store_true',dest='all',default=False,
1258                            help='address all known  boxes, like -b -t -p -q')
1259         parser.add_option ('-v',"--verbose",action='store_true',dest='verbose',default=False,
1260                            help='verbose mode')
1261         parser.add_option ('-n',"--dry_run",action='store_true',dest='dry_run',default=False,
1262                            help='dry run mode')
1263         (self.options,args)=parser.parse_args()
1264
1265         self.rescope (plcs_on_vs=True, plcs_on_lxc=True)
1266
1267         boxes=args
1268         if self.options.testbox: boxes += [self.test_box]
1269         if self.options.builds: boxes += self.build_boxes
1270         if self.options.plcs: boxes += self.plc_boxes
1271         if self.options.qemus: boxes += self.qemu_boxes
1272         if self.options.all: boxes += self.all_boxes
1273         
1274         # default scope is -b -p -q -t
1275         if not boxes:
1276             boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box]
1277
1278         if self.options.reboot: self.reboot_boxes (boxes)
1279         else:                   self.list_boxes (boxes)