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