expand @DATE@
[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=re.compile("\s*(?P<pid>[0-9]+).*-[bo]\s+(?P<buildname>[^\s]+)(\s|\Z)")
305     def sense(self,reboot=False,verbose=True):
306         if reboot:
307             self.reboot(box)
308             return
309         print 'b',
310         command=['uptime']
311         self._uptime=self.backquote_ssh(command,trash_err=True).strip()
312         if not self._uptime: self._uptime='unreachable'
313         pids=self.backquote_ssh(['pgrep','build'],trash_err=True)
314         if not pids: return
315         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
316         ps_lines=self.backquote_ssh (command).split('\n')
317         for line in ps_lines:
318             if not line.strip() or line.find('PID')>=0: continue
319             m=BuildBox.matcher.match(line)
320             if m: 
321                 date=time.strftime('%Y-%m-%d',time.localtime(time.time()))
322                 buildname=m.group('buildname').replace('@DATE@',date)
323                 self.add_build (buildname,m.group('pid'))
324             else: header('command %r returned line that failed to match'%command)
325
326 ############################################################
327 class PlcInstance:
328     def __init__ (self, vservername, ctxid, plcbox):
329         self.vservername=vservername
330         self.ctxid=ctxid
331         self.plc_box=plcbox
332         # unknown yet
333         self.timestamp=0
334
335     def set_timestamp (self,timestamp): self.timestamp=timestamp
336     def set_now (self): self.timestamp=int(time.time())
337     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
338
339     def vplcname (self):
340         return self.vservername.split('-')[-1]
341
342     def line (self):
343         msg="== %s == (ctx=%s)"%(self.vservername,self.ctxid)
344         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
345         else:              msg += " *unknown timestamp*"
346         if self.ctxid==0:  msg+=" not (yet?) running"
347         return msg
348
349     def kill (self):
350         msg="vserver stopping %s on %s"%(self.vservername,self.plc_box.hostname)
351         self.plc_box.run_ssh(['vserver',self.vservername,'stop'],msg)
352         self.plc_box.forget(self)
353
354 class PlcBox (Box):
355     def __init__ (self, hostname, max_plcs):
356         Box.__init__(self,hostname)
357         self.plc_instances=[]
358         self.max_plcs=max_plcs
359
360     def add_vserver (self,vservername,ctxid):
361         for plc in self.plc_instances:
362             if plc.vservername==vservername: 
363                 header("WARNING, duplicate myplc %s running on %s"%\
364                            (vservername,self.hostname),banner=False)
365                 return
366         self.plc_instances.append(PlcInstance(vservername,ctxid,self))
367     
368     def forget (self, plc_instance):
369         self.plc_instances.remove(plc_instance)
370
371     # fill one slot even though this one is not started yet
372     def add_dummy (self, plcname):
373         dummy=PlcInstance('dummy_'+plcname,0,self)
374         dummy.set_now()
375         self.plc_instances.append(dummy)
376
377     def line(self): 
378         msg="%s [max=%d,%d free] (%s)"%(self.hostname, self.max_plcs,self.free_spots(),self.uname())
379         return msg
380         
381     def list(self):
382         if not self.plc_instances: 
383             header ('No vserver running on %s'%(self.line()))
384         else:
385             header ("Active plc VMs on %s"%self.line())
386             for p in self.plc_instances: 
387                 header (p.line(),banner=False)
388
389     def free_spots (self):
390         return self.max_plcs - len(self.plc_instances)
391
392     def uname(self):
393         if hasattr(self,'_uname') and self._uname: return self._uname
394         return '*undef* uname'
395
396     def plc_instance_by_vservername (self, vservername):
397         for p in self.plc_instances:
398             if p.vservername==vservername: return p
399         return None
400
401     def sense (self, reboot=False, soft=False):
402         if reboot:
403             # remove mark for all running servers to avoid resurrection
404             stop_command=['rm','-rf','/etc/vservers/*/apps/init/mark']
405             self.run_ssh(stop_command,"Removing all vserver marks on %s"%self.hostname)
406             if not soft:
407                 self.reboot()
408                 return
409             else:
410                 self.run_ssh(['service','util-vserver','stop'],"Stopping all running vservers")
411             return
412         print 'p',
413         self._uname=self.backquote_ssh(['uname','-r']).strip()
414         # try to find fullname (vserver_stat truncates to a ridiculously short name)
415         # fetch the contexts for all vservers on that box
416         map_command=['grep','.','/etc/vservers/*/context','/dev/null',]
417         context_map=self.backquote_ssh (map_command)
418         # at this point we have a set of lines like
419         # /etc/vservers/2010.01.20--k27-f12-32-vplc03/context:40144
420         ctx_dict={}
421         for map_line in context_map.split("\n"):
422             if not map_line: continue
423             [path,xid] = map_line.split(':')
424             ctx_dict[xid]=os.path.basename(os.path.dirname(path))
425         # at this point ctx_id maps context id to vservername
426
427         command=['vserver-stat']
428         vserver_stat = self.backquote_ssh (command)
429         for vserver_line in vserver_stat.split("\n"):
430             if not vserver_line: continue
431             context=vserver_line.split()[0]
432             if context=="CTX": continue
433             longname=ctx_dict[context]
434             self.add_vserver(longname,context)
435 #            print self.margin_outline(self.vplcname(longname)),"%(vserver_line)s [=%(longname)s]"%locals()
436
437         # scan timestamps 
438         running_vsnames = [ i.vservername for i in self.plc_instances ]
439         command=   ['grep','.']
440         command += ['/vservers/%s/timestamp'%vs for vs in running_vsnames]
441         command += ['/dev/null']
442         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
443         for ts_line in ts_lines:
444             if not ts_line.strip(): continue
445             # expect /vservers/<vservername>/timestamp:<timestamp>
446             try:
447                 (_,__,vservername,tail)=ts_line.split('/')
448                 (_,timestamp)=tail.split(':')
449                 timestamp=int(timestamp)
450                 p=self.plc_instance_by_vservername(vservername)
451                 if not p: 
452                     print 'WARNING unattached plc instance',ts_line
453                     print 'was expecting to find',vservername,'in',[i.vservername for i in self.plc_instances]
454                     continue
455                 p.set_timestamp(timestamp)
456             except:  print 'WARNING, could not parse ts line',ts_line
457         
458
459
460
461 ############################################################
462 class QemuInstance: 
463     def __init__ (self, nodename, pid, qemubox):
464         self.nodename=nodename
465         self.pid=pid
466         self.qemu_box=qemubox
467         # not known yet
468         self.buildname=None
469         self.timestamp=0
470         
471     def set_buildname (self,buildname): self.buildname=buildname
472     def set_timestamp (self,timestamp): self.timestamp=timestamp
473     def set_now (self): self.timestamp=int(time.time())
474     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
475     
476     def line (self):
477         msg = "== %s == (pid=%s)"%(self.nodename,self.pid)
478         if self.buildname: msg += " <--> %s"%self.buildname
479         else:              msg += " *unknown build*"
480         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
481         else:              msg += " *unknown timestamp*"
482         if self.pid:       msg += " pid=%s"%self.pid
483         else:              msg += " not (yet?) running"
484         return msg
485     
486     def kill(self):
487         if self.pid==0: 
488             print "cannot kill qemu %s with pid==0"%self.nodename
489             return
490         msg="Killing qemu %s with pid=%s on box %s"%(self.nodename,self.pid,self.qemu_box.hostname)
491         self.qemu_box.run_ssh(['kill',"%s"%self.pid],msg)
492         self.qemu_box.forget(self)
493
494
495 class QemuBox (Box):
496     def __init__ (self, hostname, max_qemus):
497         Box.__init__(self,hostname)
498         self.qemu_instances=[]
499         self.max_qemus=max_qemus
500
501     def add_node (self,nodename,pid):
502         for qemu in self.qemu_instances:
503             if qemu.nodename==nodename: 
504                 header("WARNING, duplicate qemu %s running on %s"%\
505                            (nodename,self.hostname), banner=False)
506                 return
507         self.qemu_instances.append(QemuInstance(nodename,pid,self))
508
509     def forget (self, qemu_instance):
510         self.qemu_instances.remove(qemu_instance)
511
512     # fill one slot even though this one is not started yet
513     def add_dummy (self, nodename):
514         dummy=QemuInstance('dummy_'+nodename,0,self)
515         dummy.set_now()
516         self.qemu_instances.append(dummy)
517
518     def line (self):
519         msg="%s [max=%d,%d free] (%s)"%(self.hostname, self.max_qemus,self.free_spots(),self.driver())
520         return msg
521
522     def list(self):
523         if not self.qemu_instances: 
524             header ('No qemu process on %s'%(self.line()))
525         else:
526             header ("Active qemu processes on %s"%(self.line()))
527             for q in self.qemu_instances: 
528                 header (q.line(),banner=False)
529
530     def free_spots (self):
531         return self.max_qemus - len(self.qemu_instances)
532
533     def driver(self):
534         if hasattr(self,'_driver') and self._driver: return self._driver
535         return '*undef* driver'
536
537     def qemu_instance_by_pid (self,pid):
538         for q in self.qemu_instances:
539             if q.pid==pid: return q
540         return None
541
542     def qemu_instance_by_nodename_buildname (self,nodename,buildname):
543         for q in self.qemu_instances:
544             if q.nodename==nodename and q.buildname==buildname:
545                 return q
546         return None
547
548     matcher=re.compile("\s*(?P<pid>[0-9]+).*-cdrom\s+(?P<nodename>[^\s]+)\.iso")
549     def sense(self, reboot=False, soft=False):
550         if reboot:
551             if not soft:
552                 self.reboot()
553             else:
554                 self.run_ssh(box,['pkill','qemu'],"Killing qemu instances")
555             return
556         print 'q',
557         modules=self.backquote_ssh(['lsmod']).split('\n')
558         self._driver='*NO kqemu/kmv_intel MODULE LOADED*'
559         for module in modules:
560             if module.find('kqemu')==0:
561                 self._driver='kqemu module loaded'
562             # kvm might be loaded without vkm_intel (we dont have AMD)
563             elif module.find('kvm_intel')==0:
564                 self._driver='kvm_intel module loaded'
565         ########## find out running pids
566         pids=self.backquote_ssh(['pgrep','qemu'])
567         if not pids: return
568         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
569         ps_lines = self.backquote_ssh (command).split("\n")
570         for line in ps_lines:
571             if not line.strip() or line.find('PID') >=0 : continue
572             m=QemuBox.matcher.match(line)
573             if m: self.add_node (m.group('nodename'),m.group('pid'))
574             else: header('command %r returned line that failed to match'%command)
575         ########## retrieve alive instances and map to build
576         live_builds=[]
577         command=['grep','.','*/*/qemu.pid','/dev/null']
578         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
579         for pid_line in pid_lines:
580             if not pid_line.strip(): continue
581             # expect <build>/<nodename>/qemu.pid:<pid>pid
582             try:
583                 (buildname,nodename,tail)=pid_line.split('/')
584                 (_,pid)=tail.split(':')
585                 q=self.qemu_instance_by_pid (pid)
586                 if not q: continue
587                 q.set_buildname(buildname)
588                 live_builds.append(buildname)
589             except: print 'WARNING, could not parse pid line',pid_line
590         # retrieve timestamps
591         command=   ['grep','.']
592         command += ['%s/*/timestamp'%b for b in live_builds]
593         command += ['/dev/null']
594         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
595         for ts_line in ts_lines:
596             if not ts_line.strip(): continue
597             # expect <build>/<nodename>/timestamp:<timestamp>
598             try:
599                 (buildname,nodename,tail)=ts_line.split('/')
600                 nodename=nodename.replace('qemu-','')
601                 (_,timestamp)=tail.split(':')
602                 timestamp=int(timestamp)
603                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
604                 if not q: 
605                     print 'WARNING unattached qemu instance',ts_line,nodename,buildname
606                     continue
607                 q.set_timestamp(timestamp)
608             except:  print 'WARNING, could not parse ts line',ts_line
609
610 ############################################################
611 class Options: pass
612
613 class Substrate:
614
615     def __init__ (self):
616         self.options=Options()
617         self.options.dry_run=False
618         self.options.verbose=False
619         self.options.probe=True
620         self.options.soft=True
621         self.build_boxes = [ BuildBox(h) for h in self.build_boxes_spec() ]
622         self.plc_boxes = [ PlcBox (h,m) for (h,m) in self.plc_boxes_spec ()]
623         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
624         self.all_boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes
625         self._sensed=False
626
627         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs")
628         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes")
629
630     def fqdn (self, hostname):
631         if hostname.find('.')<0: return "%s.%s"%(hostname,self.domain())
632     def short_hostname (self, hostname):
633         if hostname.find('.')>=0: return hostname.split('.')[0]
634
635     # return True if actual sensing takes place
636     def sense (self,force=False):
637         if self._sensed and not force: return False
638         print 'Sensing local substrate...',
639         for b in self.all_boxes: b.sense()
640         print 'Done'
641         self._sensed=True
642         return True
643
644     def add_dummy_plc (self, plc_boxname, plcname):
645         for pb in self.plc_boxes:
646             if pb.hostname==plc_boxname:
647                 pb.add_dummy(plcname)
648     def add_dummy_qemu (self, qemu_boxname, qemuname):
649         for qb in self.qemu_boxes:
650             if qb.hostname==qemu_boxname:
651                 qb.add_dummy(qemuname)
652
653     ########## 
654     def provision (self,plcs,options):
655         try:
656             # attach each plc to a plc box and an IP address
657             plcs = [ self.provision_plc (plc,options) for plc in plcs ]
658             # attach each node/qemu to a qemu box with an IP address
659             plcs = [ self.provision_qemus (plc,options) for plc in plcs ]
660             # update the SFA spec accordingly
661             plcs = [ self.localize_sfa_rspec(plc,options) for plc in plcs ]
662             return plcs
663         except Exception, e:
664             print '* Could not provision this test on current substrate','--',e,'--','exiting'
665             traceback.print_exc()
666             sys.exit(1)
667
668     # it is expected that a couple of options like ips_bplc and ips_vplc 
669     # are set or unset together
670     @staticmethod
671     def check_options (x,y):
672         if not x and not y: return True
673         return len(x)==len(y)
674
675     # find an available plc box (or make space)
676     # and a free IP address (using options if present)
677     def provision_plc (self, plc, options):
678         
679         assert Substrate.check_options (options.ips_bplc, options.ips_vplc)
680
681         #### let's find an IP address for that plc
682         # look in options 
683         if options.ips_vplc:
684             # this is a rerun
685             # we don't check anything here, 
686             # it is the caller's responsability to cleanup and make sure this makes sense
687             plc_boxname = options.ips_bplc.pop()
688             vplc_hostname=options.ips_vplc.pop()
689         else:
690             if self.sense(): self.list_all()
691             plc_boxname=None
692             vplc_hostname=None
693             # try to find an available IP 
694             self.vplc_pool.sense()
695             couple=self.vplc_pool.next_free()
696             if couple:
697                 (vplc_hostname,unused)=couple
698             #### we need to find one plc box that still has a slot
699             max_free=0
700             # use the box that has max free spots for load balancing
701             for pb in self.plc_boxes:
702                 free=pb.free_spots()
703                 if free>max_free:
704                     plc_boxname=pb.hostname
705                     max_free=free
706             # if there's no available slot in the plc_boxes, or we need a free IP address
707             # make space by killing the oldest running instance
708             if not plc_boxname or not vplc_hostname:
709                 # find the oldest of all our instances
710                 all_plc_instances=reduce(lambda x, y: x+y, 
711                                          [ pb.plc_instances for pb in self.plc_boxes ],
712                                          [])
713                 all_plc_instances.sort(timestamp_sort)
714                 try:
715                     plc_instance_to_kill=all_plc_instances[0]
716                 except:
717                     msg=""
718                     if not plc_boxname: msg += " PLC boxes are full"
719                     if not vplc_hostname: msg += " vplc IP pool exhausted" 
720                     raise Exception,"Could not make space for a PLC instance:"+msg
721                 freed_plc_boxname=plc_instance_to_kill.plc_box.hostname
722                 freed_vplc_hostname=plc_instance_to_kill.vplcname()
723                 message='killing oldest plc instance = %s on %s'%(plc_instance_to_kill.line(),
724                                                                   freed_plc_boxname)
725                 plc_instance_to_kill.kill()
726                 # use this new plcbox if that was the problem
727                 if not plc_boxname:
728                     plc_boxname=freed_plc_boxname
729                 # ditto for the IP address
730                 if not vplc_hostname:
731                     vplc_hostname=freed_vplc_hostname
732                     # record in pool as mine
733                     self.vplc_pool.set_mine(vplc_hostname)
734
735         # 
736         self.add_dummy_plc(plc_boxname,plc['name'])
737         vplc_ip = self.vplc_pool.get_ip(vplc_hostname)
738         self.vplc_pool.add_starting(vplc_hostname)
739
740         #### compute a helpful vserver name
741         # remove domain in hostname
742         vplc_short = self.short_hostname(vplc_hostname)
743         vservername = "%s-%d-%s" % (options.buildname,plc['index'],vplc_short)
744         plc_name = "%s_%s"%(plc['name'],vplc_short)
745
746         utils.header( 'PROVISION plc %s in box %s at IP %s as %s'%\
747                           (plc['name'],plc_boxname,vplc_hostname,vservername))
748
749         #### apply in the plc_spec
750         # # informative
751         # label=options.personality.replace("linux","")
752         mapper = {'plc': [ ('*' , {'host_box':plc_boxname,
753                                    # 'name':'%s-'+label,
754                                    'name': plc_name,
755                                    'vservername':vservername,
756                                    'vserverip':vplc_ip,
757                                    'PLC_DB_HOST':vplc_hostname,
758                                    'PLC_API_HOST':vplc_hostname,
759                                    'PLC_BOOT_HOST':vplc_hostname,
760                                    'PLC_WWW_HOST':vplc_hostname,
761                                    'PLC_NET_DNS1' : self.network_settings() [ 'interface_fields:dns1' ],
762                                    'PLC_NET_DNS2' : self.network_settings() [ 'interface_fields:dns2' ],
763                                    } ) ]
764                   }
765
766
767         # mappers only work on a list of plcs
768         return TestMapper([plc],options).map(mapper)[0]
769
770     ##########
771     def provision_qemus (self, plc, options):
772
773         assert Substrate.check_options (options.ips_bnode, options.ips_vnode)
774
775         test_mapper = TestMapper ([plc], options)
776         nodenames = test_mapper.node_names()
777         maps=[]
778         for nodename in nodenames:
779
780             if options.ips_vnode:
781                 # as above, it's a rerun, take it for granted
782                 qemu_boxname=options.ips_bnode.pop()
783                 vnode_hostname=options.ips_vnode.pop()
784             else:
785                 if self.sense(): self.list_all()
786                 qemu_boxname=None
787                 vnode_hostname=None
788                 # try to find an available IP 
789                 self.vnode_pool.sense()
790                 couple=self.vnode_pool.next_free()
791                 if couple:
792                     (vnode_hostname,unused)=couple
793                 # find a physical box
794                 max_free=0
795                 # use the box that has max free spots for load balancing
796                 for qb in self.qemu_boxes:
797                     free=qb.free_spots()
798                     if free>max_free:
799                         qemu_boxname=qb.hostname
800                         max_free=free
801                 # if we miss the box or the IP, kill the oldest instance
802                 if not qemu_boxname or not vnode_hostname:
803                 # find the oldest of all our instances
804                     all_qemu_instances=reduce(lambda x, y: x+y, 
805                                               [ qb.qemu_instances for qb in self.qemu_boxes ],
806                                               [])
807                     all_qemu_instances.sort(timestamp_sort)
808                     try:
809                         qemu_instance_to_kill=all_qemu_instances[0]
810                     except:
811                         msg=""
812                         if not qemu_boxname: msg += " QEMU boxes are full"
813                         if not vnode_hostname: msg += " vnode IP pool exhausted" 
814                         raise Exception,"Could not make space for a QEMU instance:"+msg
815                     freed_qemu_boxname=qemu_instance_to_kill.qemu_box.hostname
816                     freed_vnode_hostname=self.short_hostname(qemu_instance_to_kill.nodename)
817                     # kill it
818                     message='killing oldest qemu node = %s on %s'%(qemu_instance_to_kill.line(),
819                                                                    freed_qemu_boxname)
820                     qemu_instance_to_kill.kill()
821                     # use these freed resources where needed
822                     if not qemu_boxname:
823                         qemu_boxname=freed_qemu_boxname
824                     if not vnode_hostname:
825                         vnode_hostname=freed_vnode_hostname
826                         self.vnode_pool.set_mine(vnode_hostname)
827
828             self.add_dummy_qemu (qemu_boxname,nodename)
829             mac=self.vnode_pool.retrieve_userdata(vnode_hostname)
830             ip=self.vnode_pool.get_ip (vnode_hostname)
831             self.vnode_pool.add_starting(vnode_hostname)
832
833             vnode_fqdn = self.fqdn(vnode_hostname)
834             nodemap={'host_box':qemu_boxname,
835                      'node_fields:hostname':vnode_fqdn,
836                      'interface_fields:ip':ip, 
837                      'interface_fields:mac':mac,
838                      }
839             nodemap.update(self.network_settings())
840             maps.append ( (nodename, nodemap) )
841
842             utils.header("PROVISION node %s in box %s at IP %s with MAC %s"%\
843                              (nodename,qemu_boxname,vnode_hostname,mac))
844
845         return test_mapper.map({'node':maps})[0]
846
847     def localize_sfa_rspec (self,plc,options):
848        
849         plc['sfa']['SFA_REGISTRY_HOST'] = plc['PLC_DB_HOST']
850         plc['sfa']['SFA_AGGREGATE_HOST'] = plc['PLC_DB_HOST']
851         plc['sfa']['SFA_SM_HOST'] = plc['PLC_DB_HOST']
852         plc['sfa']['SFA_PLC_DB_HOST'] = plc['PLC_DB_HOST']
853         plc['sfa']['SFA_PLC_URL'] = 'https://' + plc['PLC_API_HOST'] + ':443/PLCAPI/' 
854         for site in plc['sites']:
855             for node in site['nodes']:
856                 plc['sfa']['sfa_slice_rspec']['part4'] = node['node_fields']['hostname']
857         return plc
858
859     #################### release:
860     def release (self,options):
861         self.vplc_pool.release_my_starting()
862         self.vnode_pool.release_my_starting()
863         pass
864
865     #################### show results for interactive mode
866     def list_all (self):
867         self.sense()
868         for b in self.all_boxes: b.list()
869
870     def get_box (self,box):
871         for b in self.build_boxes + self.plc_boxes + self.qemu_boxes:
872             if b.short_hostname()==box:
873                 return b
874         print "Could not find box %s"%box
875         return None
876
877     def list_box(self,box):
878         b=self.get_box(box)
879         if not b: return
880         b.sense()
881         b.list()
882
883     # can be run as a utility to manage the local infrastructure
884     def main (self):
885         parser=OptionParser()
886         parser.add_option ('-v',"--verbose",action='store_true',dest='verbose',default=False,
887                            help='verbose mode')
888         (options,args)=parser.parse_args()
889         if options.verbose:
890             self.options.verbose=True
891         if not args:
892             self.list_all()
893         else:
894             for box in args:
895                 self.list_box(box)