sense first, load starting second, minimize the risk for concurrently
[tests.git] / system / Substrate.py
1 #
2 # Thierry Parmentelat <thierry.parmentelat@inria.fr>
3 # Copyright (C) 2010 INRIA 
4 #
5 # #################### history
6 #
7 # This is a complete rewrite of TestResources/Tracker/Pool
8 # we don't use trackers anymore and just probe/sense the running 
9 # boxes to figure out where we are
10 # in order to implement some fairness in the round-robin allocation scheme
11 # we need an indication of the 'age' of each running entity, 
12 # hence the 'timestamp-*' steps in TestPlc
13
14 # this should be much more flexible:
15 # * supports several plc boxes 
16 # * supports several qemu guests per host
17 # * no need to worry about tracker being in sync or not
18 #
19 # #################### howto use
20 #
21 # each site is to write its own LocalSubstrate.py, 
22 # (see e.g. LocalSubstrate.inria.py)
23 # LocalSubstrate.py is expected to be in /root on the testmaster box
24 # and needs to define
25 # MYPLCs
26 # . the vserver-capable boxes used for hosting myplcs
27 # .  and their admissible load (max # of myplcs)
28 # . the pool of DNS-names and IP-addresses available for myplcs
29 # QEMU nodes
30 # . the kvm-qemu capable boxes to host qemu instances
31 # .  and their admissible load (max # of myplcs)
32 # . the pool of DNS-names and IP-addresses available for nodes
33
34 # #################### implem. note
35
36 # this model relies on 'sensing' the substrate, 
37 # i.e. probing all the boxes for their running instances of vservers and qemu
38 # this is how we get rid of tracker inconsistencies 
39 # however there is a 'black hole' between the time where a given address is 
40 # allocated and when it actually gets used/pingable
41 # this is why we still need a shared knowledge among running tests
42 # in a file named /root/starting
43 # this is connected to the Pool class 
44
45 # ####################
46
47 import os.path, sys
48 import time
49 import re
50 import traceback
51 import subprocess
52 import commands
53 import socket
54 from optparse import OptionParser
55
56 import utils
57 from TestSsh import TestSsh
58 from TestMapper import TestMapper
59
60 def header (message,banner=True):
61     if not message: return
62     if banner: print "===============",
63     print message
64     sys.stdout.flush()
65
66 def timestamp_sort(o1,o2): 
67     if not o1.timestamp:        return -1
68     elif not o2.timestamp:      return 1
69     else:                       return o2.timestamp-o1.timestamp
70
71 ####################
72 # pool class
73 # allows to pick an available IP among a pool
74 # input is expressed as a list of tuples (hostname,ip,user_data)
75 # that can be searched iteratively for a free slot
76 # e.g.
77 # pool = [ (hostname1,user_data1),  
78 #          (hostname2,user_data2),  
79 #          (hostname3,user_data2),  
80 #          (hostname4,user_data4) ]
81 # assuming that ip1 and ip3 are taken (pingable), then we'd get
82 # pool=Pool(pool)
83 # pool.next_free() -> entry2
84 # pool.next_free() -> entry4
85 # pool.next_free() -> None
86 # that is, even if ip2 is not busy/pingable when the second next_free() is issued
87
88 class PoolItem:
89     def __init__ (self,hostname,userdata):
90         self.hostname=hostname
91         self.userdata=userdata
92         # slot holds 'busy' or 'free' or 'mine' or 'starting' or None
93         # 'mine' is for our own stuff, 'starting' from the concurrent tests
94         self.status=None
95         self.ip=None
96
97     def line(self):
98         return "Pooled %s (%s) -> %s"%(self.hostname,self.userdata, self.status)
99     def get_ip(self):
100         if self.ip: return self.ip
101         ip=socket.gethostbyname(self.hostname)
102         self.ip=ip
103         return ip
104
105 class Pool:
106
107     def __init__ (self, tuples,message):
108         self.pool= [ PoolItem (h,u) for (h,u) in tuples ] 
109         self.message=message
110
111     def sense (self):
112         print 'Checking IP pool',self.message,
113         for item in self.pool:
114             if item.status is not None: 
115                 continue
116             if self.check_ping (item.hostname): 
117                 item.status='busy'
118             else:
119                 item.status='free'
120         print 'Done'
121
122     def list (self):
123         for i in self.pool: print i.line()
124
125     def retrieve_userdata (self, hostname):
126         for i in self.pool: 
127             if i.hostname==hostname: return i.userdata
128         return None
129
130     def get_ip (self, hostname):
131         # use cached if in pool
132         for i in self.pool: 
133             if i.hostname==hostname: return i.get_ip()
134         # otherwise just ask dns again
135         return socket.gethostbyname(hostname)
136
137     def next_free (self):
138         for i in self.pool:
139             if i.status in ['busy','mine','starting' ]: continue
140             i.status='mine'
141             return (i.hostname,i.userdata)
142         raise Exception,"No IP address available in pool %s"%self.message
143
144     # OS-dependent ping option (support for macos, for convenience)
145     ping_timeout_option = None
146     # checks whether a given hostname/ip responds to ping
147     def check_ping (self,hostname):
148         if not Pool.ping_timeout_option:
149             (status,osname) = commands.getstatusoutput("uname -s")
150             if status != 0:
151                 raise Exception, "TestPool: Cannot figure your OS name"
152             if osname == "Linux":
153                 Pool.ping_timeout_option="-w"
154             elif osname == "Darwin":
155                 Pool.ping_timeout_option="-t"
156
157         command="ping -c 1 %s 1 %s"%(Pool.ping_timeout_option,hostname)
158         (status,output) = commands.getstatusoutput(command)
159         if status==0:   print '+',
160         else:           print '-',
161         return status == 0
162
163     # the place were other test instances tell about their not-yet-started
164     # instances, that go undetected through sensing
165     starting='/root/starting'
166     def add_starting (self, name):
167         try:    items=[line.strip() for line in file(Pool.starting).readlines()]
168         except: items=[]
169         if not name in items:
170             file(Pool.starting,'a').write(name+'\n')
171         for i in self.pool:
172             if i.hostname==name: i.status='mine'
173             
174     def load_starting (self):
175         try:    items=[line.strip() for line in file(Pool.starting).readlines()]
176         except: items=[]
177         for item in items:
178             for i in self.pool:
179                 if i.hostname==item: i.status='starting'
180
181     def release_my_fakes (self):
182         for i in self.pool:
183             if i.status=='mine': 
184                 self.del_starting(i.hostname)
185                 i.status=None
186
187     def del_starting (self, name):
188         try:    items=[line.strip() for line in file(Pool.starting).readlines()]
189         except: items=[]
190         if name in items:
191             f=file(Pool.starting,'w')
192             for item in items: 
193                 if item != name: f.write(item+'\n')
194             f.close()
195     
196 ####################
197 class Box:
198     def __init__ (self,hostname):
199         self.hostname=hostname
200     def simple_hostname (self):
201         return self.hostname.split('.')[0]
202     def test_ssh (self): return TestSsh(self.hostname,username='root',unknown_host=False)
203     def reboot (self):
204         self.test_ssh().run("shutdown -r now",message="Rebooting %s"%self.hostname)
205
206     def run(self,argv,message=None,trash_err=False,dry_run=False):
207         if dry_run:
208             print 'DRY_RUN:',
209             print " ".join(argv)
210             return 0
211         else:
212             header(message)
213             if not trash_err:
214                 return subprocess.call(argv)
215             else:
216                 return subprocess.call(argv,stderr=file('/dev/null','w'))
217                 
218     def run_ssh (self, argv, message, trash_err=False):
219         ssh_argv = self.test_ssh().actual_argv(argv)
220         result=self.run (ssh_argv, message, trash_err)
221         if result!=0:
222             print "WARNING: failed to run %s on %s"%(" ".join(argv),self.hostname)
223         return result
224
225     def backquote (self, argv, trash_err=False):
226         if not trash_err:
227             return subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
228         else:
229             return subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
230
231     def backquote_ssh (self, argv, trash_err=False):
232         # first probe the ssh link
233         probe_argv=self.test_ssh().actual_argv(['hostname'])
234         hostname=self.backquote ( probe_argv, trash_err=True )
235         if not hostname:
236             print "root@%s unreachable"%self.hostname
237             return ''
238         else:
239             return self.backquote( self.test_ssh().actual_argv(argv), trash_err)
240
241 ############################################################
242 class BuildInstance:
243     def __init__ (self, buildname, pid, buildbox):
244         self.buildname=buildname
245         self.buildbox=buildbox
246         self.pids=[pid]
247
248     def add_pid(self,pid):
249         self.pids.append(pid)
250
251     def line (self):
252         return "== %s == (pids=%r)"%(self.buildname,self.pids)
253
254 class BuildBox (Box):
255     def __init__ (self,hostname):
256         Box.__init__(self,hostname)
257         self.build_instances=[]
258
259     def add_build (self,buildname,pid):
260         for build in self.build_instances:
261             if build.buildname==buildname: 
262                 build.add_pid(pid)
263                 return
264         self.build_instances.append(BuildInstance(buildname, pid, self))
265
266     def list(self):
267         if not self.build_instances: 
268             header ('No build process on %s (%s)'%(self.hostname,self.uptime()))
269         else:
270             header ("Builds on %s (%s)"%(self.hostname,self.uptime()))
271             for b in self.build_instances: 
272                 header (b.line(),banner=False)
273
274     def uptime(self):
275         if hasattr(self,'_uptime') and self._uptime: return self._uptime
276         return '*undef* uptime'
277
278     # inspect box and find currently running builds
279     matcher=re.compile("\s*(?P<pid>[0-9]+).*-[bo]\s+(?P<buildname>[^\s]+)(\s|\Z)")
280     def sense(self,reboot=False,verbose=True):
281         if reboot:
282             self.reboot(box)
283             return
284         print 'b',
285         command=['uptime']
286         self._uptime=self.backquote_ssh(command,trash_err=True).strip()
287         if not self._uptime: self._uptime='unreachable'
288         pids=self.backquote_ssh(['pgrep','build'],trash_err=True)
289         if not pids: return
290         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
291         ps_lines=self.backquote_ssh (command).split('\n')
292         for line in ps_lines:
293             if not line.strip() or line.find('PID')>=0: continue
294             m=BuildBox.matcher.match(line)
295             if m: self.add_build (m.group('buildname'),m.group('pid'))
296             else: header('command %r returned line that failed to match'%command)
297
298 ############################################################
299 class PlcInstance:
300     def __init__ (self, vservername, ctxid, plcbox):
301         self.vservername=vservername
302         self.ctxid=ctxid
303         self.plc_box=plcbox
304         # unknown yet
305         self.timestamp=None
306
307     def set_timestamp (self,timestamp): self.timestamp=timestamp
308     def set_now (self): self.timestamp=int(time.time())
309     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
310
311     def line (self):
312         msg="== %s == (ctx=%s)"%(self.vservername,self.ctxid)
313         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
314         else:              msg += " *unknown timestamp*"
315         if self.ctxid==0: msg+=" not (yet?) running"
316         return msg
317
318     def kill (self):
319         msg="vserver stopping %s on %s"%(self.vservername,self.plc_box.hostname)
320         self.plc_box.run_ssh(['vserver',self.vservername,'stop'],msg)
321         self.plc_box.forget(self)
322
323 class PlcBox (Box):
324     def __init__ (self, hostname, max_plcs):
325         Box.__init__(self,hostname)
326         self.plc_instances=[]
327         self.max_plcs=max_plcs
328
329     def add_vserver (self,vservername,ctxid):
330         for plc in self.plc_instances:
331             if plc.vservername==vservername: 
332                 header("WARNING, duplicate myplc %s running on %s"%\
333                            (vservername,self.hostname),banner=False)
334                 return
335         self.plc_instances.append(PlcInstance(vservername,ctxid,self))
336     
337     def forget (self, plc_instance):
338         self.plc_instances.remove(plc_instance)
339
340     # fill one slot even though this one is not started yet
341     def add_dummy (self, plcname):
342         dummy=PlcInstance('dummy_'+plcname,0,self)
343         dummy.set_now()
344         self.plc_instances.append(dummy)
345
346     def line(self): 
347         msg="%s [max=%d,%d free] (%s)"%(self.hostname, self.max_plcs,self.free_spots(),self.uname())
348         return msg
349         
350     def list(self):
351         if not self.plc_instances: 
352             header ('No vserver running on %s'%(self.line()))
353         else:
354             header ("Active plc VMs on %s"%self.line())
355             for p in self.plc_instances: 
356                 header (p.line(),banner=False)
357
358     def free_spots (self):
359         return self.max_plcs - len(self.plc_instances)
360
361     def uname(self):
362         if hasattr(self,'_uname') and self._uname: return self._uname
363         return '*undef* uname'
364
365     def plc_instance_by_vservername (self, vservername):
366         for p in self.plc_instances:
367             if p.vservername==vservername: return p
368         return None
369
370     def sense (self, reboot=False, soft=False):
371         if reboot:
372             # remove mark for all running servers to avoid resurrection
373             stop_command=['rm','-rf','/etc/vservers/*/apps/init/mark']
374             self.run_ssh(stop_command,"Removing all vserver marks on %s"%self.hostname)
375             if not soft:
376                 self.reboot()
377                 return
378             else:
379                 self.run_ssh(['service','util-vserver','stop'],"Stopping all running vservers")
380             return
381         print 'p',
382         self._uname=self.backquote_ssh(['uname','-r']).strip()
383         # try to find fullname (vserver_stat truncates to a ridiculously short name)
384         # fetch the contexts for all vservers on that box
385         map_command=['grep','.','/etc/vservers/*/context','/dev/null',]
386         context_map=self.backquote_ssh (map_command)
387         # at this point we have a set of lines like
388         # /etc/vservers/2010.01.20--k27-f12-32-vplc03/context:40144
389         ctx_dict={}
390         for map_line in context_map.split("\n"):
391             if not map_line: continue
392             [path,xid] = map_line.split(':')
393             ctx_dict[xid]=os.path.basename(os.path.dirname(path))
394         # at this point ctx_id maps context id to vservername
395
396         command=['vserver-stat']
397         vserver_stat = self.backquote_ssh (command)
398         for vserver_line in vserver_stat.split("\n"):
399             if not vserver_line: continue
400             context=vserver_line.split()[0]
401             if context=="CTX": continue
402             longname=ctx_dict[context]
403             self.add_vserver(longname,context)
404 #            print self.margin_outline(self.vplcname(longname)),"%(vserver_line)s [=%(longname)s]"%locals()
405
406         # scan timestamps
407         running_ctx_ids = [ i.ctxid for i in self.plc_instances ]
408         command=   ['grep','.']
409         command += ['/vservers/%s/timestamp'%b for b in running_ctx_ids]
410         command += ['/dev/null']
411         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
412         for ts_line in ts_lines:
413             if not ts_line.strip(): continue
414             # expect /vservers/<vservername>/timestamp:<timestamp>
415             try:
416                 (_,__,vservername,tail)=ts_line.split('/')
417                 (_,timestamp)=tail.split(':')
418                 timestamp=int(timestamp)
419                 q=self.plc_instance_by_vservername(vservername)
420                 if not q: 
421                     print 'WARNING unattached plc instance',ts_line
422                     print 'was expeting to find',vservername,'in',[i.vservername for i in self.plc_instances]
423                     continue
424                 q.set_timestamp(timestamp)
425             except:  print 'WARNING, could not parse ts line',ts_line
426         
427
428
429
430 ############################################################
431 class QemuInstance: 
432     def __init__ (self, nodename, pid, qemubox):
433         self.nodename=nodename
434         self.pid=pid
435         self.qemu_box=qemubox
436         # not known yet
437         self.buildname=None
438         self.timestamp=None
439         
440     def set_buildname (self,buildname): self.buildname=buildname
441     def set_timestamp (self,timestamp): self.timestamp=timestamp
442     def set_now (self): self.timestamp=int(time.time())
443     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
444     
445     def line (self):
446         msg = "== %s == (pid=%s)"%(self.nodename,self.pid)
447         if self.buildname: msg += " <--> %s"%self.buildname
448         else:              msg += " *unknown build*"
449         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
450         else:              msg += " *unknown timestamp*"
451         if self.pid:       msg += "pid=%s"%self.pid
452         else:              msg += " not (yet?) running"
453         return msg
454     
455     def kill(self):
456         if self.pid==0: print "cannot kill qemu %s with pid==0"%self.nodename
457         msg="Killing qemu %s with pid=%s on box %s"%(self.nodename,self.pid,self.qemu_box.hostname)
458         self.qemu_box.run_ssh(['kill',"%s"%self.pid],msg)
459         self.qemu_box.forget(self)
460
461
462 class QemuBox (Box):
463     def __init__ (self, hostname, max_qemus):
464         Box.__init__(self,hostname)
465         self.qemu_instances=[]
466         self.max_qemus=max_qemus
467
468     def add_node (self,nodename,pid):
469         for qemu in self.qemu_instances:
470             if qemu.nodename==nodename: 
471                 header("WARNING, duplicate qemu %s running on %s"%\
472                            (nodename,self.hostname), banner=False)
473                 return
474         self.qemu_instances.append(QemuInstance(nodename,pid,self))
475
476     def forget (self, qemu_instance):
477         self.qemu_instances.remove(qemu_instance)
478
479     # fill one slot even though this one is not started yet
480     def add_dummy (self, nodename):
481         dummy=QemuInstance('dummy_'+nodename,0,self)
482         dummy.set_now()
483         self.qemu_instances.append(dummy)
484
485     def line (self):
486         msg="%s [max=%d,%d free] (%s)"%(self.hostname, self.max_qemus,self.free_spots(),self.driver())
487         return msg
488
489     def list(self):
490         if not self.qemu_instances: 
491             header ('No qemu process on %s'%(self.line()))
492         else:
493             header ("Active qemu processes on %s"%(self.line()))
494             for q in self.qemu_instances: 
495                 header (q.line(),banner=False)
496
497     def free_spots (self):
498         return self.max_qemus - len(self.qemu_instances)
499
500     def driver(self):
501         if hasattr(self,'_driver') and self._driver: return self._driver
502         return '*undef* driver'
503
504     def qemu_instance_by_pid (self,pid):
505         for q in self.qemu_instances:
506             if q.pid==pid: return q
507         return None
508
509     def qemu_instance_by_nodename_buildname (self,nodename,buildname):
510         for q in self.qemu_instances:
511             if q.nodename==nodename and q.buildname==buildname:
512                 return q
513         return None
514
515     matcher=re.compile("\s*(?P<pid>[0-9]+).*-cdrom\s+(?P<nodename>[^\s]+)\.iso")
516     def sense(self, reboot=False, soft=False):
517         if reboot:
518             if not soft:
519                 self.reboot()
520             else:
521                 self.run_ssh(box,['pkill','qemu'],"Killing qemu instances")
522             return
523         print 'q',
524         modules=self.backquote_ssh(['lsmod']).split('\n')
525         self._driver='*NO kqemu/kmv_intel MODULE LOADED*'
526         for module in modules:
527             if module.find('kqemu')==0:
528                 self._driver='kqemu module loaded'
529             # kvm might be loaded without vkm_intel (we dont have AMD)
530             elif module.find('kvm_intel')==0:
531                 self._driver='kvm_intel module loaded'
532         ########## find out running pids
533         pids=self.backquote_ssh(['pgrep','qemu'])
534         if not pids: return
535         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
536         ps_lines = self.backquote_ssh (command).split("\n")
537         for line in ps_lines:
538             if not line.strip() or line.find('PID') >=0 : continue
539             m=QemuBox.matcher.match(line)
540             if m: self.add_node (m.group('nodename'),m.group('pid'))
541             else: header('command %r returned line that failed to match'%command)
542         ########## retrieve alive instances and map to build
543         live_builds=[]
544         command=['grep','.','*/*/qemu.pid','/dev/null']
545         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
546         for pid_line in pid_lines:
547             if not pid_line.strip(): continue
548             # expect <build>/<nodename>/qemu.pid:<pid>pid
549             try:
550                 (buildname,nodename,tail)=pid_line.split('/')
551                 (_,pid)=tail.split(':')
552                 q=self.qemu_instance_by_pid (pid)
553                 if not q: continue
554                 q.set_buildname(buildname)
555                 live_builds.append(buildname)
556             except: print 'WARNING, could not parse pid line',pid_line
557         # retrieve timestamps
558         command=   ['grep','.']
559         command += ['%s/*/timestamp'%b for b in live_builds]
560         command += ['/dev/null']
561         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
562         for ts_line in ts_lines:
563             if not ts_line.strip(): continue
564             # expect <build>/<nodename>/timestamp:<timestamp>
565             try:
566                 (buildname,nodename,tail)=ts_line.split('/')
567                 nodename=nodename.replace('qemu-','')
568                 (_,timestamp)=tail.split(':')
569                 timestamp=int(timestamp)
570                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
571                 if not q: 
572                     print 'WARNING unattached qemu instance',ts_line,nodename,buildname
573                     continue
574                 q.set_timestamp(timestamp)
575             except:  print 'WARNING, could not parse ts line',ts_line
576
577 ############################################################
578 class Options: pass
579
580 class Substrate:
581
582     def test (self): 
583         self.sense()
584
585     def __init__ (self):
586         self.options=Options()
587         self.options.dry_run=False
588         self.options.verbose=False
589         self.options.probe=True
590         self.options.soft=True
591         self.build_boxes = [ BuildBox(h) for h in self.build_boxes_spec() ]
592         self.plc_boxes = [ PlcBox (h,m) for (h,m) in self.plc_boxes_spec ()]
593         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
594         self.all_boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes
595         self._sensed=False
596
597         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs")
598         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes")
599
600 #    def build_box_names (self):
601 #        return [ h for h in self.build_boxes_spec() ]
602 #    def plc_boxes (self):
603 #        return [ h for (h,m) in self.plc_boxes_spec() ]
604 #    def qemu_boxes (self):
605 #        return [ h for (h,m) in self.qemu_boxes_spec() ]
606
607     def sense (self,force=False):
608         if self._sensed and not force: return
609         print 'Sensing local substrate...',
610         for b in self.all_boxes: b.sense()
611         print 'Done'
612         self._sensed=True
613
614     ########## 
615     def provision (self,plcs,options):
616         try:
617             self.sense()
618             self.list_all()
619             # attach each plc to a plc box and an IP address
620             plcs = [ self.provision_plc (plc,options) for plc in plcs ]
621             # attach each node/qemu to a qemu box with an IP address
622             plcs = [ self.provision_qemus (plc,options) for plc in plcs ]
623             # update the SFA spec accordingly
624             plcs = [ self.localize_sfa_rspec(plc,options) for plc in plcs ]
625             return plcs
626         except Exception, e:
627             print '* Could not provision this test on current substrate','--',e,'--','exiting'
628             traceback.print_exc()
629             sys.exit(1)
630
631     # find an available plc box (or make space)
632     # and a free IP address (using options if present)
633     def provision_plc (self, plc, options):
634         #### we need to find one plc box that still has a slot
635         plc_box=None
636         max_free=0
637         # use the box that has max free spots for load balancing
638         for pb in self.plc_boxes:
639             free=pb.free_spots()
640             if free>max_free:
641                 plc_box=pb
642                 max_free=free
643         # everything is already used
644         if not plc_box:
645             # find the oldest of all our instances
646             all_plc_instances=reduce(lambda x, y: x+y, 
647                                      [ pb.plc_instances for pb in self.plc_boxes ],
648                                      [])
649             all_plc_instances.sort(timestamp_sort)
650             plc_instance_to_kill=all_plc_instances[0]
651             plc_box=plc_instance_to_kill.plc_box
652             plc_instance_to_kill.kill()
653             print 'killed oldest = %s on %s'%(plc_instance_to_kill.line(),
654                                              plc_instance_to_kill.plc_box.hostname)
655
656         utils.header( 'plc %s -> box %s'%(plc['name'],plc_box.line()))
657         plc_box.add_dummy(plc['name'])
658         #### OK we have a box to run in, let's find an IP address
659         # look in options
660         if options.ips_vplc:
661             vplc_hostname=options.ips_vplc.pop()
662         else:
663             self.vplc_pool.sense()
664             self.vplc_pool.load_starting()
665             (vplc_hostname,unused)=self.vplc_pool.next_free()
666         vplc_ip = self.vplc_pool.get_ip(vplc_hostname)
667         self.vplc_pool.add_starting(vplc_hostname)
668
669         #### compute a helpful vserver name
670         # remove domain in hostname
671         vplc_simple = vplc_hostname.split('.')[0]
672         vservername = "%s-%d-%s" % (options.buildname,plc['index'],vplc_simple)
673         plc_name = "%s_%s"%(plc['name'],vplc_simple)
674
675         #### apply in the plc_spec
676         # # informative
677         # label=options.personality.replace("linux","")
678         mapper = {'plc': [ ('*' , {'hostname':plc_box.hostname,
679                                    # 'name':'%s-'+label,
680                                    'name': plc_name,
681                                    'vservername':vservername,
682                                    'vserverip':vplc_ip,
683                                    'PLC_DB_HOST':vplc_hostname,
684                                    'PLC_API_HOST':vplc_hostname,
685                                    'PLC_BOOT_HOST':vplc_hostname,
686                                    'PLC_WWW_HOST':vplc_hostname,
687                                    'PLC_NET_DNS1' : self.network_settings() [ 'interface_fields:dns1' ],
688                                    'PLC_NET_DNS2' : self.network_settings() [ 'interface_fields:dns2' ],
689                                    } ) ]
690                   }
691
692         utils.header("Attaching %s on IP %s in vserver %s"%(plc['name'],vplc_hostname,vservername))
693         # mappers only work on a list of plcs
694         return TestMapper([plc],options).map(mapper)[0]
695
696     ##########
697     def provision_qemus (self, plc, options):
698         test_mapper = TestMapper ([plc], options)
699         nodenames = test_mapper.node_names()
700         maps=[]
701         for nodename in nodenames:
702             #### similarly we want to find a qemu box that can host us
703             qemu_box=None
704             max_free=0
705             # use the box that has max free spots for load balancing
706             for qb in self.qemu_boxes:
707                 free=qb.free_spots()
708             if free>max_free:
709                 qemu_box=qb
710                 max_free=free
711             # everything is already used
712             if not qemu_box:
713                 # find the oldest of all our instances
714                 all_qemu_instances=reduce(lambda x, y: x+y, 
715                                          [ qb.qemu_instances for qb in self.qemu_boxes ],
716                                          [])
717                 all_qemu_instances.sort(timestamp_sort)
718                 qemu_instance_to_kill=all_qemu_instances[0]
719                 qemu_box=qemu_instance_to_kill.qemu_box
720                 qemu_instance_to_kill.kill()
721                 print 'killed oldest = %s on %s'%(qemu_instance_to_kill.line(),
722                                                  qemu_instance_to_kill.qemu_box.hostname)
723
724             utils.header( 'node %s -> qemu box %s'%(nodename,qemu_box.line()))
725             qemu_box.add_dummy(nodename)
726             #### OK we have a box to run in, let's find an IP address
727             # look in options
728             if options.ips_vnode:
729                 vnode_hostname=options.ips_vnode.pop()
730                 mac=self.vnode_pool.retrieve_userdata(vnode_hostname)
731             else:
732                 self.vnode_pool.sense()
733                 self.vnode_pool.load_starting()
734                 (vnode_hostname,mac)=self.vnode_pool.next_free()
735             ip=self.vnode_pool.get_ip (vnode_hostname)
736             self.vnode_pool.add_starting(vnode_hostname)
737
738             if vnode_hostname.find('.')<0:
739                 vnode_hostname += "."+self.domain()
740             nodemap={'host_box':qemu_box.hostname,
741                      'node_fields:hostname':vnode_hostname,
742                      'interface_fields:ip':ip, 
743                      'interface_fields:mac':mac,
744                      }
745             nodemap.update(self.network_settings())
746             maps.append ( (nodename, nodemap) )
747
748             utils.header("Attaching %s on IP %s MAC %s"%(plc['name'],vnode_hostname,mac))
749
750         return test_mapper.map({'node':maps})[0]
751
752     def localize_sfa_rspec (self,plc,options):
753        
754         plc['sfa']['SFA_REGISTRY_HOST'] = plc['PLC_DB_HOST']
755         plc['sfa']['SFA_AGGREGATE_HOST'] = plc['PLC_DB_HOST']
756         plc['sfa']['SFA_SM_HOST'] = plc['PLC_DB_HOST']
757         plc['sfa']['SFA_PLC_DB_HOST'] = plc['PLC_DB_HOST']
758         plc['sfa']['SFA_PLC_URL'] = 'https://' + plc['PLC_API_HOST'] + ':443/PLCAPI/' 
759         for site in plc['sites']:
760             for node in site['nodes']:
761                 plc['sfa']['sfa_slice_rspec']['part4'] = node['node_fields']['hostname']
762         return plc
763
764     #################### release:
765     def release (self,options):
766         self.vplc_pool.release_my_fakes()
767         self.vnode_pool.release_my_fakes()
768         pass
769
770     #################### show results for interactive mode
771     def list_all (self):
772         self.sense()
773         for b in self.all_boxes: b.list()
774
775     def get_box (self,box):
776         for b in self.build_boxes + self.plc_boxes + self.qemu_boxes:
777             if b.simple_hostname()==box:
778                 return b
779         print "Could not find box %s"%box
780         return None
781
782     def list_box(self,box):
783         b=self.get_box(box)
784         if not b: return
785         b.sense()
786         b.list()
787
788     # can be run as a utility to manage the local infrastructure
789     def main (self):
790         parser=OptionParser()
791         (options,args)=parser.parse_args()
792         if not args:
793             self.list_all()
794         else:
795             for box in args:
796                 self.list_box(box)