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