ignore ps entries for builds.sh
[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): return o1.timestamp-o2.timestamp
67
68 ####################
69 # pool class
70 # allows to pick an available IP among a pool
71 # input is expressed as a list of tuples (hostname,ip,user_data)
72 # that can be searched iteratively for a free slot
73 # e.g.
74 # pool = [ (hostname1,user_data1),  
75 #          (hostname2,user_data2),  
76 #          (hostname3,user_data2),  
77 #          (hostname4,user_data4) ]
78 # assuming that ip1 and ip3 are taken (pingable), then we'd get
79 # pool=Pool(pool)
80 # pool.next_free() -> entry2
81 # pool.next_free() -> entry4
82 # pool.next_free() -> None
83 # that is, even if ip2 is not busy/pingable when the second next_free() is issued
84
85 class PoolItem:
86     def __init__ (self,hostname,userdata):
87         self.hostname=hostname
88         self.userdata=userdata
89         # slot holds 'busy' or 'free' or 'mine' or 'starting' or None
90         # 'mine' is for our own stuff, 'starting' from the concurrent tests
91         self.status=None
92         self.ip=None
93
94     def line(self):
95         return "Pooled %s (%s) -> %s"%(self.hostname,self.userdata, self.status)
96
97     def char (self):
98         if   self.status==None:       return '?'
99         elif self.status=='busy':     return '*'
100         elif self.status=='free':     return '.'
101         elif self.status=='mine':     return 'M'
102         elif self.status=='starting': return 'S'
103
104     def get_ip(self):
105         if self.ip: return self.ip
106         ip=socket.gethostbyname(self.hostname)
107         self.ip=ip
108         return ip
109
110 class Pool:
111
112     def __init__ (self, tuples,message):
113         self.pool= [ PoolItem (h,u) for (h,u) in tuples ] 
114         self.message=message
115
116     def list (self):
117         for i in self.pool: print i.line()
118
119     def line (self):
120         line=self.message
121         for i in self.pool: line += ' ' + i.char()
122         return line
123
124     def _item (self, hostname):
125         for i in self.pool: 
126             if i.hostname==hostname: return i
127         raise Exception ("Could not locate hostname %s in pool %s"%(hostname,self.message))
128
129     def retrieve_userdata (self, hostname): 
130         return self._item(hostname).userdata
131
132     def get_ip (self, hostname):
133         try:    return self._item(hostname).get_ip()
134         except: return socket.gethostbyname(hostname)
135         
136     def set_mine (self, hostname):
137         try:
138             self._item(hostname).status='mine'
139         except:
140             print 'WARNING: host %s not found in IP pool %s'%(hostname,self.message)
141
142     def next_free (self):
143         for i in self.pool:
144             if i.status == 'free':
145                 i.status='mine'
146                 return (i.hostname,i.userdata)
147         return None
148
149     # the place were other test instances tell about their not-yet-started
150     # instances, that go undetected through sensing
151     starting='/root/starting'
152     def add_starting (self, name):
153         try:    items=[line.strip() for line in file(Pool.starting).readlines()]
154         except: items=[]
155         if not name in items:
156             file(Pool.starting,'a').write(name+'\n')
157         for i in self.pool:
158             if i.hostname==name: i.status='mine'
159             
160     # we load this after actual sensing; 
161     def load_starting (self):
162         try:    items=[line.strip() for line in file(Pool.starting).readlines()]
163         except: items=[]
164         for i in self.pool:
165             if i.hostname in items:
166                 if i.status=='free' : i.status='starting'
167
168     def release_my_starting (self):
169         for i in self.pool:
170             if i.status=='mine': 
171                 self.del_starting(i.hostname)
172                 i.status=None
173
174     def del_starting (self, name):
175         try:    items=[line.strip() for line in file(Pool.starting).readlines()]
176         except: items=[]
177         if name in items:
178             f=file(Pool.starting,'w')
179             for item in items: 
180                 if item != name: f.write(item+'\n')
181             f.close()
182     
183     ##########
184     def _sense (self):
185         for item in self.pool:
186             if item.status is not None: 
187                 continue
188             if self.check_ping (item.hostname): 
189                 item.status='busy'
190             else:
191                 item.status='free'
192     
193     def sense (self):
194         print 'Sensing IP pool',self.message,
195         self._sense()
196         print 'Done'
197         self.load_starting()
198         print 'After starting: IP pool'
199         print self.line()
200
201     # OS-dependent ping option (support for macos, for convenience)
202     ping_timeout_option = None
203     # returns True when a given hostname/ip responds to ping
204     def check_ping (self,hostname):
205         if not Pool.ping_timeout_option:
206             (status,osname) = commands.getstatusoutput("uname -s")
207             if status != 0:
208                 raise Exception, "TestPool: Cannot figure your OS name"
209             if osname == "Linux":
210                 Pool.ping_timeout_option="-w"
211             elif osname == "Darwin":
212                 Pool.ping_timeout_option="-t"
213
214         command="ping -c 1 %s 1 %s"%(Pool.ping_timeout_option,hostname)
215         (status,output) = commands.getstatusoutput(command)
216         if status==0:   print '*',
217         else:           print '.',
218         return status == 0
219
220 ####################
221 class Box:
222     def __init__ (self,hostname):
223         self.hostname=hostname
224     def short_hostname (self):
225         return self.hostname.split('.')[0]
226     def test_ssh (self): return TestSsh(self.hostname,username='root',unknown_host=False)
227     def reboot (self):
228         self.test_ssh().run("shutdown -r now",message="Rebooting %s"%self.hostname)
229
230     def run(self,argv,message=None,trash_err=False,dry_run=False):
231         if dry_run:
232             print 'DRY_RUN:',
233             print " ".join(argv)
234             return 0
235         else:
236             header(message)
237             if not trash_err:
238                 return subprocess.call(argv)
239             else:
240                 return subprocess.call(argv,stderr=file('/dev/null','w'))
241                 
242     def run_ssh (self, argv, message, trash_err=False):
243         ssh_argv = self.test_ssh().actual_argv(argv)
244         result=self.run (ssh_argv, message, trash_err)
245         if result!=0:
246             print "WARNING: failed to run %s on %s"%(" ".join(argv),self.hostname)
247         return result
248
249     def backquote (self, argv, trash_err=False):
250         if not trash_err:
251             result= subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
252         else:
253             result= subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
254         return result
255
256     def backquote_ssh (self, argv, trash_err=False):
257         # first probe the ssh link
258         probe_argv=self.test_ssh().actual_argv(['hostname'])
259         hostname=self.backquote ( probe_argv, trash_err=True )
260         if not hostname:
261             print "root@%s unreachable"%self.hostname
262             return ''
263         else:
264             return self.backquote( self.test_ssh().actual_argv(argv), trash_err)
265
266 ############################################################
267 class BuildInstance:
268     def __init__ (self, buildname, pid, buildbox):
269         self.buildname=buildname
270         self.buildbox=buildbox
271         self.pids=[pid]
272
273     def add_pid(self,pid):
274         self.pids.append(pid)
275
276     def line (self):
277         return "== %s == (pids=%r)"%(self.buildname,self.pids)
278
279 class BuildBox (Box):
280     def __init__ (self,hostname):
281         Box.__init__(self,hostname)
282         self.build_instances=[]
283
284     def add_build (self,buildname,pid):
285         for build in self.build_instances:
286             if build.buildname==buildname: 
287                 build.add_pid(pid)
288                 return
289         self.build_instances.append(BuildInstance(buildname, pid, self))
290
291     def list(self):
292         if not self.build_instances: 
293             header ('No build process on %s (%s)'%(self.hostname,self.uptime()))
294         else:
295             header ("Builds on %s (%s)"%(self.hostname,self.uptime()))
296             for b in self.build_instances: 
297                 header (b.line(),banner=False)
298
299     def uptime(self):
300         if hasattr(self,'_uptime') and self._uptime: return self._uptime
301         return '*undef* uptime'
302
303     # inspect box and find currently running builds
304     matcher_exclude=re.compile(".*builds\.sh.*")
305     matcher=re.compile("\s*(?P<pid>[0-9]+).*-[bo]\s+(?P<buildname>[^\s]+)(\s|\Z)")
306     def sense(self,reboot=False,verbose=True):
307         if reboot:
308             self.reboot(box)
309             return
310         print 'b',
311         command=['uptime']
312         self._uptime=self.backquote_ssh(command,trash_err=True).strip()
313         if not self._uptime: self._uptime='unreachable'
314         pids=self.backquote_ssh(['pgrep','build'],trash_err=True)
315         if not pids: return
316         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
317         ps_lines=self.backquote_ssh (command).split('\n')
318         for line in ps_lines:
319             if not line.strip() or line.find('PID')>=0: continue
320             if BuildBox.matcher_exclude.match(line): continue
321             m=BuildBox.matcher.match(line)
322             if m: 
323                 date=time.strftime('%Y-%m-%d',time.localtime(time.time()))
324                 buildname=m.group('buildname').replace('@DATE@',date)
325                 self.add_build (buildname,m.group('pid'))
326             else: header('command %r returned line that failed to match'%command)
327
328 ############################################################
329 class PlcInstance:
330     def __init__ (self, vservername, ctxid, plcbox):
331         self.vservername=vservername
332         self.ctxid=ctxid
333         self.plc_box=plcbox
334         # unknown yet
335         self.timestamp=0
336
337     def set_timestamp (self,timestamp): self.timestamp=timestamp
338     def set_now (self): self.timestamp=int(time.time())
339     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
340
341     def vplcname (self):
342         return self.vservername.split('-')[-1]
343
344     def line (self):
345         msg="== %s == (ctx=%s)"%(self.vservername,self.ctxid)
346         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
347         else:              msg += " *unknown timestamp*"
348         if self.ctxid==0:  msg+=" not (yet?) running"
349         return msg
350
351     def kill (self):
352         msg="vserver stopping %s on %s"%(self.vservername,self.plc_box.hostname)
353         self.plc_box.run_ssh(['vserver',self.vservername,'stop'],msg)
354         self.plc_box.forget(self)
355
356 class PlcBox (Box):
357     def __init__ (self, hostname, max_plcs):
358         Box.__init__(self,hostname)
359         self.plc_instances=[]
360         self.max_plcs=max_plcs
361
362     def add_vserver (self,vservername,ctxid):
363         for plc in self.plc_instances:
364             if plc.vservername==vservername: 
365                 header("WARNING, duplicate myplc %s running on %s"%\
366                            (vservername,self.hostname),banner=False)
367                 return
368         self.plc_instances.append(PlcInstance(vservername,ctxid,self))
369     
370     def forget (self, plc_instance):
371         self.plc_instances.remove(plc_instance)
372
373     # fill one slot even though this one is not started yet
374     def add_dummy (self, plcname):
375         dummy=PlcInstance('dummy_'+plcname,0,self)
376         dummy.set_now()
377         self.plc_instances.append(dummy)
378
379     def line(self): 
380         msg="%s [max=%d,%d free] (%s)"%(self.hostname, self.max_plcs,self.free_spots(),self.uname())
381         return msg
382         
383     def list(self):
384         if not self.plc_instances: 
385             header ('No vserver running on %s'%(self.line()))
386         else:
387             header ("Active plc VMs on %s"%self.line())
388             for p in self.plc_instances: 
389                 header (p.line(),banner=False)
390
391     def free_spots (self):
392         return self.max_plcs - len(self.plc_instances)
393
394     def uname(self):
395         if hasattr(self,'_uname') and self._uname: return self._uname
396         return '*undef* uname'
397
398     def plc_instance_by_vservername (self, vservername):
399         for p in self.plc_instances:
400             if p.vservername==vservername: return p
401         return None
402
403     def sense (self, reboot=False, soft=False):
404         if reboot:
405             # remove mark for all running servers to avoid resurrection
406             stop_command=['rm','-rf','/etc/vservers/*/apps/init/mark']
407             self.run_ssh(stop_command,"Removing all vserver marks on %s"%self.hostname)
408             if not soft:
409                 self.reboot()
410                 return
411             else:
412                 self.run_ssh(['service','util-vserver','stop'],"Stopping all running vservers")
413             return
414         print 'p',
415         self._uname=self.backquote_ssh(['uname','-r']).strip()
416         # try to find fullname (vserver_stat truncates to a ridiculously short name)
417         # fetch the contexts for all vservers on that box
418         map_command=['grep','.','/etc/vservers/*/context','/dev/null',]
419         context_map=self.backquote_ssh (map_command)
420         # at this point we have a set of lines like
421         # /etc/vservers/2010.01.20--k27-f12-32-vplc03/context:40144
422         ctx_dict={}
423         for map_line in context_map.split("\n"):
424             if not map_line: continue
425             [path,xid] = map_line.split(':')
426             ctx_dict[xid]=os.path.basename(os.path.dirname(path))
427         # at this point ctx_id maps context id to vservername
428
429         command=['vserver-stat']
430         vserver_stat = self.backquote_ssh (command)
431         for vserver_line in vserver_stat.split("\n"):
432             if not vserver_line: continue
433             context=vserver_line.split()[0]
434             if context=="CTX": continue
435             longname=ctx_dict[context]
436             self.add_vserver(longname,context)
437 #            print self.margin_outline(self.vplcname(longname)),"%(vserver_line)s [=%(longname)s]"%locals()
438
439         # scan timestamps 
440         running_vsnames = [ i.vservername for i in self.plc_instances ]
441         command=   ['grep','.']
442         command += ['/vservers/%s/timestamp'%vs for vs in running_vsnames]
443         command += ['/dev/null']
444         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
445         for ts_line in ts_lines:
446             if not ts_line.strip(): continue
447             # expect /vservers/<vservername>/timestamp:<timestamp>
448             try:
449                 (_,__,vservername,tail)=ts_line.split('/')
450                 (_,timestamp)=tail.split(':')
451                 timestamp=int(timestamp)
452                 p=self.plc_instance_by_vservername(vservername)
453                 if not p: 
454                     print 'WARNING unattached plc instance',ts_line
455                     print 'was expecting to find',vservername,'in',[i.vservername for i in self.plc_instances]
456                     continue
457                 p.set_timestamp(timestamp)
458             except:  print 'WARNING, could not parse ts line',ts_line
459         
460
461
462
463 ############################################################
464 class QemuInstance: 
465     def __init__ (self, nodename, pid, qemubox):
466         self.nodename=nodename
467         self.pid=pid
468         self.qemu_box=qemubox
469         # not known yet
470         self.buildname=None
471         self.timestamp=0
472         
473     def set_buildname (self,buildname): self.buildname=buildname
474     def set_timestamp (self,timestamp): self.timestamp=timestamp
475     def set_now (self): self.timestamp=int(time.time())
476     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
477     
478     def line (self):
479         msg = "== %s == (pid=%s)"%(self.nodename,self.pid)
480         if self.buildname: msg += " <--> %s"%self.buildname
481         else:              msg += " *unknown build*"
482         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
483         else:              msg += " *unknown timestamp*"
484         if self.pid:       msg += " pid=%s"%self.pid
485         else:              msg += " not (yet?) running"
486         return msg
487     
488     def kill(self):
489         if self.pid==0: 
490             print "cannot kill qemu %s with pid==0"%self.nodename
491             return
492         msg="Killing qemu %s with pid=%s on box %s"%(self.nodename,self.pid,self.qemu_box.hostname)
493         self.qemu_box.run_ssh(['kill',"%s"%self.pid],msg)
494         self.qemu_box.forget(self)
495
496
497 class QemuBox (Box):
498     def __init__ (self, hostname, max_qemus):
499         Box.__init__(self,hostname)
500         self.qemu_instances=[]
501         self.max_qemus=max_qemus
502
503     def add_node (self,nodename,pid):
504         for qemu in self.qemu_instances:
505             if qemu.nodename==nodename: 
506                 header("WARNING, duplicate qemu %s running on %s"%\
507                            (nodename,self.hostname), banner=False)
508                 return
509         self.qemu_instances.append(QemuInstance(nodename,pid,self))
510
511     def forget (self, qemu_instance):
512         self.qemu_instances.remove(qemu_instance)
513
514     # fill one slot even though this one is not started yet
515     def add_dummy (self, nodename):
516         dummy=QemuInstance('dummy_'+nodename,0,self)
517         dummy.set_now()
518         self.qemu_instances.append(dummy)
519
520     def line (self):
521         msg="%s [max=%d,%d free] (%s)"%(self.hostname, self.max_qemus,self.free_spots(),self.driver())
522         return msg
523
524     def list(self):
525         if not self.qemu_instances: 
526             header ('No qemu process on %s'%(self.line()))
527         else:
528             header ("Active qemu processes on %s"%(self.line()))
529             for q in self.qemu_instances: 
530                 header (q.line(),banner=False)
531
532     def free_spots (self):
533         return self.max_qemus - len(self.qemu_instances)
534
535     def driver(self):
536         if hasattr(self,'_driver') and self._driver: return self._driver
537         return '*undef* driver'
538
539     def qemu_instance_by_pid (self,pid):
540         for q in self.qemu_instances:
541             if q.pid==pid: return q
542         return None
543
544     def qemu_instance_by_nodename_buildname (self,nodename,buildname):
545         for q in self.qemu_instances:
546             if q.nodename==nodename and q.buildname==buildname:
547                 return q
548         return None
549
550     matcher=re.compile("\s*(?P<pid>[0-9]+).*-cdrom\s+(?P<nodename>[^\s]+)\.iso")
551     def sense(self, reboot=False, soft=False):
552         if reboot:
553             if not soft:
554                 self.reboot()
555             else:
556                 self.run_ssh(box,['pkill','qemu'],"Killing qemu instances")
557             return
558         print 'q',
559         modules=self.backquote_ssh(['lsmod']).split('\n')
560         self._driver='*NO kqemu/kmv_intel MODULE LOADED*'
561         for module in modules:
562             if module.find('kqemu')==0:
563                 self._driver='kqemu module loaded'
564             # kvm might be loaded without vkm_intel (we dont have AMD)
565             elif module.find('kvm_intel')==0:
566                 self._driver='kvm_intel module loaded'
567         ########## find out running pids
568         pids=self.backquote_ssh(['pgrep','qemu'])
569         if not pids: return
570         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
571         ps_lines = self.backquote_ssh (command).split("\n")
572         for line in ps_lines:
573             if not line.strip() or line.find('PID') >=0 : continue
574             m=QemuBox.matcher.match(line)
575             if m: self.add_node (m.group('nodename'),m.group('pid'))
576             else: header('command %r returned line that failed to match'%command)
577         ########## retrieve alive instances and map to build
578         live_builds=[]
579         command=['grep','.','*/*/qemu.pid','/dev/null']
580         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
581         for pid_line in pid_lines:
582             if not pid_line.strip(): continue
583             # expect <build>/<nodename>/qemu.pid:<pid>pid
584             try:
585                 (buildname,nodename,tail)=pid_line.split('/')
586                 (_,pid)=tail.split(':')
587                 q=self.qemu_instance_by_pid (pid)
588                 if not q: continue
589                 q.set_buildname(buildname)
590                 live_builds.append(buildname)
591             except: print 'WARNING, could not parse pid line',pid_line
592         # retrieve timestamps
593         command=   ['grep','.']
594         command += ['%s/*/timestamp'%b for b in live_builds]
595         command += ['/dev/null']
596         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
597         for ts_line in ts_lines:
598             if not ts_line.strip(): continue
599             # expect <build>/<nodename>/timestamp:<timestamp>
600             try:
601                 (buildname,nodename,tail)=ts_line.split('/')
602                 nodename=nodename.replace('qemu-','')
603                 (_,timestamp)=tail.split(':')
604                 timestamp=int(timestamp)
605                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
606                 if not q: 
607                     print 'WARNING unattached qemu instance',ts_line,nodename,buildname
608                     continue
609                 q.set_timestamp(timestamp)
610             except:  print 'WARNING, could not parse ts line',ts_line
611
612 ############################################################
613 class Options: pass
614
615 class Substrate:
616
617     def __init__ (self):
618         self.options=Options()
619         self.options.dry_run=False
620         self.options.verbose=False
621         self.options.probe=True
622         self.options.soft=True
623         self.build_boxes = [ BuildBox(h) for h in self.build_boxes_spec() ]
624         self.plc_boxes = [ PlcBox (h,m) for (h,m) in self.plc_boxes_spec ()]
625         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
626         self.all_boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes
627         self._sensed=False
628
629         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs")
630         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes")
631
632     def fqdn (self, hostname):
633         if hostname.find('.')<0: return "%s.%s"%(hostname,self.domain())
634     def short_hostname (self, hostname):
635         if hostname.find('.')>=0: return hostname.split('.')[0]
636
637     # return True if actual sensing takes place
638     def sense (self,force=False):
639         if self._sensed and not force: return False
640         print 'Sensing local substrate...',
641         for b in self.all_boxes: b.sense()
642         print 'Done'
643         self._sensed=True
644         return True
645
646     def add_dummy_plc (self, plc_boxname, plcname):
647         for pb in self.plc_boxes:
648             if pb.hostname==plc_boxname:
649                 pb.add_dummy(plcname)
650     def add_dummy_qemu (self, qemu_boxname, qemuname):
651         for qb in self.qemu_boxes:
652             if qb.hostname==qemu_boxname:
653                 qb.add_dummy(qemuname)
654
655     ########## 
656     def provision (self,plcs,options):
657         try:
658             # attach each plc to a plc box and an IP address
659             plcs = [ self.provision_plc (plc,options) for plc in plcs ]
660             # attach each node/qemu to a qemu box with an IP address
661             plcs = [ self.provision_qemus (plc,options) for plc in plcs ]
662             # update the SFA spec accordingly
663             plcs = [ self.localize_sfa_rspec(plc,options) for plc in plcs ]
664             return plcs
665         except Exception, e:
666             print '* Could not provision this test on current substrate','--',e,'--','exiting'
667             traceback.print_exc()
668             sys.exit(1)
669
670     # it is expected that a couple of options like ips_bplc and ips_vplc 
671     # are set or unset together
672     @staticmethod
673     def check_options (x,y):
674         if not x and not y: return True
675         return len(x)==len(y)
676
677     # find an available plc box (or make space)
678     # and a free IP address (using options if present)
679     def provision_plc (self, plc, options):
680         
681         assert Substrate.check_options (options.ips_bplc, options.ips_vplc)
682
683         #### let's find an IP address for that plc
684         # look in options 
685         if options.ips_vplc:
686             # this is a rerun
687             # we don't check anything here, 
688             # it is the caller's responsability to cleanup and make sure this makes sense
689             plc_boxname = options.ips_bplc.pop()
690             vplc_hostname=options.ips_vplc.pop()
691         else:
692             if self.sense(): self.list_all()
693             plc_boxname=None
694             vplc_hostname=None
695             # try to find an available IP 
696             self.vplc_pool.sense()
697             couple=self.vplc_pool.next_free()
698             if couple:
699                 (vplc_hostname,unused)=couple
700             #### we need to find one plc box that still has a slot
701             max_free=0
702             # use the box that has max free spots for load balancing
703             for pb in self.plc_boxes:
704                 free=pb.free_spots()
705                 if free>max_free:
706                     plc_boxname=pb.hostname
707                     max_free=free
708             # if there's no available slot in the plc_boxes, or we need a free IP address
709             # make space by killing the oldest running instance
710             if not plc_boxname or not vplc_hostname:
711                 # find the oldest of all our instances
712                 all_plc_instances=reduce(lambda x, y: x+y, 
713                                          [ pb.plc_instances for pb in self.plc_boxes ],
714                                          [])
715                 all_plc_instances.sort(timestamp_sort)
716                 try:
717                     plc_instance_to_kill=all_plc_instances[0]
718                 except:
719                     msg=""
720                     if not plc_boxname: msg += " PLC boxes are full"
721                     if not vplc_hostname: msg += " vplc IP pool exhausted" 
722                     raise Exception,"Could not make space for a PLC instance:"+msg
723                 freed_plc_boxname=plc_instance_to_kill.plc_box.hostname
724                 freed_vplc_hostname=plc_instance_to_kill.vplcname()
725                 message='killing oldest plc instance = %s on %s'%(plc_instance_to_kill.line(),
726                                                                   freed_plc_boxname)
727                 plc_instance_to_kill.kill()
728                 # use this new plcbox if that was the problem
729                 if not plc_boxname:
730                     plc_boxname=freed_plc_boxname
731                 # ditto for the IP address
732                 if not vplc_hostname:
733                     vplc_hostname=freed_vplc_hostname
734                     # record in pool as mine
735                     self.vplc_pool.set_mine(vplc_hostname)
736
737         # 
738         self.add_dummy_plc(plc_boxname,plc['name'])
739         vplc_ip = self.vplc_pool.get_ip(vplc_hostname)
740         self.vplc_pool.add_starting(vplc_hostname)
741
742         #### compute a helpful vserver name
743         # remove domain in hostname
744         vplc_short = self.short_hostname(vplc_hostname)
745         vservername = "%s-%d-%s" % (options.buildname,plc['index'],vplc_short)
746         plc_name = "%s_%s"%(plc['name'],vplc_short)
747
748         utils.header( 'PROVISION plc %s in box %s at IP %s as %s'%\
749                           (plc['name'],plc_boxname,vplc_hostname,vservername))
750
751         #### apply in the plc_spec
752         # # informative
753         # label=options.personality.replace("linux","")
754         mapper = {'plc': [ ('*' , {'host_box':plc_boxname,
755                                    # 'name':'%s-'+label,
756                                    'name': plc_name,
757                                    'vservername':vservername,
758                                    'vserverip':vplc_ip,
759                                    'PLC_DB_HOST':vplc_hostname,
760                                    'PLC_API_HOST':vplc_hostname,
761                                    'PLC_BOOT_HOST':vplc_hostname,
762                                    'PLC_WWW_HOST':vplc_hostname,
763                                    'PLC_NET_DNS1' : self.network_settings() [ 'interface_fields:dns1' ],
764                                    'PLC_NET_DNS2' : self.network_settings() [ 'interface_fields:dns2' ],
765                                    } ) ]
766                   }
767
768
769         # mappers only work on a list of plcs
770         return TestMapper([plc],options).map(mapper)[0]
771
772     ##########
773     def provision_qemus (self, plc, options):
774
775         assert Substrate.check_options (options.ips_bnode, options.ips_vnode)
776
777         test_mapper = TestMapper ([plc], options)
778         nodenames = test_mapper.node_names()
779         maps=[]
780         for nodename in nodenames:
781
782             if options.ips_vnode:
783                 # as above, it's a rerun, take it for granted
784                 qemu_boxname=options.ips_bnode.pop()
785                 vnode_hostname=options.ips_vnode.pop()
786             else:
787                 if self.sense(): self.list_all()
788                 qemu_boxname=None
789                 vnode_hostname=None
790                 # try to find an available IP 
791                 self.vnode_pool.sense()
792                 couple=self.vnode_pool.next_free()
793                 if couple:
794                     (vnode_hostname,unused)=couple
795                 # find a physical box
796                 max_free=0
797                 # use the box that has max free spots for load balancing
798                 for qb in self.qemu_boxes:
799                     free=qb.free_spots()
800                     if free>max_free:
801                         qemu_boxname=qb.hostname
802                         max_free=free
803                 # if we miss the box or the IP, kill the oldest instance
804                 if not qemu_boxname or not vnode_hostname:
805                 # find the oldest of all our instances
806                     all_qemu_instances=reduce(lambda x, y: x+y, 
807                                               [ qb.qemu_instances for qb in self.qemu_boxes ],
808                                               [])
809                     all_qemu_instances.sort(timestamp_sort)
810                     try:
811                         qemu_instance_to_kill=all_qemu_instances[0]
812                     except:
813                         msg=""
814                         if not qemu_boxname: msg += " QEMU boxes are full"
815                         if not vnode_hostname: msg += " vnode IP pool exhausted" 
816                         raise Exception,"Could not make space for a QEMU instance:"+msg
817                     freed_qemu_boxname=qemu_instance_to_kill.qemu_box.hostname
818                     freed_vnode_hostname=self.short_hostname(qemu_instance_to_kill.nodename)
819                     # kill it
820                     message='killing oldest qemu node = %s on %s'%(qemu_instance_to_kill.line(),
821                                                                    freed_qemu_boxname)
822                     qemu_instance_to_kill.kill()
823                     # use these freed resources where needed
824                     if not qemu_boxname:
825                         qemu_boxname=freed_qemu_boxname
826                     if not vnode_hostname:
827                         vnode_hostname=freed_vnode_hostname
828                         self.vnode_pool.set_mine(vnode_hostname)
829
830             self.add_dummy_qemu (qemu_boxname,nodename)
831             mac=self.vnode_pool.retrieve_userdata(vnode_hostname)
832             ip=self.vnode_pool.get_ip (vnode_hostname)
833             self.vnode_pool.add_starting(vnode_hostname)
834
835             vnode_fqdn = self.fqdn(vnode_hostname)
836             nodemap={'host_box':qemu_boxname,
837                      'node_fields:hostname':vnode_fqdn,
838                      'interface_fields:ip':ip, 
839                      'interface_fields:mac':mac,
840                      }
841             nodemap.update(self.network_settings())
842             maps.append ( (nodename, nodemap) )
843
844             utils.header("PROVISION node %s in box %s at IP %s with MAC %s"%\
845                              (nodename,qemu_boxname,vnode_hostname,mac))
846
847         return test_mapper.map({'node':maps})[0]
848
849     def localize_sfa_rspec (self,plc,options):
850        
851         plc['sfa']['SFA_REGISTRY_HOST'] = plc['PLC_DB_HOST']
852         plc['sfa']['SFA_AGGREGATE_HOST'] = plc['PLC_DB_HOST']
853         plc['sfa']['SFA_SM_HOST'] = plc['PLC_DB_HOST']
854         plc['sfa']['SFA_PLC_DB_HOST'] = plc['PLC_DB_HOST']
855         plc['sfa']['SFA_PLC_URL'] = 'https://' + plc['PLC_API_HOST'] + ':443/PLCAPI/' 
856         for site in plc['sites']:
857             for node in site['nodes']:
858                 plc['sfa']['sfa_slice_rspec']['part4'] = node['node_fields']['hostname']
859         return plc
860
861     #################### release:
862     def release (self,options):
863         self.vplc_pool.release_my_starting()
864         self.vnode_pool.release_my_starting()
865         pass
866
867     #################### show results for interactive mode
868     def list_all (self):
869         self.sense()
870         for b in self.all_boxes: b.list()
871
872     def get_box (self,box):
873         for b in self.build_boxes + self.plc_boxes + self.qemu_boxes:
874             if b.short_hostname()==box:
875                 return b
876         print "Could not find box %s"%box
877         return None
878
879     def list_box(self,box):
880         b=self.get_box(box)
881         if not b: return
882         b.sense()
883         b.list()
884
885     # can be run as a utility to manage the local infrastructure
886     def main (self):
887         parser=OptionParser()
888         parser.add_option ('-v',"--verbose",action='store_true',dest='verbose',default=False,
889                            help='verbose mode')
890         (options,args)=parser.parse_args()
891         if options.verbose:
892             self.options.verbose=True
893         if not args:
894             self.list_all()
895         else:
896             for box in args:
897                 self.list_box(box)