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