checkpoint
[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
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             print 'releasing-scanning','hostname',i.hostname,'status',i.status
184             if i.status=='mine': 
185                 self.del_starting(i.hostname)
186                 i.status=None
187
188     def del_starting (self, name):
189         try:    items=[line.strip() for line in file(Pool.starting).readlines()]
190         except: items=[]
191         if name in items:
192             f=file(Pool.starting,'w')
193             for item in items: 
194                 if item != name: f.write(item+'\n')
195             f.close()
196     
197     
198         
199 ####################
200 class Box:
201     def __init__ (self,hostname):
202         self.hostname=hostname
203     def simple_hostname (self):
204         return self.hostname.split('.')[0]
205     def test_ssh (self): return TestSsh(self.hostname,username='root',unknown_host=False)
206     def reboot (self):
207         self.test_ssh().run("shutdown -r now",message="Rebooting %s"%self.hostname)
208
209     def run(self,argv,message=None,trash_err=False,dry_run=False):
210         if dry_run:
211             print 'DRY_RUN:',
212             print " ".join(argv)
213             return 0
214         else:
215             header(message)
216             if not trash_err:
217                 return subprocess.call(argv)
218             else:
219                 return subprocess.call(argv,stderr=file('/dev/null','w'))
220                 
221     def run_ssh (self, argv, message, trash_err=False):
222         ssh_argv = self.test_ssh().actual_argv(argv)
223         result=self.run (ssh_argv, message, trash_err)
224         if result!=0:
225             print "WARNING: failed to run %s on %s"%(" ".join(argv),self.hostname)
226         return result
227
228     def backquote (self, argv, trash_err=False):
229         if not trash_err:
230             return subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
231         else:
232             return subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
233
234     def backquote_ssh (self, argv, trash_err=False):
235         # first probe the ssh link
236         probe_argv=self.test_ssh().actual_argv(['hostname'])
237         hostname=self.backquote ( probe_argv, trash_err=True )
238         if not hostname:
239             print "root@%s unreachable"%self.hostname
240             return ''
241         else:
242             return self.backquote( self.test_ssh().actual_argv(argv), trash_err)
243
244 ############################################################
245 class BuildInstance:
246     def __init__ (self, buildname, pid, buildbox):
247         self.buildname=buildname
248         self.buildbox=buildbox
249         self.pids=[pid]
250
251     def add_pid(self,pid):
252         self.pids.append(pid)
253
254     def line (self):
255         return "== %s == (pids=%r)"%(self.buildname,self.pids)
256
257 class BuildBox (Box):
258     def __init__ (self,hostname):
259         Box.__init__(self,hostname)
260         self.build_instances=[]
261
262     def add_build (self,buildname,pid):
263         for build in self.build_instances:
264             if build.buildname==buildname: 
265                 build.add_pid(pid)
266                 return
267         self.build_instances.append(BuildInstance(buildname, pid, self))
268
269     def list(self):
270         if not self.build_instances: 
271             header ('No build process on %s (%s)'%(self.hostname,self.uptime()))
272         else:
273             header ("Builds on %s (%s)"%(self.hostname,self.uptime()))
274             for b in self.build_instances: 
275                 header (b.line(),banner=False)
276
277     def uptime(self):
278         if hasattr(self,'_uptime') and self._uptime: return self._uptime
279         return '*undef* uptime'
280
281     # inspect box and find currently running builds
282     matcher=re.compile("\s*(?P<pid>[0-9]+).*-[bo]\s+(?P<buildname>[^\s]+)(\s|\Z)")
283     def sense(self,reboot=False,verbose=True):
284         if reboot:
285             self.reboot(box)
286             return
287         print 'b',
288         command=['uptime']
289         self._uptime=self.backquote_ssh(command,trash_err=True).strip()
290         if not self._uptime: self._uptime='unreachable'
291         pids=self.backquote_ssh(['pgrep','build'],trash_err=True)
292         if not pids: return
293         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
294         ps_lines=self.backquote_ssh (command).split('\n')
295         for line in ps_lines:
296             if not line.strip() or line.find('PID')>=0: continue
297             m=BuildBox.matcher.match(line)
298             if m: self.add_build (m.group('buildname'),m.group('pid'))
299             else: header('command %r returned line that failed to match'%command)
300
301 ############################################################
302 class PlcInstance:
303     def __init__ (self, vservername, ctxid, plcbox):
304         self.vservername=vservername
305         self.ctxid=ctxid
306         self.plc_box=plcbox
307         # unknown yet
308         self.timestamp=None
309
310     def set_timestamp (self,timestamp): self.timestamp=timestamp
311     def set_now (self): self.timestamp=int(time.time())
312     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
313
314     def line (self):
315         msg="== %s == (ctx=%s)"%(self.vservername,self.ctxid)
316         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
317         else:              msg += " *unknown timestamp*"
318         if self.ctxid==0: msg+=" not (yet?) running"
319         return msg
320
321     def kill (self):
322         msg="vserver stopping %s on %s"%(self.vservername,self.plc_box.hostname)
323         self.plc_box.run_ssh(['vserver',self.vservername,'stop'],msg)
324         self.plc_box.forget(self)
325
326 class PlcBox (Box):
327     def __init__ (self, hostname, max_plcs):
328         Box.__init__(self,hostname)
329         self.plc_instances=[]
330         self.max_plcs=max_plcs
331
332     def add_vserver (self,vservername,ctxid):
333         for plc in self.plc_instances:
334             if plc.vservername==vservername: 
335                 header("WARNING, duplicate myplc %s running on %s"%\
336                            (vservername,self.hostname),banner=False)
337                 return
338         self.plc_instances.append(PlcInstance(vservername,ctxid,self))
339     
340     def forget (self, plc_instance):
341         self.plc_instances.remove(plc_instance)
342
343     # fill one slot even though this one is not started yet
344     def add_dummy (self, plcname):
345         dummy=PlcInstance('dummy_'+plcname,0,self)
346         dummy.set_now()
347         self.plc_instances.append(dummy)
348
349     def line(self): 
350         msg="%s [max=%d,%d free] (%s)"%(self.hostname, self.max_plcs,self.free_spots(),self.uname())
351         return msg
352         
353     def list(self):
354         if not self.plc_instances: 
355             header ('No vserver running on %s'%(self.line()))
356         else:
357             header ("Active plc VMs on %s"%self.line())
358             for p in self.plc_instances: 
359                 header (p.line(),banner=False)
360
361     def free_spots (self):
362         return self.max_plcs - len(self.plc_instances)
363
364     def uname(self):
365         if hasattr(self,'_uname') and self._uname: return self._uname
366         return '*undef* uname'
367
368     def plc_instance_by_vservername (self, vservername):
369         for p in self.plc_instances:
370             if p.vservername==vservername: return p
371         return None
372
373     def sense (self, reboot=False, soft=False):
374         if reboot:
375             # remove mark for all running servers to avoid resurrection
376             stop_command=['rm','-rf','/etc/vservers/*/apps/init/mark']
377             self.run_ssh(stop_command,"Removing all vserver marks on %s"%self.hostname)
378             if not soft:
379                 self.reboot()
380                 return
381             else:
382                 self.run_ssh(['service','util-vserver','stop'],"Stopping all running vservers")
383             return
384         print 'p',
385         self._uname=self.backquote_ssh(['uname','-r']).strip()
386         # try to find fullname (vserver_stat truncates to a ridiculously short name)
387         # fetch the contexts for all vservers on that box
388         map_command=['grep','.','/etc/vservers/*/context','/dev/null',]
389         context_map=self.backquote_ssh (map_command)
390         # at this point we have a set of lines like
391         # /etc/vservers/2010.01.20--k27-f12-32-vplc03/context:40144
392         ctx_dict={}
393         for map_line in context_map.split("\n"):
394             if not map_line: continue
395             [path,xid] = map_line.split(':')
396             ctx_dict[xid]=os.path.basename(os.path.dirname(path))
397         # at this point ctx_id maps context id to vservername
398
399         command=['vserver-stat']
400         vserver_stat = self.backquote_ssh (command)
401         for vserver_line in vserver_stat.split("\n"):
402             if not vserver_line: continue
403             context=vserver_line.split()[0]
404             if context=="CTX": continue
405             longname=ctx_dict[context]
406             self.add_vserver(longname,context)
407 #            print self.margin_outline(self.vplcname(longname)),"%(vserver_line)s [=%(longname)s]"%locals()
408
409         # scan timestamps
410         command=   ['grep','.']
411         command += ['/vservers/%s/timestamp'%b for b in ctx_dict.values()]
412         command += ['/dev/null']
413         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
414         for ts_line in ts_lines:
415             if not ts_line.strip(): continue
416             # expect /vservers/<vservername>/timestamp:<timestamp>
417             try:
418                 (_,__,vservername,tail)=ts_line.split('/')
419                 (_,timestamp)=tail.split(':')
420                 timestamp=int(timestamp)
421                 q=self.plc_instance_by_vservername(vservername)
422                 if not q: 
423                     print 'WARNING unattached plc instance',ts_line
424                     continue
425                 q.set_timestamp(timestamp)
426             except:  print 'WARNING, could not parse ts line',ts_line
427         
428
429
430
431 ############################################################
432 class QemuInstance: 
433     def __init__ (self, nodename, pid, qemubox):
434         self.nodename=nodename
435         self.pid=pid
436         self.qemu_box=qemubox
437         # not known yet
438         self.buildname=None
439         self.timestamp=None
440         
441     def set_buildname (self,buildname): self.buildname=buildname
442     def set_timestamp (self,timestamp): self.timestamp=timestamp
443     def set_now (self): self.timestamp=int(time.time())
444     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
445     
446     def line (self):
447         msg = "== %s == (pid=%s)"%(self.nodename,self.pid)
448         if self.buildname: msg += " <--> %s"%self.buildname
449         else:              msg += " *unknown build*"
450         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
451         else:              msg += " *unknown timestamp*"
452         if self.pid:       msg += "pid=%s"%self.pid
453         else:              msg += " not (yet?) running"
454         return msg
455     
456     def kill(self):
457         if self.pid==0: print "cannot kill qemu %s with pid==0"%self.nodename
458         msg="Killing qemu %s with pid=%s on box %s"%(self.nodename,self.pid,self.qemu_box.hostname)
459         self.qemu_box.run_ssh(['kill',"%s"%self.pid],msg)
460         self.qemu_box.forget(self)
461
462
463 class QemuBox (Box):
464     def __init__ (self, hostname, max_qemus):
465         Box.__init__(self,hostname)
466         self.qemu_instances=[]
467         self.max_qemus=max_qemus
468
469     def add_node (self,nodename,pid):
470         for qemu in self.qemu_instances:
471             if qemu.nodename==nodename: 
472                 header("WARNING, duplicate qemu %s running on %s"%\
473                            (nodename,self.hostname), banner=False)
474                 return
475         self.qemu_instances.append(QemuInstance(nodename,pid,self))
476
477     def forget (self, qemu_instance):
478         self.qemu_instances.remove(qemu_instance)
479
480     # fill one slot even though this one is not started yet
481     def add_dummy (self, nodename):
482         dummy=QemuInstance('dummy_'+nodename,0,self)
483         dummy.set_now()
484         self.qemu_instances.append(dummy)
485
486     def line (self):
487         msg="%s [max=%d,%d free] (%s)"%(self.hostname, self.max_qemus,self.free_spots(),self.driver())
488         return msg
489
490     def list(self):
491         if not self.qemu_instances: 
492             header ('No qemu process on %s'%(self.line()))
493         else:
494             header ("Active qemu processes on %s"%(self.line()))
495             for q in self.qemu_instances: 
496                 header (q.line(),banner=False)
497
498     def free_spots (self):
499         return self.max_qemus - len(self.qemu_instances)
500
501     def driver(self):
502         if hasattr(self,'_driver') and self._driver: return self._driver
503         return '*undef* driver'
504
505     def qemu_instance_by_pid (self,pid):
506         for q in self.qemu_instances:
507             if q.pid==pid: return q
508         return None
509
510     def qemu_instance_by_nodename_buildname (self,nodename,buildname):
511         for q in self.qemu_instances:
512             if q.nodename==nodename and q.buildname==buildname:
513                 return q
514         return None
515
516     matcher=re.compile("\s*(?P<pid>[0-9]+).*-cdrom\s+(?P<nodename>[^\s]+)\.iso")
517     def sense(self, reboot=False, soft=False):
518         if reboot:
519             if not soft:
520                 self.reboot()
521             else:
522                 self.run_ssh(box,['pkill','qemu'],"Killing qemu instances")
523             return
524         print 'q',
525         modules=self.backquote_ssh(['lsmod']).split('\n')
526         self._driver='*NO kqemu/kmv_intel MODULE LOADED*'
527         for module in modules:
528             if module.find('kqemu')==0:
529                 self._driver='kqemu module loaded'
530             # kvm might be loaded without vkm_intel (we dont have AMD)
531             elif module.find('kvm_intel')==0:
532                 self._driver='kvm_intel module loaded'
533         ########## find out running pids
534         pids=self.backquote_ssh(['pgrep','qemu'])
535         if not pids: return
536         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
537         ps_lines = self.backquote_ssh (command).split("\n")
538         for line in ps_lines:
539             if not line.strip() or line.find('PID') >=0 : continue
540             m=QemuBox.matcher.match(line)
541             if m: self.add_node (m.group('nodename'),m.group('pid'))
542             else: header('command %r returned line that failed to match'%command)
543         ########## retrieve alive instances and map to build
544         live_builds=[]
545         command=['grep','.','*/*/qemu.pid','/dev/null']
546         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
547         for pid_line in pid_lines:
548             if not pid_line.strip(): continue
549             # expect <build>/<nodename>/qemu.pid:<pid>pid
550             try:
551                 (buildname,nodename,tail)=pid_line.split('/')
552                 (_,pid)=tail.split(':')
553                 q=self.qemu_instance_by_pid (pid)
554                 if not q: continue
555                 q.set_buildname(buildname)
556                 live_builds.append(buildname)
557             except: print 'WARNING, could not parse pid line',pid_line
558         # retrieve timestamps
559         command=   ['grep','.']
560         command += ['%s/*/timestamp'%b for b in live_builds]
561         command += ['/dev/null']
562         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
563         for ts_line in ts_lines:
564             if not ts_line.strip(): continue
565             # expect <build>/<nodename>/timestamp:<timestamp>
566             try:
567                 (buildname,nodename,tail)=ts_line.split('/')
568                 nodename=nodename.replace('qemu-','')
569                 (_,timestamp)=tail.split(':')
570                 timestamp=int(timestamp)
571                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
572                 if not q: 
573                     print 'WARNING unattached qemu instance',ts_line,nodename,buildname
574                     continue
575                 q.set_timestamp(timestamp)
576             except:  print 'WARNING, could not parse ts line',ts_line
577
578 ############################################################
579 class Options: pass
580
581 class Substrate:
582
583     def test (self): 
584         self.sense()
585
586     def __init__ (self):
587         self.options=Options()
588         self.options.dry_run=False
589         self.options.verbose=False
590         self.options.probe=True
591         self.options.soft=True
592         self.build_boxes = [ BuildBox(h) for h in self.build_boxes_spec() ]
593         self.plc_boxes = [ PlcBox (h,m) for (h,m) in self.plc_boxes_spec ()]
594         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
595         self.all_boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes
596         self._sensed=False
597
598         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs")
599         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes")
600
601         self.vnode_pool.list()
602
603
604 #    def build_box_names (self):
605 #        return [ h for h in self.build_boxes_spec() ]
606 #    def plc_boxes (self):
607 #        return [ h for (h,m) in self.plc_boxes_spec() ]
608 #    def qemu_boxes (self):
609 #        return [ h for (h,m) in self.qemu_boxes_spec() ]
610
611     def sense (self,force=False):
612         if self._sensed and not force: return
613         print 'Sensing local substrate...',
614         for b in self.all_boxes: b.sense()
615         print 'Done'
616         self._sensed=True
617
618     ########## 
619     def provision (self,plcs,options):
620         try:
621             self.sense()
622             self.list_all()
623             # attach each plc to a plc box and an IP address
624             plcs = [ self.provision_plc (plc,options) for plc in plcs ]
625             # attach each node/qemu to a qemu box with an IP address
626             plcs = [ self.provision_qemus (plc,options) for plc in plcs ]
627             # update the SFA spec accordingly
628             plcs = [ self.localize_sfa_rspec(plc,options) for plc in plcs ]
629             return plcs
630         except Exception, e:
631             print '* Could not provision this test on current substrate','--',e,'--','exiting'
632             traceback.print_exc()
633             sys.exit(1)
634
635     # find an available plc box (or make space)
636     # and a free IP address (using options if present)
637     def provision_plc (self, plc, options):
638         #### we need to find one plc box that still has a slot
639         plc_box=None
640         max_free=0
641         # use the box that has max free spots for load balancing
642         for pb in self.plc_boxes:
643             free=pb.free_spots()
644             if free>max_free:
645                 plc_box=pb
646                 max_free=free
647         # everything is already used
648         if not plc_box:
649             # find the oldest of all our instances
650             all_plc_instances=reduce(lambda x, y: x+y, 
651                                      [ pb.plc_instances for pb in self.plc_boxes ],
652                                      [])
653             all_plc_instances.sort(timestamp_sort)
654             plc_instance_to_kill=all_plc_instances[0]
655             plc_box=plc_instance_to_kill.plc_box
656             plc_instance_to_kill.kill()
657             print 'killed oldest = %s on %s'%(plc_instance_to_kill.line(),
658                                              plc_instance_to_kill.plc_box.hostname)
659
660         utils.header( 'plc %s -> box %s'%(plc['name'],plc_box.line()))
661         plc_box.add_dummy(plc['name'])
662         #### OK we have a box to run in, let's find an IP address
663         # look in options
664         if options.ips_vplc:
665             vplc_hostname=options.ips_vplc.pop()
666         else:
667             self.vplc_pool.load_starting()
668             self.vplc_pool.sense()
669             (vplc_hostname,unused)=self.vplc_pool.next_free()
670         vplc_ip = self.vplc_pool.get_ip(vplc_hostname)
671         self.vplc_pool.add_starting(vplc_hostname)
672
673         #### compute a helpful vserver name
674         # remove domain in hostname
675         vplc_simple = vplc_hostname.split('.')[0]
676         vservername = "%s-%d-%s" % (options.buildname,plc['index'],vplc_simple)
677         plc_name = "%s_%s"%(plc['name'],vplc_simple)
678
679         #### apply in the plc_spec
680         # # informative
681         # label=options.personality.replace("linux","")
682         mapper = {'plc': [ ('*' , {'hostname':plc_box.hostname,
683                                    # 'name':'%s-'+label,
684                                    'name': plc_name,
685                                    'vservername':vservername,
686                                    'vserverip':vplc_ip,
687                                    'PLC_DB_HOST':vplc_hostname,
688                                    'PLC_API_HOST':vplc_hostname,
689                                    'PLC_BOOT_HOST':vplc_hostname,
690                                    'PLC_WWW_HOST':vplc_hostname,
691                                    'PLC_NET_DNS1' : self.network_settings() [ 'interface_fields:dns1' ],
692                                    'PLC_NET_DNS2' : self.network_settings() [ 'interface_fields:dns2' ],
693                                    } ) ]
694                   }
695
696         utils.header("Attaching %s on IP %s in vserver %s"%(plc['name'],vplc_hostname,vservername))
697         # mappers only work on a list of plcs
698         return TestMapper([plc],options).map(mapper)[0]
699
700     ##########
701     def provision_qemus (self, plc, options):
702         test_mapper = TestMapper ([plc], options)
703         nodenames = test_mapper.node_names()
704         maps=[]
705         for nodename in nodenames:
706             #### similarly we want to find a qemu box that can host us
707             qemu_box=None
708             max_free=0
709             # use the box that has max free spots for load balancing
710             for qb in self.qemu_boxes:
711                 free=qb.free_spots()
712             if free>max_free:
713                 qemu_box=qb
714                 max_free=free
715             # everything is already used
716             if not qemu_box:
717                 # find the oldest of all our instances
718                 all_qemu_instances=reduce(lambda x, y: x+y, 
719                                          [ qb.qemu_instances for qb in self.qemu_boxes ],
720                                          [])
721                 all_qemu_instances.sort(timestamp_sort)
722                 qemu_instance_to_kill=all_qemu_instances[0]
723                 qemu_box=qemu_instance_to_kill.qemu_box
724                 qemu_instance_to_kill.kill()
725                 print 'killed oldest = %s on %s'%(qemu_instance_to_kill.line(),
726                                                  qemu_instance_to_kill.qemu_box.hostname)
727
728             utils.header( 'node %s -> qemu box %s'%(nodename,qemu_box.line()))
729             qemu_box.add_dummy(nodename)
730             #### OK we have a box to run in, let's find an IP address
731             # look in options
732             if options.ips_vnode:
733                 vnode_hostname=options.ips_vnode.pop()
734                 mac=self.vnode_pool.retrieve_userdata(vnode_hostname)
735                 print 'case 1 hostname',vnode_hostname,'mac',mac
736             else:
737                 self.vnode_pool.load_starting()
738                 self.vnode_pool.sense()
739                 (vnode_hostname,mac)=self.vnode_pool.next_free()
740                 print 'case 2 hostname',vnode_hostname,'mac',mac
741             ip=self.vnode_pool.get_ip (vnode_hostname)
742             self.vnode_pool.add_starting(vnode_hostname)
743
744             if vnode_hostname.find('.')<0:
745                 vnode_hostname += "."+self.domain()
746             nodemap={'host_box':qemu_box.hostname,
747                      'node_fields:hostname':vnode_hostname,
748                      'interface_fields:ip':ip, 
749                      'interface_fields:mac':mac,
750                      }
751             nodemap.update(self.network_settings())
752             maps.append ( (nodename, nodemap) )
753
754             utils.header("Attaching %s on IP %s MAC %s"%(plc['name'],vnode_hostname,mac))
755
756         return test_mapper.map({'node':maps})[0]
757
758     def localize_sfa_rspec (self,plc,options):
759        
760         plc['sfa']['SFA_REGISTRY_HOST'] = plc['PLC_DB_HOST']
761         plc['sfa']['SFA_AGGREGATE_HOST'] = plc['PLC_DB_HOST']
762         plc['sfa']['SFA_SM_HOST'] = plc['PLC_DB_HOST']
763         plc['sfa']['SFA_PLC_DB_HOST'] = plc['PLC_DB_HOST']
764         plc['sfa']['SFA_PLC_URL'] = 'https://' + plc['PLC_API_HOST'] + ':443/PLCAPI/' 
765         for site in plc['sites']:
766             for node in site['nodes']:
767                 plc['sfa']['sfa_slice_rspec']['part4'] = node['node_fields']['hostname']
768         return plc
769
770     #################### release:
771     def release (self,options):
772         self.vplc_pool.release_my_fakes()
773         self.vnode_pool.release_my_fakes()
774         pass
775
776     #################### show results for interactive mode
777     def list_all (self):
778         self.sense()
779         for b in self.all_boxes: b.list()
780
781     def get_box (self,box):
782         for b in self.build_boxes + self.plc_boxes + self.qemu_boxes:
783             if b.simple_hostname()==box:
784                 return b
785         print "Could not find box %s"%box
786         return None
787
788     def list_box(self,box):
789         b=self.get_box(box)
790         if not b: return
791         b.sense()
792         b.list()
793
794     # can be run as a utility to manage the local infrastructure
795     def main (self):
796         parser=OptionParser()
797         (options,args)=parser.parse_args()
798         if not args:
799             self.list_all()
800         else:
801             for box in args:
802                 self.list_box(box)