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