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