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