cleanup
[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 ####################
71 # pool class
72 # allows to pick an available IP among a pool
73 # input is expressed as a list of tuples (hostname,ip,user_data)
74 # that can be searched iteratively for a free slot
75 # e.g.
76 # pool = [ (hostname1,user_data1),  
77 #          (hostname2,user_data2),  
78 #          (hostname3,user_data2),  
79 #          (hostname4,user_data4) ]
80 # assuming that ip1 and ip3 are taken (pingable), then we'd get
81 # pool=Pool(pool)
82 # pool.next_free() -> entry2
83 # pool.next_free() -> entry4
84 # pool.next_free() -> None
85 # that is, even if ip2 is not busy/pingable when the second next_free() is issued
86
87 class PoolItem:
88     def __init__ (self,hostname,userdata):
89         self.hostname=hostname
90         self.userdata=userdata
91         # slot holds 'busy' or 'free' or 'mine' or 'starting' or None
92         # 'mine' is for our own stuff, 'starting' from the concurrent tests
93         self.status=None
94         self.ip=None
95
96     def line(self):
97         return "Pooled %s (%s) -> %s"%(self.hostname,self.userdata, self.status)
98
99     def char (self):
100         if   self.status==None:       return '?'
101         elif self.status=='busy':     return '+'
102         elif self.status=='free':     return '-'
103         elif self.status=='mine':     return 'M'
104         elif self.status=='starting': return 'S'
105
106     def get_ip(self):
107         if self.ip: return self.ip
108         ip=socket.gethostbyname(self.hostname)
109         self.ip=ip
110         return ip
111
112 class Pool:
113
114     def __init__ (self, tuples,message):
115         self.pool_items= [ PoolItem (hostname,userdata) for (hostname,userdata) in tuples ] 
116         self.message=message
117
118     def list (self):
119         for i in self.pool_items: print i.line()
120
121     def line (self):
122         line=self.message
123         for i in self.pool_items: line += ' ' + i.char()
124         return line
125
126     def _item (self, hostname):
127         for i in self.pool_items: 
128             if i.hostname==hostname: return i
129         raise Exception ("Could not locate hostname %s in pool %s"%(hostname,self.message))
130
131     def retrieve_userdata (self, hostname): 
132         return self._item(hostname).userdata
133
134     def get_ip (self, hostname):
135         try:    return self._item(hostname).get_ip()
136         except: return socket.gethostbyname(hostname)
137         
138     def set_mine (self, hostname):
139         try:
140             self._item(hostname).status='mine'
141         except:
142             print 'WARNING: host %s not found in IP pool %s'%(hostname,self.message)
143
144     def next_free (self):
145         for i in self.pool_items:
146             if i.status == 'free':
147                 i.status='mine'
148                 return (i.hostname,i.userdata)
149         return None
150
151     # the place were other test instances tell about their not-yet-started
152     # instances, that go undetected through sensing
153     starting='/root/starting'
154     def add_starting (self, name):
155         try:    items=[line.strip() for line in file(Pool.starting).readlines()]
156         except: items=[]
157         if not name in items:
158             file(Pool.starting,'a').write(name+'\n')
159         for i in self.pool_items:
160             if i.hostname==name: i.status='mine'
161             
162     # we load this after actual sensing; 
163     def load_starting (self):
164         try:    items=[line.strip() for line in file(Pool.starting).readlines()]
165         except: items=[]
166         for i in self.pool_items:
167             if i.hostname in items:
168                 if i.status=='free' : i.status='starting'
169
170     def release_my_starting (self):
171         for i in self.pool_items:
172             if i.status=='mine': 
173                 self.del_starting(i.hostname)
174                 i.status=None
175
176     def del_starting (self, name):
177         try:    items=[line.strip() for line in file(Pool.starting).readlines()]
178         except: items=[]
179         if name in items:
180             f=file(Pool.starting,'w')
181             for item in items: 
182                 if item != name: f.write(item+'\n')
183             f.close()
184     
185     ##########
186     def _sense (self):
187         for item in self.pool_items:
188             if item.status is not None: 
189                 print item.char(),
190                 continue
191             if self.check_ping (item.hostname): 
192                 item.status='busy'
193                 print '*',
194             else:
195                 item.status='free'
196                 print '.',
197     
198     def sense (self):
199         print 'Sensing IP pool',self.message,
200         self._sense()
201         print 'Done'
202         self.load_starting()
203         print 'After starting: IP pool'
204         print self.line()
205
206     # OS-dependent ping option (support for macos, for convenience)
207     ping_timeout_option = None
208     # returns True when a given hostname/ip responds to ping
209     def check_ping (self,hostname):
210         if not Pool.ping_timeout_option:
211             (status,osname) = commands.getstatusoutput("uname -s")
212             if status != 0:
213                 raise Exception, "TestPool: Cannot figure your OS name"
214             if osname == "Linux":
215                 Pool.ping_timeout_option="-w"
216             elif osname == "Darwin":
217                 Pool.ping_timeout_option="-t"
218
219         command="ping -c 1 %s 1 %s"%(Pool.ping_timeout_option,hostname)
220         (status,output) = commands.getstatusoutput(command)
221         return status == 0
222
223 ####################
224 class Box:
225     def __init__ (self,hostname):
226         self.hostname=hostname
227     def short_hostname (self):
228         return self.hostname.split('.')[0]
229     def test_ssh (self): return TestSsh(self.hostname,username='root',unknown_host=False)
230     def reboot (self, options):
231         self.test_ssh().run("shutdown -r now",message="Rebooting %s"%self.hostname,
232                             dry_run=options.dry_run)
233
234     def run(self,argv,message=None,trash_err=False,dry_run=False):
235         if dry_run:
236             print 'DRY_RUN:',
237             print " ".join(argv)
238             return 0
239         else:
240             header(message)
241             if not trash_err:
242                 return subprocess.call(argv)
243             else:
244                 return subprocess.call(argv,stderr=file('/dev/null','w'))
245                 
246     def run_ssh (self, argv, message, trash_err=False, dry_run=False):
247         ssh_argv = self.test_ssh().actual_argv(argv)
248         result=self.run (ssh_argv, message, trash_err, dry_run=dry_run)
249         if result!=0:
250             print "WARNING: failed to run %s on %s"%(" ".join(argv),self.hostname)
251         return result
252
253     def backquote (self, argv, trash_err=False):
254         if not trash_err:
255             result= subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
256         else:
257             result= subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
258         return result
259
260     def backquote_ssh (self, argv, trash_err=False):
261         # first probe the ssh link
262         probe_argv=self.test_ssh().actual_argv(['hostname'])
263         hostname=self.backquote ( probe_argv, trash_err=True )
264         if not hostname:
265             print "root@%s unreachable"%self.hostname
266             return ''
267         else:
268             return self.backquote( self.test_ssh().actual_argv(argv), trash_err)
269
270 ############################################################
271 class BuildInstance:
272     def __init__ (self, buildname, pid, buildbox):
273         self.buildname=buildname
274         self.buildbox=buildbox
275         self.pids=[pid]
276
277     def add_pid(self,pid):
278         self.pids.append(pid)
279
280     def line (self):
281         return "== %s == (pids=%r)"%(self.buildname,self.pids)
282
283 class BuildBox (Box):
284     def __init__ (self,hostname):
285         Box.__init__(self,hostname)
286         self.build_instances=[]
287
288     def add_build (self,buildname,pid):
289         for build in self.build_instances:
290             if build.buildname==buildname: 
291                 build.add_pid(pid)
292                 return
293         self.build_instances.append(BuildInstance(buildname, pid, self))
294
295     def list(self):
296         if not self.build_instances: 
297             header ('No build process on %s (%s)'%(self.hostname,self.uptime()))
298         else:
299             header ("Builds on %s (%s)"%(self.hostname,self.uptime()))
300             for b in self.build_instances: 
301                 header (b.line(),banner=False)
302
303     def uptime(self):
304         if hasattr(self,'_uptime') and self._uptime: return self._uptime
305         return '*undef* uptime'
306
307     # inspect box and find currently running builds
308     matcher=re.compile("\s*(?P<pid>[0-9]+).*-[bo]\s+(?P<buildname>[^\s]+)(\s|\Z)")
309     def sense(self,options):
310         if options.reboot:
311             if not options.soft:
312                 self.reboot(options)
313             else:
314                 command=['pkill','vbuild']
315                 self.run_ssh(command,"Terminating vbuild processes",dry_run=options.dry_run)
316             return
317         print 'b',
318         command=['uptime']
319         self._uptime=self.backquote_ssh(command,trash_err=True).strip()
320         if not self._uptime: self._uptime='unreachable'
321         pids=self.backquote_ssh(['pgrep','vbuild'],trash_err=True)
322         if not pids: return
323         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
324         ps_lines=self.backquote_ssh (command).split('\n')
325         for line in ps_lines:
326             if not line.strip() or line.find('PID')>=0: continue
327             m=BuildBox.matcher.match(line)
328             if m: 
329                 date=time.strftime('%Y-%m-%d',time.localtime(time.time()))
330                 buildname=m.group('buildname').replace('@DATE@',date)
331                 self.add_build (buildname,m.group('pid'))
332             else: header('command %r returned line that failed to match'%command)
333
334 ############################################################
335 class PlcInstance:
336     def __init__ (self, vservername, ctxid, plcbox):
337         self.vservername=vservername
338         self.ctxid=ctxid
339         self.plc_box=plcbox
340         # unknown yet
341         self.timestamp=0
342
343     def set_timestamp (self,timestamp): self.timestamp=timestamp
344     def set_now (self): self.timestamp=int(time.time())
345     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
346
347     def vplcname (self):
348         return self.vservername.split('-')[-1]
349     def buildname (self):
350         return self.vservername.rsplit('-',2)[0]
351
352     def line (self):
353         msg="== %s =="%(self.vplcname())
354         msg += " [=%s]"%self.vservername
355         if self.ctxid==0:  msg+=" not (yet?) running"
356         else:              msg+=" (ctx=%s)"%self.ctxid     
357         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
358         else:              msg += " *unknown timestamp*"
359         return msg
360
361     def kill (self):
362         msg="vserver stopping %s on %s"%(self.vservername,self.plc_box.hostname)
363         self.plc_box.run_ssh(['vserver',self.vservername,'stop'],msg)
364         self.plc_box.forget(self)
365
366 class PlcBox (Box):
367     def __init__ (self, hostname, max_plcs):
368         Box.__init__(self,hostname)
369         self.plc_instances=[]
370         self.max_plcs=max_plcs
371
372     def add_vserver (self,vservername,ctxid):
373         for plc in self.plc_instances:
374             if plc.vservername==vservername: 
375                 header("WARNING, duplicate myplc %s running on %s"%\
376                            (vservername,self.hostname),banner=False)
377                 return
378         self.plc_instances.append(PlcInstance(vservername,ctxid,self))
379     
380     def forget (self, plc_instance):
381         self.plc_instances.remove(plc_instance)
382
383     # fill one slot even though this one is not started yet
384     def add_dummy (self, plcname):
385         dummy=PlcInstance('dummy_'+plcname,0,self)
386         dummy.set_now()
387         self.plc_instances.append(dummy)
388
389     def line(self): 
390         msg="%s [max=%d,%d free] (%s)"%(self.hostname, self.max_plcs,self.free_spots(),self.uname())
391         return msg
392         
393     def list(self):
394         if not self.plc_instances: 
395             header ('No vserver running on %s'%(self.line()))
396         else:
397             header ("Active plc VMs on %s"%self.line())
398             self.plc_instances.sort(timestamp_sort)
399             for p in self.plc_instances: 
400                 header (p.line(),banner=False)
401
402     def free_spots (self):
403         return self.max_plcs - len(self.plc_instances)
404
405     def uname(self):
406         if hasattr(self,'_uname') and self._uname: return self._uname
407         return '*undef* uname'
408
409     def plc_instance_by_vservername (self, vservername):
410         for p in self.plc_instances:
411             if p.vservername==vservername: return p
412         return None
413
414     def sense (self, options):
415         if options.reboot:
416             if not options.soft:
417                 self.reboot(options)
418                 return
419             else:
420                 self.run_ssh(['service','util-vserver','stop'],"Stopping all running vservers",
421                              dry_run=options.dry_run)
422             return
423         print 'p',
424         self._uname=self.backquote_ssh(['uname','-r']).strip()
425         # try to find fullname (vserver_stat truncates to a ridiculously short name)
426         # fetch the contexts for all vservers on that box
427         map_command=['grep','.','/etc/vservers/*/context','/dev/null',]
428         context_map=self.backquote_ssh (map_command)
429         # at this point we have a set of lines like
430         # /etc/vservers/2010.01.20--k27-f12-32-vplc03/context:40144
431         ctx_dict={}
432         for map_line in context_map.split("\n"):
433             if not map_line: continue
434             [path,xid] = map_line.split(':')
435             ctx_dict[xid]=os.path.basename(os.path.dirname(path))
436         # at this point ctx_id maps context id to vservername
437
438         command=['vserver-stat']
439         vserver_stat = self.backquote_ssh (command)
440         for vserver_line in vserver_stat.split("\n"):
441             if not vserver_line: continue
442             context=vserver_line.split()[0]
443             if context=="CTX": continue
444             longname=ctx_dict[context]
445             self.add_vserver(longname,context)
446 #            print self.margin_outline(self.vplcname(longname)),"%(vserver_line)s [=%(longname)s]"%locals()
447
448         # scan timestamps 
449         running_vsnames = [ i.vservername for i in self.plc_instances ]
450         command=   ['grep','.']
451         command += ['/vservers/%s/timestamp'%vs for vs in running_vsnames]
452         command += ['/dev/null']
453         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
454         for ts_line in ts_lines:
455             if not ts_line.strip(): continue
456             # expect /vservers/<vservername>/timestamp:<timestamp>
457             try:
458                 (_,__,vservername,tail)=ts_line.split('/')
459                 (_,timestamp)=tail.split(':')
460                 timestamp=int(timestamp)
461                 p=self.plc_instance_by_vservername(vservername)
462                 if not p: 
463                     print 'WARNING unattached plc instance',ts_line
464                     print 'was expecting to find',vservername,'in',[i.vservername for i in self.plc_instances]
465                     continue
466                 p.set_timestamp(timestamp)
467             except:  print 'WARNING, could not parse ts line',ts_line
468         
469
470
471
472 ############################################################
473 class QemuInstance: 
474     def __init__ (self, nodename, pid, qemubox):
475         self.nodename=nodename
476         self.pid=pid
477         self.qemu_box=qemubox
478         # not known yet
479         self.buildname=None
480         self.timestamp=0
481         
482     def set_buildname (self,buildname): self.buildname=buildname
483     def set_timestamp (self,timestamp): self.timestamp=timestamp
484     def set_now (self): self.timestamp=int(time.time())
485     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
486     
487     def line (self):
488         msg = "== %s =="%(self.nodename)
489         msg += " [=%s]"%self.buildname
490         if self.pid:       msg += " (pid=%s)"%self.pid
491         else:              msg += " not (yet?) running"
492         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
493         else:              msg += " *unknown timestamp*"
494         return msg
495     
496     def kill(self):
497         if self.pid==0: 
498             print "cannot kill qemu %s with pid==0"%self.nodename
499             return
500         msg="Killing qemu %s with pid=%s on box %s"%(self.nodename,self.pid,self.qemu_box.hostname)
501         self.qemu_box.run_ssh(['kill',"%s"%self.pid],msg)
502         self.qemu_box.forget(self)
503
504
505 class QemuBox (Box):
506     def __init__ (self, hostname, max_qemus):
507         Box.__init__(self,hostname)
508         self.qemu_instances=[]
509         self.max_qemus=max_qemus
510
511     def add_node (self,nodename,pid):
512         for qemu in self.qemu_instances:
513             if qemu.nodename==nodename: 
514                 header("WARNING, duplicate qemu %s running on %s"%\
515                            (nodename,self.hostname), banner=False)
516                 return
517         self.qemu_instances.append(QemuInstance(nodename,pid,self))
518
519     def forget (self, qemu_instance):
520         self.qemu_instances.remove(qemu_instance)
521
522     # fill one slot even though this one is not started yet
523     def add_dummy (self, nodename):
524         dummy=QemuInstance('dummy_'+nodename,0,self)
525         dummy.set_now()
526         self.qemu_instances.append(dummy)
527
528     def line (self):
529         msg="%s [max=%d,%d free] (%s)"%(self.hostname, self.max_qemus,self.free_spots(),self.driver())
530         return msg
531
532     def list(self):
533         if not self.qemu_instances: 
534             header ('No qemu process on %s'%(self.line()))
535         else:
536             header ("Active qemu processes on %s"%(self.line()))
537             self.qemu_instances.sort(timestamp_sort)
538             for q in self.qemu_instances: 
539                 header (q.line(),banner=False)
540
541     def free_spots (self):
542         return self.max_qemus - len(self.qemu_instances)
543
544     def driver(self):
545         if hasattr(self,'_driver') and self._driver: return self._driver
546         return '*undef* driver'
547
548     def qemu_instance_by_pid (self,pid):
549         for q in self.qemu_instances:
550             if q.pid==pid: return q
551         return None
552
553     def qemu_instance_by_nodename_buildname (self,nodename,buildname):
554         for q in self.qemu_instances:
555             if q.nodename==nodename and q.buildname==buildname:
556                 return q
557         return None
558
559     matcher=re.compile("\s*(?P<pid>[0-9]+).*-cdrom\s+(?P<nodename>[^\s]+)\.iso")
560     def sense(self, options):
561         if options.reboot:
562             if not options.soft:
563                 self.reboot(options)
564             else:
565                 self.run_ssh(['pkill','qemu'],"Killing qemu instances",
566                              dry_run=options.dry_run)
567             return
568         print 'q',
569         modules=self.backquote_ssh(['lsmod']).split('\n')
570         self._driver='*NO kqemu/kmv_intel MODULE LOADED*'
571         for module in modules:
572             if module.find('kqemu')==0:
573                 self._driver='kqemu module loaded'
574             # kvm might be loaded without vkm_intel (we dont have AMD)
575             elif module.find('kvm_intel')==0:
576                 self._driver='kvm_intel module loaded'
577         ########## find out running pids
578         pids=self.backquote_ssh(['pgrep','qemu'])
579         if not pids: return
580         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
581         ps_lines = self.backquote_ssh (command).split("\n")
582         for line in ps_lines:
583             if not line.strip() or line.find('PID') >=0 : continue
584             m=QemuBox.matcher.match(line)
585             if m: self.add_node (m.group('nodename'),m.group('pid'))
586             else: header('command %r returned line that failed to match'%command)
587         ########## retrieve alive instances and map to build
588         live_builds=[]
589         command=['grep','.','*/*/qemu.pid','/dev/null']
590         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
591         for pid_line in pid_lines:
592             if not pid_line.strip(): continue
593             # expect <build>/<nodename>/qemu.pid:<pid>pid
594             try:
595                 (buildname,nodename,tail)=pid_line.split('/')
596                 (_,pid)=tail.split(':')
597                 q=self.qemu_instance_by_pid (pid)
598                 if not q: continue
599                 q.set_buildname(buildname)
600                 live_builds.append(buildname)
601             except: print 'WARNING, could not parse pid line',pid_line
602         # retrieve timestamps
603         command=   ['grep','.']
604         command += ['%s/*/timestamp'%b for b in live_builds]
605         command += ['/dev/null']
606         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
607         for ts_line in ts_lines:
608             if not ts_line.strip(): continue
609             # expect <build>/<nodename>/timestamp:<timestamp>
610             try:
611                 (buildname,nodename,tail)=ts_line.split('/')
612                 nodename=nodename.replace('qemu-','')
613                 (_,timestamp)=tail.split(':')
614                 timestamp=int(timestamp)
615                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
616                 if not q: 
617                     print 'WARNING unattached qemu instance',ts_line,nodename,buildname
618                     continue
619                 q.set_timestamp(timestamp)
620             except:  print 'WARNING, could not parse ts line',ts_line
621
622 ############################################################
623 class Options: pass
624
625 class Substrate:
626
627     def __init__ (self):
628         self.options=Options()
629         self.options.dry_run=False
630         self.options.verbose=False
631         self.options.reboot=False
632         self.options.soft=False
633         self.build_boxes = [ BuildBox(h) for h in self.build_boxes_spec() ]
634         self.plc_boxes = [ PlcBox (h,m) for (h,m) in self.plc_boxes_spec ()]
635         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
636         self.all_boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes
637         self._sensed=False
638
639         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs")
640         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes")
641
642     def fqdn (self, hostname):
643         if hostname.find('.')<0: return "%s.%s"%(hostname,self.domain())
644         return hostname
645     def short_hostname (self, hostname):
646         if hostname.find('.')>=0: return hostname.split('.')[0]
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 = self.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=self.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.short_hostname()==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)