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