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