be robust against running lxc containers without a timestamp
[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         self._hostname = self._uptime = self._uname = self._fedora = "** Unknown **"
290         if not composite: 
291             print "root@%s unreachable"%self.hostname
292             self._probed=''
293         else:
294             try:
295                 pieces = composite.split(Box.separator)
296                 pieces = [ x.strip() for x in pieces ]
297                 [self._hostname, self._uptime, self._uname, self._fedora] = pieces
298                 # customize
299                 self._uptime = ', '.join([ x.strip() for x in self._uptime.split(',')[2:]])
300                 self._fedora = self._fedora.replace("Fedora release ","f").split(" ")[0]
301             except:
302                 import traceback
303                 print 'BEG issue with pieces',pieces
304                 traceback.print_exc()
305                 print 'END issue with pieces',pieces
306             self._probed=self._hostname
307         return self._probed
308
309     # use argv=['bash','-c',"the command line"]
310     def uptime(self):
311         self.probe()
312         if hasattr(self,'_uptime') and self._uptime: return self._uptime
313         return '*unprobed* uptime'
314     def uname(self):
315         self.probe()
316         if hasattr(self,'_uname') and self._uname: return self._uname
317         return '*unprobed* uname'
318     def fedora(self):
319         self.probe()
320         if hasattr(self,'_fedora') and self._fedora: return self._fedora
321         return '*unprobed* fedora'
322
323     def run(self,argv,message=None,trash_err=False,dry_run=False):
324         if dry_run:
325             print 'DRY_RUN:',
326             print " ".join(argv)
327             return 0
328         else:
329             header(message)
330             if not trash_err:
331                 return subprocess.call(argv)
332             else:
333                 return subprocess.call(argv,stderr=file('/dev/null','w'))
334                 
335     def run_ssh (self, argv, message, trash_err=False, dry_run=False):
336         ssh_argv = self.test_ssh().actual_argv(argv)
337         result=self.run (ssh_argv, message, trash_err, dry_run=dry_run)
338         if result!=0:
339             print "WARNING: failed to run %s on %s"%(" ".join(argv),self.hostname)
340         return result
341
342     def backquote (self, argv, trash_err=False):
343         # print 'running backquote',argv
344         if not trash_err:
345             result= subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
346         else:
347             result= subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
348         return result
349
350     # if you have any shell-expanded arguments like *
351     # and if there's any chance the command is adressed to the local host
352     def backquote_ssh (self, argv, trash_err=False):
353         if not self.probe(): return ''
354         return self.backquote( self.test_ssh().actual_argv(argv), trash_err)
355
356 ############################################################
357 class BuildInstance:
358     def __init__ (self, buildname, pid, buildbox):
359         self.buildname=buildname
360         self.buildbox=buildbox
361         self.pids=[pid]
362
363     def add_pid(self,pid):
364         self.pids.append(pid)
365
366     def line (self):
367         return "== %s == (pids=%r)"%(self.buildname,self.pids)
368
369 class BuildBox (Box):
370     def __init__ (self,hostname):
371         Box.__init__(self,hostname)
372         self.build_instances=[]
373
374     def add_build (self,buildname,pid):
375         for build in self.build_instances:
376             if build.buildname==buildname: 
377                 build.add_pid(pid)
378                 return
379         self.build_instances.append(BuildInstance(buildname, pid, self))
380
381     def list(self, verbose=False):
382         if not self.build_instances: 
383             header ('No build process on %s (%s)'%(self.hostname_fedora(),self.uptime()))
384         else:
385             header ("Builds on %s (%s)"%(self.hostname_fedora(),self.uptime()))
386             for b in self.build_instances: 
387                 header (b.line(),banner=False)
388
389     def reboot (self, options):
390         if not options.soft:
391             Box.reboot(self,options)
392         else:
393             command=['pkill','vbuild']
394             self.run_ssh(command,"Terminating vbuild processes",dry_run=options.dry_run)
395
396     # inspect box and find currently running builds
397     matcher=re.compile("\s*(?P<pid>[0-9]+).*-[bo]\s+(?P<buildname>[^\s]+)(\s|\Z)")
398     matcher_building_vm=re.compile("\s*(?P<pid>[0-9]+).*init-vserver.*\s+(?P<buildname>[^\s]+)\s*\Z")
399     def sense(self, options):
400         print 'bb',
401         pids=self.backquote_ssh(['pgrep','vbuild'],trash_err=True)
402         if not pids: return
403         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
404         ps_lines=self.backquote_ssh (command).split('\n')
405         for line in ps_lines:
406             if not line.strip() or line.find('PID')>=0: continue
407             m=BuildBox.matcher.match(line)
408             if m: 
409                 date=time.strftime('%Y-%m-%d',time.localtime(time.time()))
410                 buildname=m.group('buildname').replace('@DATE@',date)
411                 self.add_build (buildname,m.group('pid'))
412                 continue
413             m=BuildBox.matcher_building_vm.match(line)
414             if m: 
415                 # buildname is expansed here
416                 self.add_build (buildname,m.group('pid'))
417                 continue
418             header('BuildBox.sense: command %r returned line that failed to match'%command)
419             header(">>%s<<"%line)
420
421 ############################################################
422 class PlcInstance:
423     def __init__ (self, plcbox):
424         self.plc_box=plcbox
425         # unknown yet
426         self.timestamp=0
427         
428     def set_timestamp (self,timestamp): self.timestamp=timestamp
429     def set_now (self): self.timestamp=int(time.time())
430     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
431
432 class PlcVsInstance (PlcInstance):
433     def __init__ (self, plcbox, vservername, ctxid):
434         PlcInstance.__init__(self,plcbox)
435         self.vservername=vservername
436         self.ctxid=ctxid
437
438     def vplcname (self):
439         return self.vservername.split('-')[-1]
440     def buildname (self):
441         return self.vservername.rsplit('-',2)[0]
442
443     def line (self):
444         msg="== %s =="%(self.vplcname())
445         msg += " [=%s]"%self.vservername
446         if self.ctxid==0:  msg+=" not (yet?) running"
447         else:              msg+=" (ctx=%s)"%self.ctxid     
448         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
449         else:              msg += " *unknown timestamp*"
450         return msg
451
452     def kill (self):
453         msg="vserver stopping %s on %s"%(self.vservername,self.plc_box.hostname)
454         self.plc_box.run_ssh(['vserver',self.vservername,'stop'],msg)
455         self.plc_box.forget(self)
456
457 class PlcLxcInstance (PlcInstance):
458     # does lxc have a context id of any kind ?
459     def __init__ (self, plcbox, lxcname, pid):
460         PlcInstance.__init__(self, plcbox)
461         self.lxcname = lxcname
462         self.pid = pid
463
464     def vplcname (self):
465         return self.lxcname.split('-')[-1]
466     def buildname (self):
467         return self.lxcname.rsplit('-',2)[0]
468
469     def line (self):
470         msg="== %s =="%(self.vplcname())
471         msg += " [=%s]"%self.lxcname
472         if self.pid==-1:  msg+=" not (yet?) running"
473         else:              msg+=" (pid=%s)"%self.pid
474         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
475         else:              msg += " *unknown timestamp*"
476         return msg
477
478     def kill (self):
479         command="rsync lxc-driver.sh  %s:/root"%self.plc_box.hostname
480         commands.getstatusoutput(command)
481         msg="lxc container stopping %s on %s"%(self.lxcname,self.plc_box.hostname)
482         self.plc_box.run_ssh(['/root/lxc-driver.sh','-c','stop_lxc','-n',self.lxcname],msg)
483         self.plc_box.forget(self)
484
485 ##########
486 class PlcBox (Box):
487     def __init__ (self, hostname, max_plcs):
488         Box.__init__(self,hostname)
489         self.plc_instances=[]
490         self.max_plcs=max_plcs
491
492     def free_slots (self):
493         return self.max_plcs - len(self.plc_instances)
494
495     # fill one slot even though this one is not started yet
496     def add_dummy (self, plcname):
497         dummy=PlcVsInstance(self,'dummy_'+plcname,0)
498         dummy.set_now()
499         self.plc_instances.append(dummy)
500
501     def forget (self, plc_instance):
502         self.plc_instances.remove(plc_instance)
503
504     def reboot (self, options):
505         if not options.soft:
506             Box.reboot(self,options)
507         else:
508             self.soft_reboot (options)
509
510     def list(self, verbose=False):
511         if not self.plc_instances: 
512             header ('No plc running on %s'%(self.line()))
513         else:
514             header ("Active plc VMs on %s"%self.line())
515             self.plc_instances.sort(timestamp_sort)
516             for p in self.plc_instances: 
517                 header (p.line(),banner=False)
518
519
520 class PlcVsBox (PlcBox):
521
522     def add_vserver (self,vservername,ctxid):
523         for plc in self.plc_instances:
524             if plc.vservername==vservername: 
525                 header("WARNING, duplicate myplc %s running on %s"%\
526                            (vservername,self.hostname),banner=False)
527                 return
528         self.plc_instances.append(PlcVsInstance(self,vservername,ctxid))
529     
530     def line(self): 
531         msg="%s [max=%d,free=%d, VS-based] (%s)"%(self.hostname_fedora(), self.max_plcs,self.free_slots(),self.uname())
532         return msg
533         
534     def plc_instance_by_vservername (self, vservername):
535         for p in self.plc_instances:
536             if p.vservername==vservername: return p
537         return None
538
539     def soft_reboot (self, options):
540         self.run_ssh(['service','util-vserver','stop'],"Stopping all running vservers on %s"%(self.hostname,),
541                      dry_run=options.dry_run)
542
543     def sense (self, options):
544         print 'vp',
545         # try to find fullname (vserver_stat truncates to a ridiculously short name)
546         # fetch the contexts for all vservers on that box
547         map_command=['grep','.','/etc/vservers/*/context','/dev/null',]
548         context_map=self.backquote_ssh (map_command)
549         # at this point we have a set of lines like
550         # /etc/vservers/2010.01.20--k27-f12-32-vplc03/context:40144
551         ctx_dict={}
552         for map_line in context_map.split("\n"):
553             if not map_line: continue
554             [path,xid] = map_line.split(':')
555             ctx_dict[xid]=os.path.basename(os.path.dirname(path))
556         # at this point ctx_id maps context id to vservername
557
558         command=['vserver-stat']
559         vserver_stat = self.backquote_ssh (command)
560         for vserver_line in vserver_stat.split("\n"):
561             if not vserver_line: continue
562             context=vserver_line.split()[0]
563             if context=="CTX": continue
564             try:
565                 longname=ctx_dict[context]
566                 self.add_vserver(longname,context)
567             except:
568                 print 'WARNING: found ctx %s in vserver_stat but was unable to figure a corresp. vserver'%context
569
570         # scan timestamps 
571         running_vsnames = [ i.vservername for i in self.plc_instances ]
572         command=   ['grep','.']
573         command += ['/vservers/%s.timestamp'%vs for vs in running_vsnames]
574         command += ['/dev/null']
575         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
576         for ts_line in ts_lines:
577             if not ts_line.strip(): continue
578             # expect /vservers/<vservername>.timestamp:<timestamp>
579             try:
580                 (ts_file,timestamp)=ts_line.split(':')
581                 ts_file=os.path.basename(ts_file)
582                 (vservername,_)=os.path.splitext(ts_file)
583                 timestamp=int(timestamp)
584                 p=self.plc_instance_by_vservername(vservername)
585                 if not p: 
586                     print 'WARNING zombie plc',self.hostname,ts_line
587                     print '... was expecting',vservername,'in',[i.vservername for i in self.plc_instances]
588                     continue
589                 p.set_timestamp(timestamp)
590             except:  print 'WARNING, could not parse ts line',ts_line
591         
592
593 class PlcLxcBox (PlcBox):
594
595     def add_lxc (self,lxcname,pid):
596         for plc in self.plc_instances:
597             if plc.lxcname==lxcname:
598                 header("WARNING, duplicate myplc %s running on %s"%\
599                            (lxcname,self.hostname),banner=False)
600                 return
601         self.plc_instances.append(PlcLxcInstance(self,lxcname,pid))    
602
603
604     # a line describing the box
605     def line(self): 
606         return "%s [max=%d,free=%d, LXC-based] (%s)"%(self.hostname_fedora(), self.max_plcs,self.free_slots(),
607                                                       self.uname())
608     
609     def plc_instance_by_lxcname (self, lxcname):
610         for p in self.plc_instances:
611             if p.lxcname==lxcname: return p
612         return None
613     
614     # essentially shutdown all running containers
615     def soft_reboot (self, options):
616         command="rsync lxc-driver.sh  %s:/root"%self.hostname
617         commands.getstatusoutput(command)
618         self.run_ssh(['/root/lxc-driver.sh','-c','stop_all'],"Stopping all running lxc containers on %s"%(self.hostname,),
619                      dry_run=options.dry_run)
620
621
622     # sense is expected to fill self.plc_instances with PlcLxcInstance's 
623     # to describe the currently running VM's
624     # as well as to call  self.get_uname() once
625     def sense (self, options):
626         print "xp",
627         command="rsync lxc-driver.sh  %s:/root"%self.hostname
628         commands.getstatusoutput(command)
629         command=['/root/lxc-driver.sh','-c','sense_all']
630         lxc_stat = self.backquote_ssh (command)
631         for lxc_line in lxc_stat.split("\n"):
632             if not lxc_line: continue
633             lxcname=lxc_line.split(";")[0]
634             pid=lxc_line.split(";")[1]
635             timestamp=lxc_line.split(";")[2]
636             self.add_lxc(lxcname,pid)
637             try: timestamp=int(timestamp)
638             except: timestamp=0
639             p=self.plc_instance_by_lxcname(lxcname)
640             if not p:
641                 print 'WARNING zombie plc',self.hostname,lxcname
642                 print '... was expecting',lxcname,'in',[i.lxcname for i in self.plc_instances]
643                 continue
644             p.set_timestamp(timestamp)
645
646 ############################################################
647 class QemuInstance: 
648     def __init__ (self, nodename, pid, qemubox):
649         self.nodename=nodename
650         self.pid=pid
651         self.qemu_box=qemubox
652         # not known yet
653         self.buildname=None
654         self.timestamp=0
655         
656     def set_buildname (self,buildname): self.buildname=buildname
657     def set_timestamp (self,timestamp): self.timestamp=timestamp
658     def set_now (self): self.timestamp=int(time.time())
659     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
660     
661     def line (self):
662         msg = "== %s =="%(short_hostname(self.nodename))
663         msg += " [=%s]"%self.buildname
664         if self.pid:       msg += " (pid=%s)"%self.pid
665         else:              msg += " not (yet?) running"
666         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
667         else:              msg += " *unknown timestamp*"
668         return msg
669     
670     def kill(self):
671         if self.pid==0: 
672             print "cannot kill qemu %s with pid==0"%self.nodename
673             return
674         msg="Killing qemu %s with pid=%s on box %s"%(self.nodename,self.pid,self.qemu_box.hostname)
675         self.qemu_box.run_ssh(['kill',"%s"%self.pid],msg)
676         self.qemu_box.forget(self)
677
678
679 class QemuBox (Box):
680     def __init__ (self, hostname, max_qemus):
681         Box.__init__(self,hostname)
682         self.qemu_instances=[]
683         self.max_qemus=max_qemus
684
685     def add_node (self,nodename,pid):
686         for qemu in self.qemu_instances:
687             if qemu.nodename==nodename: 
688                 header("WARNING, duplicate qemu %s running on %s"%\
689                            (nodename,self.hostname), banner=False)
690                 return
691         self.qemu_instances.append(QemuInstance(nodename,pid,self))
692
693     def forget (self, qemu_instance):
694         self.qemu_instances.remove(qemu_instance)
695
696     # fill one slot even though this one is not started yet
697     def add_dummy (self, nodename):
698         dummy=QemuInstance('dummy_'+nodename,0,self)
699         dummy.set_now()
700         self.qemu_instances.append(dummy)
701
702     def line (self):
703         return "%s [max=%d,free=%d] (%s) %s"%(
704             self.hostname_fedora(), self.max_qemus,self.free_slots(),
705             self.uptime(),self.driver())
706
707     def list(self, verbose=False):
708         if not self.qemu_instances: 
709             header ('No qemu on %s'%(self.line()))
710         else:
711             header ("Qemus on %s"%(self.line()))
712             self.qemu_instances.sort(timestamp_sort)
713             for q in self.qemu_instances: 
714                 header (q.line(),banner=False)
715
716     def free_slots (self):
717         return self.max_qemus - len(self.qemu_instances)
718
719     def driver(self):
720         if hasattr(self,'_driver') and self._driver: return self._driver
721         return '*undef* driver'
722
723     def qemu_instance_by_pid (self,pid):
724         for q in self.qemu_instances:
725             if q.pid==pid: return q
726         return None
727
728     def qemu_instance_by_nodename_buildname (self,nodename,buildname):
729         for q in self.qemu_instances:
730             if q.nodename==nodename and q.buildname==buildname:
731                 return q
732         return None
733
734     def reboot (self, options):
735         if not options.soft:
736             Box.reboot(self,options)
737         else:
738             self.run_ssh(['pkill','qemu'],"Killing qemu instances",
739                          dry_run=options.dry_run)
740
741     matcher=re.compile("\s*(?P<pid>[0-9]+).*-cdrom\s+(?P<nodename>[^\s]+)\.iso")
742     def sense(self, options):
743         print 'qn',
744         modules=self.backquote_ssh(['lsmod']).split('\n')
745         self._driver='*NO kqemu/kvm_intel MODULE LOADED*'
746         for module in modules:
747             if module.find('kqemu')==0:
748                 self._driver='kqemu module loaded'
749             # kvm might be loaded without kvm_intel (we dont have AMD)
750             elif module.find('kvm_intel')==0:
751                 self._driver='kvm_intel OK'
752         ########## find out running pids
753         pids=self.backquote_ssh(['pgrep','qemu'])
754         if not pids: return
755         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
756         ps_lines = self.backquote_ssh (command).split("\n")
757         for line in ps_lines:
758             if not line.strip() or line.find('PID') >=0 : continue
759             m=QemuBox.matcher.match(line)
760             if m: 
761                 self.add_node (m.group('nodename'),m.group('pid'))
762                 continue
763             header('QemuBox.sense: command %r returned line that failed to match'%command)
764             header(">>%s<<"%line)
765         ########## retrieve alive instances and map to build
766         live_builds=[]
767         command=['grep','.','/vservers/*/*/qemu.pid','/dev/null']
768         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
769         for pid_line in pid_lines:
770             if not pid_line.strip(): continue
771             # expect <build>/<nodename>/qemu.pid:<pid>pid
772             try:
773                 (_,__,buildname,nodename,tail)=pid_line.split('/')
774                 (_,pid)=tail.split(':')
775                 q=self.qemu_instance_by_pid (pid)
776                 if not q: continue
777                 q.set_buildname(buildname)
778                 live_builds.append(buildname)
779             except: print 'WARNING, could not parse pid line',pid_line
780         # retrieve timestamps
781         if not live_builds: return
782         command=   ['grep','.']
783         command += ['/vservers/%s/*/timestamp'%b for b in live_builds]
784         command += ['/dev/null']
785         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
786         for ts_line in ts_lines:
787             if not ts_line.strip(): continue
788             # expect <build>/<nodename>/timestamp:<timestamp>
789             try:
790                 (_,__,buildname,nodename,tail)=ts_line.split('/')
791                 nodename=nodename.replace('qemu-','')
792                 (_,timestamp)=tail.split(':')
793                 timestamp=int(timestamp)
794                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
795                 if not q: 
796                     print 'WARNING zombie qemu',self.hostname,ts_line
797                     print '... was expecting (',short_hostname(nodename),buildname,') in',\
798                         [ (short_hostname(i.nodename),i.buildname) for i in self.qemu_instances ]
799                     continue
800                 q.set_timestamp(timestamp)
801             except:  print 'WARNING, could not parse ts line',ts_line
802
803 ####################
804 class TestInstance:
805     def __init__ (self, buildname, pid=0):
806         self.pids=[]
807         if pid!=0: self.pid.append(pid)
808         self.buildname=buildname
809         # latest trace line
810         self.trace=''
811         # has a KO test
812         self.broken_steps=[]
813         self.timestamp = 0
814
815     def set_timestamp (self,timestamp): self.timestamp=timestamp
816     def set_now (self): self.timestamp=int(time.time())
817     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
818
819     def is_running (self): return len(self.pids) != 0
820
821     def add_pid (self,pid):
822         self.pids.append(pid)
823     def set_broken (self, plcindex, step): 
824         self.broken_steps.append ( (plcindex, step,) )
825
826     def line (self):
827         double='=='
828         if self.pids: double='*'+double[1]
829         if self.broken_steps: double=double[0]+'B'
830         msg = " %s %s =="%(double,self.buildname)
831         if not self.pids:       pass
832         elif len(self.pids)==1: msg += " (pid=%s)"%self.pids[0]
833         else:                   msg += " !!!pids=%s!!!"%self.pids
834         msg += " @%s"%self.pretty_timestamp()
835         if self.broken_steps:
836             # sometimes we have an empty plcindex
837             msg += " [BROKEN=" + " ".join( [ "%s@%s"%(s,i) if i else s for (i,s) in self.broken_steps ] ) + "]"
838         return msg
839
840 class TestBox (Box):
841     def __init__ (self,hostname):
842         Box.__init__(self,hostname)
843         self.starting_ips=[]
844         self.test_instances=[]
845
846     def reboot (self, options):
847         # can't reboot a vserver VM
848         self.run_ssh (['pkill','run_log'],"Terminating current runs",
849                       dry_run=options.dry_run)
850         self.run_ssh (['rm','-f',Starting.location],"Cleaning %s"%Starting.location,
851                       dry_run=options.dry_run)
852
853     def get_test (self, buildname):
854         for i in self.test_instances:
855             if i.buildname==buildname: return i
856
857     # we scan ALL remaining test results, even the ones not running
858     def add_timestamp (self, buildname, timestamp):
859         i=self.get_test(buildname)
860         if i:   
861             i.set_timestamp(timestamp)
862         else:   
863             i=TestInstance(buildname,0)
864             i.set_timestamp(timestamp)
865             self.test_instances.append(i)
866
867     def add_running_test (self, pid, buildname):
868         i=self.get_test(buildname)
869         if not i:
870             self.test_instances.append (TestInstance (buildname,pid))
871             return
872         if i.pids:
873             print "WARNING: 2 concurrent tests run on same build %s"%buildname
874         i.add_pid (pid)
875
876     def add_broken (self, buildname, plcindex, step):
877         i=self.get_test(buildname)
878         if not i:
879             i=TestInstance(buildname)
880             self.test_instances.append(i)
881         i.set_broken(plcindex, step)
882
883     matcher_proc=re.compile (".*/proc/(?P<pid>[0-9]+)/cwd.*/root/(?P<buildname>[^/]+)$")
884     matcher_grep=re.compile ("/root/(?P<buildname>[^/]+)/logs/trace.*:TRACE:\s*(?P<plcindex>[0-9]+).*step=(?P<step>\S+).*")
885     matcher_grep_missing=re.compile ("grep: /root/(?P<buildname>[^/]+)/logs/trace: No such file or directory")
886     def sense (self, options):
887         print 'tm',
888         self.starting_ips=[x for x in self.backquote_ssh(['cat',Starting.location], trash_err=True).strip().split('\n') if x]
889
890         # scan timestamps on all tests
891         # this is likely to not invoke ssh so we need to be a bit smarter to get * expanded
892         # xxx would make sense above too
893         command=['bash','-c',"grep . /root/*/timestamp /dev/null"]
894         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
895         for ts_line in ts_lines:
896             if not ts_line.strip(): continue
897             # expect /root/<buildname>/timestamp:<timestamp>
898             try:
899                 (ts_file,timestamp)=ts_line.split(':')
900                 ts_file=os.path.dirname(ts_file)
901                 buildname=os.path.basename(ts_file)
902                 timestamp=int(timestamp)
903                 t=self.add_timestamp(buildname,timestamp)
904             except:  print 'WARNING, could not parse ts line',ts_line
905
906         # let's try to be robust here -- tests that fail very early like e.g.
907         # "Cannot make space for a PLC instance: vplc IP pool exhausted", that occurs as part of provision
908         # will result in a 'trace' symlink to an inexisting 'trace-<>.txt' because no step has gone through
909         # simple 'trace' sohuld exist though as it is created by run_log
910         command=['bash','-c',"grep KO /root/*/logs/trace /dev/null 2>&1" ]
911         trace_lines=self.backquote_ssh (command).split('\n')
912         for line in trace_lines:
913             if not line.strip(): continue
914             m=TestBox.matcher_grep_missing.match(line)
915             if m:
916                 buildname=m.group('buildname')
917                 self.add_broken(buildname,'','NO STEP DONE')
918                 continue
919             m=TestBox.matcher_grep.match(line)
920             if m: 
921                 buildname=m.group('buildname')
922                 plcindex=m.group('plcindex')
923                 step=m.group('step')
924                 self.add_broken(buildname,plcindex, step)
925                 continue
926             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
927             header(">>%s<<"%line)
928
929         pids = self.backquote_ssh (['pgrep','run_log'],trash_err=True)
930         if not pids: return
931         command=['ls','-ld'] + ["/proc/%s/cwd"%pid for pid in pids.split("\n") if pid]
932         ps_lines=self.backquote_ssh (command).split('\n')
933         for line in ps_lines:
934             if not line.strip(): continue
935             m=TestBox.matcher_proc.match(line)
936             if m: 
937                 pid=m.group('pid')
938                 buildname=m.group('buildname')
939                 self.add_running_test(pid, buildname)
940                 continue
941             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
942             header(">>%s<<"%line)
943         
944         
945     def line (self):
946         return self.hostname_fedora()
947
948     def list (self, verbose=False):
949         # verbose shows all tests
950         if verbose:
951             instances = self.test_instances
952             msg="tests"
953         else:
954             instances = [ i for i in self.test_instances if i.is_running() ]
955             msg="running tests"
956
957         if not instances:
958             header ("No %s on %s"%(msg,self.line()))
959         else:
960             header ("%s on %s"%(msg,self.line()))
961             instances.sort(timestamp_sort)
962             for i in instances: print i.line()
963         # show 'starting' regardless of verbose
964         if self.starting_ips:
965             header ("Starting IP addresses on %s"%self.line())
966             self.starting_ips.sort()
967             for starting in self.starting_ips: print starting
968         else:
969             header ("Empty 'starting' on %s"%self.line())
970
971 ############################################################
972 class Options: pass
973
974 class Substrate:
975
976     def __init__ (self, plcs_on_vs=True, plcs_on_lxc=False):
977         self.options=Options()
978         self.options.dry_run=False
979         self.options.verbose=False
980         self.options.reboot=False
981         self.options.soft=False
982         self.test_box = TestBox (self.test_box_spec())
983         self.build_boxes = [ BuildBox(h) for h in self.build_boxes_spec() ]
984         # for compat with older LocalSubstrate
985         try:
986             self.plc_vs_boxes = [ PlcVsBox (h,m) for (h,m) in self.plc_vs_boxes_spec ()]
987             self.plc_lxc_boxes = [ PlcLxcBox (h,m) for (h,m) in self.plc_lxc_boxes_spec ()]
988         except:
989             self.plc_vs_boxes = [ PlcVsBox (h,m) for (h,m) in self.plc_boxes_spec ()]
990             self.plc_lxc_boxes = [ ]
991         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
992         self._sensed=False
993
994         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs",self)
995         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes",self)
996         
997         self.rescope (plcs_on_vs=plcs_on_vs, plcs_on_lxc=plcs_on_lxc)
998
999     # which plc boxes are we interested in ?
1000     def rescope (self, plcs_on_vs, plcs_on_lxc):
1001         self.plc_boxes=[]
1002         if plcs_on_vs: self.plc_boxes += self.plc_vs_boxes
1003         if plcs_on_lxc: self.plc_boxes += self.plc_lxc_boxes
1004         self.default_boxes = self.plc_boxes + self.qemu_boxes
1005         self.all_boxes = self.build_boxes + [ self.test_box ] + self.plc_boxes + self.qemu_boxes
1006
1007     def summary_line (self):
1008         msg  = "["
1009         msg += " %d vp"%len(self.plc_vs_boxes)
1010         msg += " %d xp"%len(self.plc_lxc_boxes)
1011         msg += " %d tried plc boxes"%len(self.plc_boxes)
1012         msg += "]"
1013         return msg
1014
1015     def fqdn (self, hostname):
1016         if hostname.find('.')<0: return "%s.%s"%(hostname,self.domain())
1017         return hostname
1018
1019     # return True if actual sensing takes place
1020     def sense (self,force=False):
1021         if self._sensed and not force: return False
1022         print 'Sensing local substrate...',
1023         for b in self.default_boxes: b.sense(self.options)
1024         print 'Done'
1025         self._sensed=True
1026         return True
1027
1028     def list (self, verbose=False):
1029         for b in self.default_boxes:
1030             b.list()
1031
1032     def add_dummy_plc (self, plc_boxname, plcname):
1033         for pb in self.plc_boxes:
1034             if pb.hostname==plc_boxname:
1035                 pb.add_dummy(plcname)
1036                 return True
1037     def add_dummy_qemu (self, qemu_boxname, qemuname):
1038         for qb in self.qemu_boxes:
1039             if qb.hostname==qemu_boxname:
1040                 qb.add_dummy(qemuname)
1041                 return True
1042
1043     def add_starting_dummy (self, bname, vname):
1044         return self.add_dummy_plc (bname, vname) or self.add_dummy_qemu (bname, vname)
1045
1046     ########## 
1047     def provision (self,plcs,options):
1048         try:
1049             # attach each plc to a plc box and an IP address
1050             plcs = [ self.provision_plc (plc,options) for plc in plcs ]
1051             # attach each node/qemu to a qemu box with an IP address
1052             plcs = [ self.provision_qemus (plc,options) for plc in plcs ]
1053             # update the SFA spec accordingly
1054             plcs = [ self.localize_sfa_rspec(plc,options) for plc in plcs ]
1055             self.list()
1056             return plcs
1057         except Exception, e:
1058             print '* Could not provision this test on current substrate','--',e,'--','exiting'
1059             traceback.print_exc()
1060             sys.exit(1)
1061
1062     # it is expected that a couple of options like ips_bplc and ips_vplc 
1063     # are set or unset together
1064     @staticmethod
1065     def check_options (x,y):
1066         if not x and not y: return True
1067         return len(x)==len(y)
1068
1069     # find an available plc box (or make space)
1070     # and a free IP address (using options if present)
1071     def provision_plc (self, plc, options):
1072         
1073         assert Substrate.check_options (options.ips_bplc, options.ips_vplc)
1074
1075         #### let's find an IP address for that plc
1076         # look in options 
1077         if options.ips_vplc:
1078             # this is a rerun
1079             # we don't check anything here, 
1080             # it is the caller's responsability to cleanup and make sure this makes sense
1081             plc_boxname = options.ips_bplc.pop()
1082             vplc_hostname=options.ips_vplc.pop()
1083         else:
1084             if self.sense(): self.list()
1085             plc_boxname=None
1086             vplc_hostname=None
1087             # try to find an available IP 
1088             self.vplc_pool.sense()
1089             couple=self.vplc_pool.next_free()
1090             if couple:
1091                 (vplc_hostname,unused)=couple
1092             #### we need to find one plc box that still has a slot
1093             max_free=0
1094             # use the box that has max free spots for load balancing
1095             for pb in self.plc_boxes:
1096                 free=pb.free_slots()
1097                 if free>max_free:
1098                     plc_boxname=pb.hostname
1099                     max_free=free
1100             # if there's no available slot in the plc_boxes, or we need a free IP address
1101             # make space by killing the oldest running instance
1102             if not plc_boxname or not vplc_hostname:
1103                 # find the oldest of all our instances
1104                 all_plc_instances=reduce(lambda x, y: x+y, 
1105                                          [ pb.plc_instances for pb in self.plc_boxes ],
1106                                          [])
1107                 all_plc_instances.sort(timestamp_sort)
1108                 try:
1109                     plc_instance_to_kill=all_plc_instances[0]
1110                 except:
1111                     msg=""
1112                     if not plc_boxname: msg += " PLC boxes are full"
1113                     if not vplc_hostname: msg += " vplc IP pool exhausted"
1114                     msg += " %s"%self.summary_line()
1115                     raise Exception,"Cannot make space for a PLC instance:"+msg
1116                 freed_plc_boxname=plc_instance_to_kill.plc_box.hostname
1117                 freed_vplc_hostname=plc_instance_to_kill.vplcname()
1118                 message='killing oldest plc instance = %s on %s'%(plc_instance_to_kill.line(),
1119                                                                   freed_plc_boxname)
1120                 plc_instance_to_kill.kill()
1121                 # use this new plcbox if that was the problem
1122                 if not plc_boxname:
1123                     plc_boxname=freed_plc_boxname
1124                 # ditto for the IP address
1125                 if not vplc_hostname:
1126                     vplc_hostname=freed_vplc_hostname
1127                     # record in pool as mine
1128                     self.vplc_pool.set_mine(vplc_hostname)
1129
1130         # 
1131         self.add_dummy_plc(plc_boxname,plc['name'])
1132         vplc_ip = self.vplc_pool.get_ip(vplc_hostname)
1133         self.vplc_pool.add_starting(vplc_hostname, plc_boxname)
1134
1135         #### compute a helpful vserver name
1136         # remove domain in hostname
1137         vplc_short = short_hostname(vplc_hostname)
1138         vservername = "%s-%d-%s" % (options.buildname,plc['index'],vplc_short)
1139         plc_name = "%s_%s"%(plc['name'],vplc_short)
1140
1141         utils.header( 'PROVISION plc %s in box %s at IP %s as %s'%\
1142                           (plc['name'],plc_boxname,vplc_hostname,vservername))
1143
1144         #### apply in the plc_spec
1145         # # informative
1146         # label=options.personality.replace("linux","")
1147         mapper = {'plc': [ ('*' , {'host_box':plc_boxname,
1148                                    # 'name':'%s-'+label,
1149                                    'name': plc_name,
1150                                    'vservername':vservername,
1151                                    'vserverip':vplc_ip,
1152                                    'PLC_DB_HOST':vplc_hostname,
1153                                    'PLC_API_HOST':vplc_hostname,
1154                                    'PLC_BOOT_HOST':vplc_hostname,
1155                                    'PLC_WWW_HOST':vplc_hostname,
1156                                    'PLC_NET_DNS1' : self.network_settings() [ 'interface_fields:dns1' ],
1157                                    'PLC_NET_DNS2' : self.network_settings() [ 'interface_fields:dns2' ],
1158                                    } ) ]
1159                   }
1160
1161
1162         # mappers only work on a list of plcs
1163         return TestMapper([plc],options).map(mapper)[0]
1164
1165     ##########
1166     def provision_qemus (self, plc, options):
1167
1168         assert Substrate.check_options (options.ips_bnode, options.ips_vnode)
1169
1170         test_mapper = TestMapper ([plc], options)
1171         nodenames = test_mapper.node_names()
1172         maps=[]
1173         for nodename in nodenames:
1174
1175             if options.ips_vnode:
1176                 # as above, it's a rerun, take it for granted
1177                 qemu_boxname=options.ips_bnode.pop()
1178                 vnode_hostname=options.ips_vnode.pop()
1179             else:
1180                 if self.sense(): self.list()
1181                 qemu_boxname=None
1182                 vnode_hostname=None
1183                 # try to find an available IP 
1184                 self.vnode_pool.sense()
1185                 couple=self.vnode_pool.next_free()
1186                 if couple:
1187                     (vnode_hostname,unused)=couple
1188                 # find a physical box
1189                 max_free=0
1190                 # use the box that has max free spots for load balancing
1191                 for qb in self.qemu_boxes:
1192                     free=qb.free_slots()
1193                     if free>max_free:
1194                         qemu_boxname=qb.hostname
1195                         max_free=free
1196                 # if we miss the box or the IP, kill the oldest instance
1197                 if not qemu_boxname or not vnode_hostname:
1198                 # find the oldest of all our instances
1199                     all_qemu_instances=reduce(lambda x, y: x+y, 
1200                                               [ qb.qemu_instances for qb in self.qemu_boxes ],
1201                                               [])
1202                     all_qemu_instances.sort(timestamp_sort)
1203                     try:
1204                         qemu_instance_to_kill=all_qemu_instances[0]
1205                     except:
1206                         msg=""
1207                         if not qemu_boxname: msg += " QEMU boxes are full"
1208                         if not vnode_hostname: msg += " vnode IP pool exhausted" 
1209                         msg += " %s"%self.summary_line()
1210                         raise Exception,"Cannot make space for a QEMU instance:"+msg
1211                     freed_qemu_boxname=qemu_instance_to_kill.qemu_box.hostname
1212                     freed_vnode_hostname=short_hostname(qemu_instance_to_kill.nodename)
1213                     # kill it
1214                     message='killing oldest qemu node = %s on %s'%(qemu_instance_to_kill.line(),
1215                                                                    freed_qemu_boxname)
1216                     qemu_instance_to_kill.kill()
1217                     # use these freed resources where needed
1218                     if not qemu_boxname:
1219                         qemu_boxname=freed_qemu_boxname
1220                     if not vnode_hostname:
1221                         vnode_hostname=freed_vnode_hostname
1222                         self.vnode_pool.set_mine(vnode_hostname)
1223
1224             self.add_dummy_qemu (qemu_boxname,vnode_hostname)
1225             mac=self.vnode_pool.retrieve_userdata(vnode_hostname)
1226             ip=self.vnode_pool.get_ip (vnode_hostname)
1227             self.vnode_pool.add_starting(vnode_hostname,qemu_boxname)
1228
1229             vnode_fqdn = self.fqdn(vnode_hostname)
1230             nodemap={'host_box':qemu_boxname,
1231                      'node_fields:hostname':vnode_fqdn,
1232                      'interface_fields:ip':ip, 
1233                      'ipaddress_fields:ip_addr':ip, 
1234                      'interface_fields:mac':mac,
1235                      }
1236             nodemap.update(self.network_settings())
1237             maps.append ( (nodename, nodemap) )
1238
1239             utils.header("PROVISION node %s in box %s at IP %s with MAC %s"%\
1240                              (nodename,qemu_boxname,vnode_hostname,mac))
1241
1242         return test_mapper.map({'node':maps})[0]
1243
1244     def localize_sfa_rspec (self,plc,options):
1245        
1246         plc['sfa']['SFA_REGISTRY_HOST'] = plc['PLC_DB_HOST']
1247         plc['sfa']['SFA_AGGREGATE_HOST'] = plc['PLC_DB_HOST']
1248         plc['sfa']['SFA_SM_HOST'] = plc['PLC_DB_HOST']
1249         plc['sfa']['SFA_DB_HOST'] = plc['PLC_DB_HOST']
1250         plc['sfa']['SFA_PLC_URL'] = 'https://' + plc['PLC_API_HOST'] + ':443/PLCAPI/' 
1251         return plc
1252
1253     #################### release:
1254     def release (self,options):
1255         self.vplc_pool.release_my_starting()
1256         self.vnode_pool.release_my_starting()
1257         pass
1258
1259     #################### show results for interactive mode
1260     def get_box (self,boxname):
1261         for b in self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box] :
1262             if b.shortname()==boxname:                          return b
1263             try:
1264                 if b.shortname()==boxname.split('.')[0]:        return b
1265             except: pass
1266         print "Could not find box %s"%boxname
1267         return None
1268
1269     def list_boxes(self,box_or_names):
1270         print 'Sensing',
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.sense(self.options)
1275         print 'Done'
1276         for box in box_or_names:
1277             if not isinstance(box,Box): box=self.get_box(box)
1278             if not box: continue
1279             box.list(self.options.verbose)
1280
1281     def reboot_boxes(self,box_or_names):
1282         for box in box_or_names:
1283             if not isinstance(box,Box): box=self.get_box(box)
1284             if not box: continue
1285             box.reboot(self.options)
1286
1287     ####################
1288     # can be run as a utility to probe/display/manage the local infrastructure
1289     def main (self):
1290         parser=OptionParser()
1291         parser.add_option ('-r',"--reboot",action='store_true',dest='reboot',default=False,
1292                            help='reboot mode (use shutdown -r)')
1293         parser.add_option ('-s',"--soft",action='store_true',dest='soft',default=False,
1294                            help='soft mode for reboot (vserver stop or kill qemus)')
1295         parser.add_option ('-t',"--testbox",action='store_true',dest='testbox',default=False,
1296                            help='add test box') 
1297         parser.add_option ('-b',"--build",action='store_true',dest='builds',default=False,
1298                            help='add build boxes')
1299         parser.add_option ('-p',"--plc",action='store_true',dest='plcs',default=False,
1300                            help='add plc boxes')
1301         parser.add_option ('-q',"--qemu",action='store_true',dest='qemus',default=False,
1302                            help='add qemu boxes') 
1303         parser.add_option ('-a',"--all",action='store_true',dest='all',default=False,
1304                            help='address all known  boxes, like -b -t -p -q')
1305         parser.add_option ('-v',"--verbose",action='store_true',dest='verbose',default=False,
1306                            help='verbose mode')
1307         parser.add_option ('-n',"--dry_run",action='store_true',dest='dry_run',default=False,
1308                            help='dry run mode')
1309         (self.options,args)=parser.parse_args()
1310
1311         self.rescope (plcs_on_vs=True, plcs_on_lxc=True)
1312
1313         boxes=args
1314         if self.options.testbox: boxes += [self.test_box]
1315         if self.options.builds: boxes += self.build_boxes
1316         if self.options.plcs: boxes += self.plc_boxes
1317         if self.options.qemus: boxes += self.qemu_boxes
1318         if self.options.all: boxes += self.all_boxes
1319         
1320         # default scope is -b -p -q -t
1321         if not boxes:
1322             boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box]
1323
1324         if self.options.reboot: self.reboot_boxes (boxes)
1325         else:                   self.list_boxes (boxes)