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