fix parsing of timestamp
[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                 (ts_file,timestamp)=ts_line.split(':')
461                 ts_file=os.path.basename(ts_file)
462                 (vservername,_)=os.path.splitext(ts_file)
463                 timestamp=int(timestamp)
464                 p=self.plc_instance_by_vservername(vservername)
465                 if not p: 
466                     print 'WARNING unattached plc instance',ts_line
467                     print 'was expecting to find',vservername,'in',[i.vservername for i in self.plc_instances]
468                     continue
469                 p.set_timestamp(timestamp)
470             except:  print 'WARNING, could not parse ts line',ts_line
471         
472
473
474
475 ############################################################
476 class QemuInstance: 
477     def __init__ (self, nodename, pid, qemubox):
478         self.nodename=nodename
479         self.pid=pid
480         self.qemu_box=qemubox
481         # not known yet
482         self.buildname=None
483         self.timestamp=0
484         
485     def set_buildname (self,buildname): self.buildname=buildname
486     def set_timestamp (self,timestamp): self.timestamp=timestamp
487     def set_now (self): self.timestamp=int(time.time())
488     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
489     
490     def line (self):
491         msg = "== %s =="%(short_hostname(self.nodename))
492         msg += " [=%s]"%self.buildname
493         if self.pid:       msg += " (pid=%s)"%self.pid
494         else:              msg += " not (yet?) running"
495         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
496         else:              msg += " *unknown timestamp*"
497         return msg
498     
499     def kill(self):
500         if self.pid==0: 
501             print "cannot kill qemu %s with pid==0"%self.nodename
502             return
503         msg="Killing qemu %s with pid=%s on box %s"%(self.nodename,self.pid,self.qemu_box.hostname)
504         self.qemu_box.run_ssh(['kill',"%s"%self.pid],msg)
505         self.qemu_box.forget(self)
506
507
508 class QemuBox (Box):
509     def __init__ (self, hostname, max_qemus):
510         Box.__init__(self,hostname)
511         self.qemu_instances=[]
512         self.max_qemus=max_qemus
513
514     def add_node (self,nodename,pid):
515         for qemu in self.qemu_instances:
516             if qemu.nodename==nodename: 
517                 header("WARNING, duplicate qemu %s running on %s"%\
518                            (nodename,self.hostname), banner=False)
519                 return
520         self.qemu_instances.append(QemuInstance(nodename,pid,self))
521
522     def forget (self, qemu_instance):
523         self.qemu_instances.remove(qemu_instance)
524
525     # fill one slot even though this one is not started yet
526     def add_dummy (self, nodename):
527         dummy=QemuInstance('dummy_'+nodename,0,self)
528         dummy.set_now()
529         self.qemu_instances.append(dummy)
530
531     def line (self):
532         msg="%s [max=%d,%d free] (%s)"%(self.hostname, self.max_qemus,self.free_spots(),self.driver())
533         return msg
534
535     def list(self):
536         if not self.qemu_instances: 
537             header ('No qemu process on %s'%(self.line()))
538         else:
539             header ("Active qemu processes on %s"%(self.line()))
540             self.qemu_instances.sort(timestamp_sort)
541             for q in self.qemu_instances: 
542                 header (q.line(),banner=False)
543
544     def free_spots (self):
545         return self.max_qemus - len(self.qemu_instances)
546
547     def driver(self):
548         if hasattr(self,'_driver') and self._driver: return self._driver
549         return '*undef* driver'
550
551     def qemu_instance_by_pid (self,pid):
552         for q in self.qemu_instances:
553             if q.pid==pid: return q
554         return None
555
556     def qemu_instance_by_nodename_buildname (self,nodename,buildname):
557         for q in self.qemu_instances:
558             if q.nodename==nodename and q.buildname==buildname:
559                 return q
560         return None
561
562     matcher=re.compile("\s*(?P<pid>[0-9]+).*-cdrom\s+(?P<nodename>[^\s]+)\.iso")
563     def sense(self, options):
564         if options.reboot:
565             if not options.soft:
566                 self.reboot(options)
567             else:
568                 self.run_ssh(['pkill','qemu'],"Killing qemu instances",
569                              dry_run=options.dry_run)
570             return
571         print 'q',
572         modules=self.backquote_ssh(['lsmod']).split('\n')
573         self._driver='*NO kqemu/kmv_intel MODULE LOADED*'
574         for module in modules:
575             if module.find('kqemu')==0:
576                 self._driver='kqemu module loaded'
577             # kvm might be loaded without vkm_intel (we dont have AMD)
578             elif module.find('kvm_intel')==0:
579                 self._driver='kvm_intel module loaded'
580         ########## find out running pids
581         pids=self.backquote_ssh(['pgrep','qemu'])
582         if not pids: return
583         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
584         ps_lines = self.backquote_ssh (command).split("\n")
585         for line in ps_lines:
586             if not line.strip() or line.find('PID') >=0 : continue
587             m=QemuBox.matcher.match(line)
588             if m: self.add_node (m.group('nodename'),m.group('pid'))
589             else: header('command %r returned line that failed to match'%command)
590         ########## retrieve alive instances and map to build
591         live_builds=[]
592         command=['grep','.','*/*/qemu.pid','/dev/null']
593         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
594         for pid_line in pid_lines:
595             if not pid_line.strip(): continue
596             # expect <build>/<nodename>/qemu.pid:<pid>pid
597             try:
598                 (buildname,nodename,tail)=pid_line.split('/')
599                 (_,pid)=tail.split(':')
600                 q=self.qemu_instance_by_pid (pid)
601                 if not q: continue
602                 q.set_buildname(buildname)
603                 live_builds.append(buildname)
604             except: print 'WARNING, could not parse pid line',pid_line
605         # retrieve timestamps
606         command=   ['grep','.']
607         command += ['%s/*/timestamp'%b for b in live_builds]
608         command += ['/dev/null']
609         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
610         for ts_line in ts_lines:
611             if not ts_line.strip(): continue
612             # expect <build>/<nodename>/timestamp:<timestamp>
613             try:
614                 (buildname,nodename,tail)=ts_line.split('/')
615                 nodename=nodename.replace('qemu-','')
616                 (_,timestamp)=tail.split(':')
617                 timestamp=int(timestamp)
618                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
619                 if not q: 
620                     print 'WARNING unattached qemu instance',ts_line,nodename,buildname
621                     continue
622                 q.set_timestamp(timestamp)
623             except:  print 'WARNING, could not parse ts line',ts_line
624
625 ############################################################
626 class Options: pass
627
628 class Substrate:
629
630     def __init__ (self):
631         self.options=Options()
632         self.options.dry_run=False
633         self.options.verbose=False
634         self.options.reboot=False
635         self.options.soft=False
636         self.build_boxes = [ BuildBox(h) for h in self.build_boxes_spec() ]
637         self.plc_boxes = [ PlcBox (h,m) for (h,m) in self.plc_boxes_spec ()]
638         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
639         self.all_boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes
640         self._sensed=False
641
642         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs")
643         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes")
644
645     def fqdn (self, hostname):
646         if hostname.find('.')<0: return "%s.%s"%(hostname,self.domain())
647         return hostname
648
649     # return True if actual sensing takes place
650     def sense (self,force=False):
651         if self._sensed and not force: return False
652         print 'Sensing local substrate...',
653         for b in self.all_boxes: b.sense(self.options)
654         print 'Done'
655         self._sensed=True
656         return True
657
658     def add_dummy_plc (self, plc_boxname, plcname):
659         for pb in self.plc_boxes:
660             if pb.hostname==plc_boxname:
661                 pb.add_dummy(plcname)
662     def add_dummy_qemu (self, qemu_boxname, qemuname):
663         for qb in self.qemu_boxes:
664             if qb.hostname==qemu_boxname:
665                 qb.add_dummy(qemuname)
666
667     ########## 
668     def provision (self,plcs,options):
669         try:
670             # attach each plc to a plc box and an IP address
671             plcs = [ self.provision_plc (plc,options) for plc in plcs ]
672             # attach each node/qemu to a qemu box with an IP address
673             plcs = [ self.provision_qemus (plc,options) for plc in plcs ]
674             # update the SFA spec accordingly
675             plcs = [ self.localize_sfa_rspec(plc,options) for plc in plcs ]
676             return plcs
677         except Exception, e:
678             print '* Could not provision this test on current substrate','--',e,'--','exiting'
679             traceback.print_exc()
680             sys.exit(1)
681
682     # it is expected that a couple of options like ips_bplc and ips_vplc 
683     # are set or unset together
684     @staticmethod
685     def check_options (x,y):
686         if not x and not y: return True
687         return len(x)==len(y)
688
689     # find an available plc box (or make space)
690     # and a free IP address (using options if present)
691     def provision_plc (self, plc, options):
692         
693         assert Substrate.check_options (options.ips_bplc, options.ips_vplc)
694
695         #### let's find an IP address for that plc
696         # look in options 
697         if options.ips_vplc:
698             # this is a rerun
699             # we don't check anything here, 
700             # it is the caller's responsability to cleanup and make sure this makes sense
701             plc_boxname = options.ips_bplc.pop()
702             vplc_hostname=options.ips_vplc.pop()
703         else:
704             if self.sense(): self.list_all()
705             plc_boxname=None
706             vplc_hostname=None
707             # try to find an available IP 
708             self.vplc_pool.sense()
709             couple=self.vplc_pool.next_free()
710             if couple:
711                 (vplc_hostname,unused)=couple
712             #### we need to find one plc box that still has a slot
713             max_free=0
714             # use the box that has max free spots for load balancing
715             for pb in self.plc_boxes:
716                 free=pb.free_spots()
717                 if free>max_free:
718                     plc_boxname=pb.hostname
719                     max_free=free
720             # if there's no available slot in the plc_boxes, or we need a free IP address
721             # make space by killing the oldest running instance
722             if not plc_boxname or not vplc_hostname:
723                 # find the oldest of all our instances
724                 all_plc_instances=reduce(lambda x, y: x+y, 
725                                          [ pb.plc_instances for pb in self.plc_boxes ],
726                                          [])
727                 all_plc_instances.sort(timestamp_sort)
728                 try:
729                     plc_instance_to_kill=all_plc_instances[0]
730                 except:
731                     msg=""
732                     if not plc_boxname: msg += " PLC boxes are full"
733                     if not vplc_hostname: msg += " vplc IP pool exhausted" 
734                     raise Exception,"Could not make space for a PLC instance:"+msg
735                 freed_plc_boxname=plc_instance_to_kill.plc_box.hostname
736                 freed_vplc_hostname=plc_instance_to_kill.vplcname()
737                 message='killing oldest plc instance = %s on %s'%(plc_instance_to_kill.line(),
738                                                                   freed_plc_boxname)
739                 plc_instance_to_kill.kill()
740                 # use this new plcbox if that was the problem
741                 if not plc_boxname:
742                     plc_boxname=freed_plc_boxname
743                 # ditto for the IP address
744                 if not vplc_hostname:
745                     vplc_hostname=freed_vplc_hostname
746                     # record in pool as mine
747                     self.vplc_pool.set_mine(vplc_hostname)
748
749         # 
750         self.add_dummy_plc(plc_boxname,plc['name'])
751         vplc_ip = self.vplc_pool.get_ip(vplc_hostname)
752         self.vplc_pool.add_starting(vplc_hostname)
753
754         #### compute a helpful vserver name
755         # remove domain in hostname
756         vplc_short = short_hostname(vplc_hostname)
757         vservername = "%s-%d-%s" % (options.buildname,plc['index'],vplc_short)
758         plc_name = "%s_%s"%(plc['name'],vplc_short)
759
760         utils.header( 'PROVISION plc %s in box %s at IP %s as %s'%\
761                           (plc['name'],plc_boxname,vplc_hostname,vservername))
762
763         #### apply in the plc_spec
764         # # informative
765         # label=options.personality.replace("linux","")
766         mapper = {'plc': [ ('*' , {'host_box':plc_boxname,
767                                    # 'name':'%s-'+label,
768                                    'name': plc_name,
769                                    'vservername':vservername,
770                                    'vserverip':vplc_ip,
771                                    'PLC_DB_HOST':vplc_hostname,
772                                    'PLC_API_HOST':vplc_hostname,
773                                    'PLC_BOOT_HOST':vplc_hostname,
774                                    'PLC_WWW_HOST':vplc_hostname,
775                                    'PLC_NET_DNS1' : self.network_settings() [ 'interface_fields:dns1' ],
776                                    'PLC_NET_DNS2' : self.network_settings() [ 'interface_fields:dns2' ],
777                                    } ) ]
778                   }
779
780
781         # mappers only work on a list of plcs
782         return TestMapper([plc],options).map(mapper)[0]
783
784     ##########
785     def provision_qemus (self, plc, options):
786
787         assert Substrate.check_options (options.ips_bnode, options.ips_vnode)
788
789         test_mapper = TestMapper ([plc], options)
790         nodenames = test_mapper.node_names()
791         maps=[]
792         for nodename in nodenames:
793
794             if options.ips_vnode:
795                 # as above, it's a rerun, take it for granted
796                 qemu_boxname=options.ips_bnode.pop()
797                 vnode_hostname=options.ips_vnode.pop()
798             else:
799                 if self.sense(): self.list_all()
800                 qemu_boxname=None
801                 vnode_hostname=None
802                 # try to find an available IP 
803                 self.vnode_pool.sense()
804                 couple=self.vnode_pool.next_free()
805                 if couple:
806                     (vnode_hostname,unused)=couple
807                 # find a physical box
808                 max_free=0
809                 # use the box that has max free spots for load balancing
810                 for qb in self.qemu_boxes:
811                     free=qb.free_spots()
812                     if free>max_free:
813                         qemu_boxname=qb.hostname
814                         max_free=free
815                 # if we miss the box or the IP, kill the oldest instance
816                 if not qemu_boxname or not vnode_hostname:
817                 # find the oldest of all our instances
818                     all_qemu_instances=reduce(lambda x, y: x+y, 
819                                               [ qb.qemu_instances for qb in self.qemu_boxes ],
820                                               [])
821                     all_qemu_instances.sort(timestamp_sort)
822                     try:
823                         qemu_instance_to_kill=all_qemu_instances[0]
824                     except:
825                         msg=""
826                         if not qemu_boxname: msg += " QEMU boxes are full"
827                         if not vnode_hostname: msg += " vnode IP pool exhausted" 
828                         raise Exception,"Could not make space for a QEMU instance:"+msg
829                     freed_qemu_boxname=qemu_instance_to_kill.qemu_box.hostname
830                     freed_vnode_hostname=short_hostname(qemu_instance_to_kill.nodename)
831                     # kill it
832                     message='killing oldest qemu node = %s on %s'%(qemu_instance_to_kill.line(),
833                                                                    freed_qemu_boxname)
834                     qemu_instance_to_kill.kill()
835                     # use these freed resources where needed
836                     if not qemu_boxname:
837                         qemu_boxname=freed_qemu_boxname
838                     if not vnode_hostname:
839                         vnode_hostname=freed_vnode_hostname
840                         self.vnode_pool.set_mine(vnode_hostname)
841
842             self.add_dummy_qemu (qemu_boxname,nodename)
843             mac=self.vnode_pool.retrieve_userdata(vnode_hostname)
844             ip=self.vnode_pool.get_ip (vnode_hostname)
845             self.vnode_pool.add_starting(vnode_hostname)
846
847             vnode_fqdn = self.fqdn(vnode_hostname)
848             nodemap={'host_box':qemu_boxname,
849                      'node_fields:hostname':vnode_fqdn,
850                      'interface_fields:ip':ip, 
851                      'interface_fields:mac':mac,
852                      }
853             nodemap.update(self.network_settings())
854             maps.append ( (nodename, nodemap) )
855
856             utils.header("PROVISION node %s in box %s at IP %s with MAC %s"%\
857                              (nodename,qemu_boxname,vnode_hostname,mac))
858
859         return test_mapper.map({'node':maps})[0]
860
861     def localize_sfa_rspec (self,plc,options):
862        
863         plc['sfa']['SFA_REGISTRY_HOST'] = plc['PLC_DB_HOST']
864         plc['sfa']['SFA_AGGREGATE_HOST'] = plc['PLC_DB_HOST']
865         plc['sfa']['SFA_SM_HOST'] = plc['PLC_DB_HOST']
866         plc['sfa']['SFA_PLC_DB_HOST'] = plc['PLC_DB_HOST']
867         plc['sfa']['SFA_PLC_URL'] = 'https://' + plc['PLC_API_HOST'] + ':443/PLCAPI/' 
868         for site in plc['sites']:
869             for node in site['nodes']:
870                 plc['sfa']['sfa_slice_rspec']['part4'] = node['node_fields']['hostname']
871         return plc
872
873     #################### release:
874     def release (self,options):
875         self.vplc_pool.release_my_starting()
876         self.vnode_pool.release_my_starting()
877         pass
878
879     #################### show results for interactive mode
880     def get_box (self,box):
881         for b in self.build_boxes + self.plc_boxes + self.qemu_boxes:
882             if b.shortname()==box:
883                 return b
884         print "Could not find box %s"%box
885         return None
886
887     def list_all (self):
888         self.sense()
889         for b in self.all_boxes: b.list()
890
891     def list_boxes(self,boxes):
892         print 'Partial Sensing',
893         for box in boxes:
894             b=self.get_box(box)
895             if not b: continue
896             b.sense(self.options)
897         print 'Done'
898         for box in boxes:
899             b=self.get_box(box)
900             if not b: continue
901             b.list()
902
903     ####################
904     # can be run as a utility to manage the local infrastructure
905     def main (self):
906         parser=OptionParser()
907         parser.add_option ('-r',"--reboot",action='store_true',dest='reboot',default=False,
908                            help='reboot mode (use shutdown -r)')
909         parser.add_option ('-s',"--soft",action='store_true',dest='soft',default=False,
910                            help='soft mode for reboot (vserver stop or kill qemus)')
911         parser.add_option ('-b',"--build",action='store_true',dest='builds',default=False,
912                            help='add build boxes')
913         parser.add_option ('-p',"--plc",action='store_true',dest='plcs',default=False,
914                            help='add plc boxes')
915         parser.add_option ('-q',"--qemu",action='store_true',dest='qemus',default=False,
916                            help='add qemu boxes')
917         parser.add_option ('-v',"--verbose",action='store_true',dest='verbose',default=False,
918                            help='verbose mode')
919         parser.add_option ('-n',"--dry_run",action='store_true',dest='dry_run',default=False,
920                            help='dry run mode')
921         (self.options,args)=parser.parse_args()
922
923         boxes=args
924         if self.options.builds: boxes += [b.hostname for b in self.build_boxes]
925         if self.options.plcs: boxes += [b.hostname for b in self.plc_boxes]
926         if self.options.qemus: boxes += [b.hostname for b in self.qemu_boxes]
927         boxes=list(set(boxes))
928         
929         if not boxes:
930             self.list_all()
931         else:
932             self.list_boxes (boxes)