cosmetic
[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 # too painful to propagate this cleanly
63 verbose=None
64
65 def header (message,banner=True):
66     if not message: return
67     if banner: print "===============",
68     print message
69     sys.stdout.flush()
70
71 def timestamp_sort(o1,o2): return o1.timestamp-o2.timestamp
72
73 def short_hostname (hostname):
74     return hostname.split('.')[0]
75
76 ####################
77 # the place were other test instances tell about their not-yet-started
78 # instances, that go undetected through sensing
79 class Starting:
80
81     location='/root/starting'
82     def __init__ (self):
83         self.tuples=[]
84
85     def load (self):
86         try:    self.tuples=[line.strip().split('@') 
87                              for line in file(Starting.location).readlines()]
88         except: self.tuples=[]
89
90     def vnames (self) : 
91         self.load()
92         return [ x for (x,_) in self.tuples ]
93
94     def add (self, vname, bname):
95         if not vname in self.vnames():
96             file(Starting.location,'a').write("%s@%s\n"%(vname,bname))
97             
98     def delete_vname (self, vname):
99         self.load()
100         if vname in self.vnames():
101             f=file(Starting.location,'w')
102             for (v,b) in self.tuples: 
103                 if v != vname: f.write("%s@%s\n"%(v,b))
104             f.close()
105     
106 ####################
107 # pool class
108 # allows to pick an available IP among a pool
109 # input is expressed as a list of tuples (hostname,ip,user_data)
110 # that can be searched iteratively for a free slot
111 # e.g.
112 # pool = [ (hostname1,user_data1),  
113 #          (hostname2,user_data2),  
114 #          (hostname3,user_data2),  
115 #          (hostname4,user_data4) ]
116 # assuming that ip1 and ip3 are taken (pingable), then we'd get
117 # pool=Pool(pool)
118 # pool.next_free() -> entry2
119 # pool.next_free() -> entry4
120 # pool.next_free() -> None
121 # that is, even if ip2 is not busy/pingable when the second next_free() is issued
122
123 class PoolItem:
124     def __init__ (self,hostname,userdata):
125         self.hostname=hostname
126         self.userdata=userdata
127         # slot holds 'busy' or 'free' or 'mine' or 'starting' or None
128         # 'mine' is for our own stuff, 'starting' from the concurrent tests
129         self.status=None
130         self.ip=None
131
132     def line(self):
133         return "Pooled %s (%s) -> %s"%(self.hostname,self.userdata, self.status)
134
135     def char (self):
136         if   self.status==None:       return '?'
137         elif self.status=='busy':     return '+'
138         elif self.status=='free':     return '-'
139         elif self.status=='mine':     return 'M'
140         elif self.status=='starting': return 'S'
141
142     def get_ip(self):
143         if self.ip: return self.ip
144         ip=socket.gethostbyname(self.hostname)
145         self.ip=ip
146         return ip
147
148 class Pool:
149
150     def __init__ (self, tuples,message, substrate):
151         self.pool_items= [ PoolItem (hostname,userdata) for (hostname,userdata) in tuples ] 
152         self.message=message
153         # where to send notifications upon load_starting
154         self.substrate=substrate
155
156     def list (self, verbose=False):
157         for i in self.pool_items: print i.line()
158
159     def line (self):
160         line=self.message
161         for i in self.pool_items: line += ' ' + i.char()
162         return line
163
164     def _item (self, hostname):
165         for i in self.pool_items: 
166             if i.hostname==hostname: return i
167         raise Exception ("Could not locate hostname %s in pool %s"%(hostname,self.message))
168
169     def retrieve_userdata (self, hostname): 
170         return self._item(hostname).userdata
171
172     def get_ip (self, hostname):
173         try:    return self._item(hostname).get_ip()
174         except: return socket.gethostbyname(hostname)
175         
176     def set_mine (self, hostname):
177         try:
178             self._item(hostname).status='mine'
179         except:
180             print 'WARNING: host %s not found in IP pool %s'%(hostname,self.message)
181
182     def next_free (self):
183         for i in self.pool_items:
184             if i.status == 'free':
185                 i.status='mine'
186                 return (i.hostname,i.userdata)
187         return None
188
189     ####################
190     # we have a starting instance of our own
191     def add_starting (self, vname, bname):
192         Starting().add(vname,bname)
193         for i in self.pool_items:
194             if i.hostname==vname: i.status='mine'
195
196     # load the starting instances from the common file
197     # remember that might be ours
198     # return the list of (vname,bname) that are not ours
199     def load_starting (self):
200         starting=Starting()
201         starting.load()
202         new_tuples=[]
203         for (v,b) in starting.tuples:
204             for i in self.pool_items:
205                 if i.hostname==v and i.status=='free':
206                     i.status='starting'
207                     new_tuples.append( (v,b,) )
208         return new_tuples
209
210     def release_my_starting (self):
211         for i in self.pool_items:
212             if i.status=='mine':
213                 Starting().delete_vname (i.hostname)
214                 i.status=None
215
216
217     ##########
218     def _sense (self):
219         for item in self.pool_items:
220             if item.status is not None: 
221                 print item.char(),
222                 continue
223             if self.check_ping (item.hostname): 
224                 item.status='busy'
225                 print '*',
226             else:
227                 item.status='free'
228                 print '.',
229     
230     def sense (self):
231         print 'Sensing IP pool',self.message,
232         self._sense()
233         print 'Done'
234         for (vname,bname) in self.load_starting():
235             self.substrate.add_starting_dummy (bname, vname)
236         print 'After starting: IP pool'
237         print self.line()
238     # OS-dependent ping option (support for macos, for convenience)
239     ping_timeout_option = None
240     # returns True when a given hostname/ip responds to ping
241     def check_ping (self,hostname):
242         if not Pool.ping_timeout_option:
243             (status,osname) = commands.getstatusoutput("uname -s")
244             if status != 0:
245                 raise Exception, "TestPool: Cannot figure your OS name"
246             if osname == "Linux":
247                 Pool.ping_timeout_option="-w"
248             elif osname == "Darwin":
249                 Pool.ping_timeout_option="-t"
250
251         command="ping -c 1 %s 1 %s"%(Pool.ping_timeout_option,hostname)
252         (status,output) = commands.getstatusoutput(command)
253         return status == 0
254
255 ####################
256 class Box:
257     def __init__ (self,hostname):
258         self.hostname=hostname
259         self._probed=None
260     def shortname (self):
261         return short_hostname(self.hostname)
262     def test_ssh (self): return TestSsh(self.hostname,username='root',unknown_host=False)
263     def reboot (self, options):
264         self.test_ssh().run("shutdown -r now",message="Rebooting %s"%self.hostname,
265                             dry_run=options.dry_run)
266
267     def hostname_fedora (self,virt=None):
268         result = "%s {"%self.hostname
269         if virt: result += "%s-"%virt
270         result += "%s"%self.fedora()
271         # too painful to propagate this cleanly
272         global verbose
273         if verbose:
274             result += "-%s" % self.uname()
275         result += "}"
276         return result
277
278     separator = "===composite==="
279
280     # probe the ssh link
281     # take this chance to gather useful stuff
282     def probe (self):
283         # try it only once
284         if self._probed is not None: return self._probed
285         composite_command = [ ]
286         composite_command += [ "hostname" ]
287         composite_command += [ ";" , "echo", Box.separator , ";" ]
288         composite_command += [ "uptime" ]
289         composite_command += [ ";" , "echo", Box.separator , ";" ]
290         composite_command += [ "uname", "-r"]
291         composite_command += [ ";" , "echo", Box.separator , ";" ]
292         composite_command += [ "cat" , "/etc/fedora-release" ]
293
294         # due to colons and all, this is going wrong on the local box (typically testmaster)
295         # I am reluctant to change TestSsh as it might break all over the place, so
296         if self.test_ssh().is_local():
297             probe_argv = [ "bash", "-c", " ".join (composite_command) ]
298         else:
299             probe_argv=self.test_ssh().actual_argv(composite_command)
300         composite=self.backquote ( probe_argv, trash_err=True )
301         self._hostname = self._uptime = self._uname = self._fedora = "** Unknown **"
302         if not composite: 
303             print "root@%s unreachable"%self.hostname
304             self._probed=''
305         else:
306             try:
307                 pieces = composite.split(Box.separator)
308                 pieces = [ x.strip() for x in pieces ]
309                 [self._hostname, self._uptime, self._uname, self._fedora] = pieces
310                 # customize
311                 self._uptime = ', '.join([ x.strip() for x in self._uptime.split(',')[2:]])
312                 self._fedora = self._fedora.replace("Fedora release ","f").split(" ")[0]
313             except:
314                 import traceback
315                 print 'BEG issue with pieces',pieces
316                 traceback.print_exc()
317                 print 'END issue with pieces',pieces
318             self._probed=self._hostname
319         return self._probed
320
321     # use argv=['bash','-c',"the command line"]
322     def uptime(self):
323         self.probe()
324         if hasattr(self,'_uptime') and self._uptime: return self._uptime
325         return '*unprobed* uptime'
326     def uname(self):
327         self.probe()
328         if hasattr(self,'_uname') and self._uname: return self._uname
329         return '*unprobed* uname'
330     def fedora(self):
331         self.probe()
332         if hasattr(self,'_fedora') and self._fedora: return self._fedora
333         return '*unprobed* fedora'
334
335     def run(self,argv,message=None,trash_err=False,dry_run=False):
336         if dry_run:
337             print 'DRY_RUN:',
338             print " ".join(argv)
339             return 0
340         else:
341             header(message)
342             if not trash_err:
343                 return subprocess.call(argv)
344             else:
345                 return subprocess.call(argv,stderr=file('/dev/null','w'))
346                 
347     def run_ssh (self, argv, message, trash_err=False, dry_run=False):
348         ssh_argv = self.test_ssh().actual_argv(argv)
349         result=self.run (ssh_argv, message, trash_err, dry_run=dry_run)
350         if result!=0:
351             print "WARNING: failed to run %s on %s"%(" ".join(argv),self.hostname)
352         return result
353
354     def backquote (self, argv, trash_err=False):
355         # print 'running backquote',argv
356         if not trash_err:
357             result= subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
358         else:
359             result= subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
360         return result
361
362     # if you have any shell-expanded arguments like *
363     # and if there's any chance the command is adressed to the local host
364     def backquote_ssh (self, argv, trash_err=False):
365         if not self.probe(): return ''
366         return self.backquote( self.test_ssh().actual_argv(argv), trash_err)
367
368 ############################################################
369 class BuildInstance:
370     def __init__ (self, buildname, pid, buildbox):
371         self.buildname=buildname
372         self.buildbox=buildbox
373         self.pids=[pid]
374
375     def add_pid(self,pid):
376         self.pids.append(pid)
377
378     def line (self):
379         return "== %s == (pids=%r)"%(self.buildname,self.pids)
380
381 class BuildBox (Box):
382     def __init__ (self,hostname):
383         Box.__init__(self,hostname)
384         self.build_instances=[]
385
386     def add_build (self,buildname,pid):
387         for build in self.build_instances:
388             if build.buildname==buildname: 
389                 build.add_pid(pid)
390                 return
391         self.build_instances.append(BuildInstance(buildname, pid, self))
392
393     def list(self, verbose=False):
394         if not self.build_instances: 
395             header ('No build process on %s (%s)'%(self.hostname_fedora(),self.uptime()))
396         else:
397             header ("Builds on %s (%s)"%(self.hostname_fedora(),self.uptime()))
398             for b in self.build_instances: 
399                 header (b.line(),banner=False)
400
401     def reboot (self, options):
402         if not options.soft:
403             Box.reboot(self,options)
404         else:
405             self.soft_reboot (options)
406
407 class BuildVsBox (BuildBox):
408     def soft_reboot (self, options):
409             command=['pkill','vbuild']
410             self.run_ssh(command,"Terminating vbuild processes",dry_run=options.dry_run)
411
412     # inspect box and find currently running builds
413     matcher=re.compile("\s*(?P<pid>[0-9]+).*-[bo]\s+(?P<buildname>[^\s]+)(\s|\Z)")
414     matcher_building_vm=re.compile("\s*(?P<pid>[0-9]+).*init-vserver.*\s+(?P<buildname>[^\s]+)\s*\Z")
415     def sense(self, options):
416         print 'vb',
417         pids=self.backquote_ssh(['pgrep','vbuild'],trash_err=True)
418         if not pids: return
419         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
420         ps_lines=self.backquote_ssh (command).split('\n')
421         for line in ps_lines:
422             if not line.strip() or line.find('PID')>=0: continue
423             m=BuildBox.matcher.match(line)
424             if m: 
425                 date=time.strftime('%Y-%m-%d',time.localtime(time.time()))
426                 buildname=m.group('buildname').replace('@DATE@',date)
427                 self.add_build (buildname,m.group('pid'))
428                 continue
429             m=BuildBox.matcher_building_vm.match(line)
430             if m: 
431                 # buildname is expansed here
432                 self.add_build (buildname,m.group('pid'))
433                 continue
434             header('BuildBox.sense: command %r returned line that failed to match'%command)
435             header(">>%s<<"%line)
436
437 class BuildLxcBox (BuildBox):
438     def soft_reboot (self, options):
439             command=['pkill','lbuild']
440             self.run_ssh(command,"Terminating vbuild processes",dry_run=options.dry_run)
441
442     # inspect box and find currently running builds
443     def sense(self, options):
444         print 'xb (Substrate.BuildLxcBox.sense - NIY)',
445     
446 ############################################################
447 class PlcInstance:
448     def __init__ (self, plcbox):
449         self.plc_box=plcbox
450         # unknown yet
451         self.timestamp=0
452         
453     def set_timestamp (self,timestamp): self.timestamp=timestamp
454     def set_now (self): self.timestamp=int(time.time())
455     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
456
457 class PlcVsInstance (PlcInstance):
458     def __init__ (self, plcbox, vservername, ctxid):
459         PlcInstance.__init__(self,plcbox)
460         self.vservername=vservername
461         self.ctxid=ctxid
462
463     def vplcname (self):
464         return self.vservername.split('-')[-1]
465     def buildname (self):
466         return self.vservername.rsplit('-',2)[0]
467
468     def line (self):
469         msg="== %s =="%(self.vplcname())
470         msg += " [=%s]"%self.vservername
471         if self.ctxid==0:  msg+=" not (yet?) running"
472         else:              msg+=" (ctx=%s)"%self.ctxid     
473         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
474         else:              msg += " *unknown timestamp*"
475         return msg
476
477     def kill (self):
478         msg="vserver stopping %s on %s"%(self.vservername,self.plc_box.hostname)
479         self.plc_box.run_ssh(['vserver',self.vservername,'stop'],msg)
480         self.plc_box.forget(self)
481
482 class PlcLxcInstance (PlcInstance):
483     # does lxc have a context id of any kind ?
484     def __init__ (self, plcbox, lxcname, pid):
485         PlcInstance.__init__(self, plcbox)
486         self.lxcname = lxcname
487         self.pid = pid
488
489     def vplcname (self):
490         return self.lxcname.split('-')[-1]
491     def buildname (self):
492         return self.lxcname.rsplit('-',2)[0]
493
494     def line (self):
495         msg="== %s =="%(self.vplcname())
496         msg += " [=%s]"%self.lxcname
497         if self.pid==-1:  msg+=" not (yet?) running"
498         else:              msg+=" (pid=%s)"%self.pid
499         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
500         else:              msg += " *unknown timestamp*"
501         return msg
502
503     def kill (self):
504         command="rsync lxc-driver.sh  %s:/root"%self.plc_box.hostname
505         commands.getstatusoutput(command)
506         msg="lxc container stopping %s on %s"%(self.lxcname,self.plc_box.hostname)
507         self.plc_box.run_ssh(['/root/lxc-driver.sh','-c','stop_lxc','-n',self.lxcname],msg)
508         self.plc_box.forget(self)
509
510 ##########
511 class PlcBox (Box):
512     def __init__ (self, hostname, max_plcs):
513         Box.__init__(self,hostname)
514         self.plc_instances=[]
515         self.max_plcs=max_plcs
516
517     def free_slots (self):
518         return self.max_plcs - len(self.plc_instances)
519
520     # fill one slot even though this one is not started yet
521     def add_dummy (self, plcname):
522         dummy=PlcVsInstance(self,'dummy_'+plcname,0)
523         dummy.set_now()
524         self.plc_instances.append(dummy)
525
526     def forget (self, plc_instance):
527         self.plc_instances.remove(plc_instance)
528
529     def reboot (self, options):
530         if not options.soft:
531             Box.reboot(self,options)
532         else:
533             self.soft_reboot (options)
534
535     def list(self, verbose=False):
536         if not self.plc_instances: 
537             header ('No plc running on %s'%(self.line()))
538         else:
539             header ("Active plc VMs on %s"%self.line())
540             self.plc_instances.sort(timestamp_sort)
541             for p in self.plc_instances: 
542                 header (p.line(),banner=False)
543
544 # we do not this at INRIA any more
545 class PlcVsBox (PlcBox):
546
547     def add_vserver (self,vservername,ctxid):
548         for plc in self.plc_instances:
549             if plc.vservername==vservername: 
550                 header("WARNING, duplicate myplc %s running on %s"%\
551                            (vservername,self.hostname),banner=False)
552                 return
553         self.plc_instances.append(PlcVsInstance(self,vservername,ctxid))
554     
555     def line(self): 
556         msg="%s [max=%d,free=%d] (%s)"%(self.hostname_fedora(virt="vs"), self.max_plcs,self.free_slots(),self.uptime())
557         return msg
558         
559     def plc_instance_by_vservername (self, vservername):
560         for p in self.plc_instances:
561             if p.vservername==vservername: return p
562         return None
563
564     def soft_reboot (self, options):
565         self.run_ssh(['service','util-vserver','stop'],"Stopping all running vservers on %s"%(self.hostname,),
566                      dry_run=options.dry_run)
567
568     def sense (self, options):
569         print 'vp',
570         # try to find fullname (vserver_stat truncates to a ridiculously short name)
571         # fetch the contexts for all vservers on that box
572         map_command=['grep','.','/etc/vservers/*/context','/dev/null',]
573         context_map=self.backquote_ssh (map_command)
574         # at this point we have a set of lines like
575         # /etc/vservers/2010.01.20--k27-f12-32-vplc03/context:40144
576         ctx_dict={}
577         for map_line in context_map.split("\n"):
578             if not map_line: continue
579             [path,xid] = map_line.split(':')
580             ctx_dict[xid]=os.path.basename(os.path.dirname(path))
581         # at this point ctx_id maps context id to vservername
582
583         command=['vserver-stat']
584         vserver_stat = self.backquote_ssh (command)
585         for vserver_line in vserver_stat.split("\n"):
586             if not vserver_line: continue
587             context=vserver_line.split()[0]
588             if context=="CTX": continue
589             try:
590                 longname=ctx_dict[context]
591                 self.add_vserver(longname,context)
592             except:
593                 print 'WARNING: found ctx %s in vserver_stat but was unable to figure a corresp. vserver'%context
594
595         # scan timestamps 
596         running_vsnames = [ i.vservername for i in self.plc_instances ]
597         command=   ['grep','.']
598         command += ['/vservers/%s.timestamp'%vs for vs in running_vsnames]
599         command += ['/dev/null']
600         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
601         for ts_line in ts_lines:
602             if not ts_line.strip(): continue
603             # expect /vservers/<vservername>.timestamp:<timestamp>
604             try:
605                 (ts_file,timestamp)=ts_line.split(':')
606                 ts_file=os.path.basename(ts_file)
607                 (vservername,_)=os.path.splitext(ts_file)
608                 timestamp=int(timestamp)
609                 p=self.plc_instance_by_vservername(vservername)
610                 if not p: 
611                     print 'WARNING zombie plc',self.hostname,ts_line
612                     print '... was expecting',vservername,'in',[i.vservername for i in self.plc_instances]
613                     continue
614                 p.set_timestamp(timestamp)
615             except:  print 'WARNING, could not parse ts line',ts_line
616         
617
618 class PlcLxcBox (PlcBox):
619
620     def add_lxc (self,lxcname,pid):
621         for plc in self.plc_instances:
622             if plc.lxcname==lxcname:
623                 header("WARNING, duplicate myplc %s running on %s"%\
624                            (lxcname,self.hostname),banner=False)
625                 return
626         self.plc_instances.append(PlcLxcInstance(self,lxcname,pid))    
627
628
629     # a line describing the box
630     def line(self): 
631         return "%s [max=%d,free=%d] (%s)"%(self.hostname_fedora(virt="lxc"), 
632                                            self.max_plcs,self.free_slots(),
633                                            self.uptime(),
634                                            )
635     
636     def plc_instance_by_lxcname (self, lxcname):
637         for p in self.plc_instances:
638             if p.lxcname==lxcname: return p
639         return None
640     
641     # essentially shutdown all running containers
642     def soft_reboot (self, options):
643         command="rsync lxc-driver.sh  %s:/root"%self.hostname
644         commands.getstatusoutput(command)
645         self.run_ssh(['/root/lxc-driver.sh','-c','stop_all'],"Stopping all running lxc containers on %s"%(self.hostname,),
646                      dry_run=options.dry_run)
647
648
649     # sense is expected to fill self.plc_instances with PlcLxcInstance's 
650     # to describe the currently running VM's
651     def sense (self, options):
652         print "xp",
653         command="rsync lxc-driver.sh  %s:/root"%self.hostname
654         commands.getstatusoutput(command)
655         command=['/root/lxc-driver.sh','-c','sense_all']
656         lxc_stat = self.backquote_ssh (command)
657         for lxc_line in lxc_stat.split("\n"):
658             if not lxc_line: continue
659             lxcname=lxc_line.split(";")[0]
660             pid=lxc_line.split(";")[1]
661             timestamp=lxc_line.split(";")[2]
662             self.add_lxc(lxcname,pid)
663             try: timestamp=int(timestamp)
664             except: timestamp=0
665             p=self.plc_instance_by_lxcname(lxcname)
666             if not p:
667                 print 'WARNING zombie plc',self.hostname,lxcname
668                 print '... was expecting',lxcname,'in',[i.lxcname for i in self.plc_instances]
669                 continue
670             p.set_timestamp(timestamp)
671
672 ############################################################
673 class QemuInstance: 
674     def __init__ (self, nodename, pid, qemubox):
675         self.nodename=nodename
676         self.pid=pid
677         self.qemu_box=qemubox
678         # not known yet
679         self.buildname=None
680         self.timestamp=0
681         
682     def set_buildname (self,buildname): self.buildname=buildname
683     def set_timestamp (self,timestamp): self.timestamp=timestamp
684     def set_now (self): self.timestamp=int(time.time())
685     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
686     
687     def line (self):
688         msg = "== %s =="%(short_hostname(self.nodename))
689         msg += " [=%s]"%self.buildname
690         if self.pid:       msg += " (pid=%s)"%self.pid
691         else:              msg += " not (yet?) running"
692         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
693         else:              msg += " *unknown timestamp*"
694         return msg
695     
696     def kill(self):
697         if self.pid==0: 
698             print "cannot kill qemu %s with pid==0"%self.nodename
699             return
700         msg="Killing qemu %s with pid=%s on box %s"%(self.nodename,self.pid,self.qemu_box.hostname)
701         self.qemu_box.run_ssh(['kill',"%s"%self.pid],msg)
702         self.qemu_box.forget(self)
703
704
705 class QemuBox (Box):
706     def __init__ (self, hostname, max_qemus):
707         Box.__init__(self,hostname)
708         self.qemu_instances=[]
709         self.max_qemus=max_qemus
710
711     def add_node (self,nodename,pid):
712         for qemu in self.qemu_instances:
713             if qemu.nodename==nodename: 
714                 header("WARNING, duplicate qemu %s running on %s"%\
715                            (nodename,self.hostname), banner=False)
716                 return
717         self.qemu_instances.append(QemuInstance(nodename,pid,self))
718
719     def forget (self, qemu_instance):
720         self.qemu_instances.remove(qemu_instance)
721
722     # fill one slot even though this one is not started yet
723     def add_dummy (self, nodename):
724         dummy=QemuInstance('dummy_'+nodename,0,self)
725         dummy.set_now()
726         self.qemu_instances.append(dummy)
727
728     def line (self):
729         return "%s [max=%d,free=%d] (%s) %s"%(
730             self.hostname_fedora(virt="qemu"), self.max_qemus,self.free_slots(),
731             self.uptime(),self.driver())
732
733     def list(self, verbose=False):
734         if not self.qemu_instances: 
735             header ('No qemu on %s'%(self.line()))
736         else:
737             header ("Qemus on %s"%(self.line()))
738             self.qemu_instances.sort(timestamp_sort)
739             for q in self.qemu_instances: 
740                 header (q.line(),banner=False)
741
742     def free_slots (self):
743         return self.max_qemus - len(self.qemu_instances)
744
745     def driver(self):
746         if hasattr(self,'_driver') and self._driver: return self._driver
747         return '*undef* driver'
748
749     def qemu_instance_by_pid (self,pid):
750         for q in self.qemu_instances:
751             if q.pid==pid: return q
752         return None
753
754     def qemu_instance_by_nodename_buildname (self,nodename,buildname):
755         for q in self.qemu_instances:
756             if q.nodename==nodename and q.buildname==buildname:
757                 return q
758         return None
759
760     def reboot (self, options):
761         if not options.soft:
762             Box.reboot(self,options)
763         else:
764             self.run_ssh(['pkill','qemu'],"Killing qemu instances",
765                          dry_run=options.dry_run)
766
767     matcher=re.compile("\s*(?P<pid>[0-9]+).*-cdrom\s+(?P<nodename>[^\s]+)\.iso")
768     def sense(self, options):
769         print 'qn',
770         modules=self.backquote_ssh(['lsmod']).split('\n')
771         self._driver='*NO kqemu/kvm_intel MODULE LOADED*'
772         for module in modules:
773             if module.find('kqemu')==0:
774                 self._driver='kqemu module loaded'
775             # kvm might be loaded without kvm_intel (we dont have AMD)
776             elif module.find('kvm_intel')==0:
777                 self._driver='kvm_intel OK'
778         ########## find out running pids
779         pids=self.backquote_ssh(['pgrep','qemu'])
780         if not pids: return
781         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
782         ps_lines = self.backquote_ssh (command).split("\n")
783         for line in ps_lines:
784             if not line.strip() or line.find('PID') >=0 : continue
785             m=QemuBox.matcher.match(line)
786             if m: 
787                 self.add_node (m.group('nodename'),m.group('pid'))
788                 continue
789             header('QemuBox.sense: command %r returned line that failed to match'%command)
790             header(">>%s<<"%line)
791         ########## retrieve alive instances and map to build
792         live_builds=[]
793         command=['grep','.','/vservers/*/*/qemu.pid','/dev/null']
794         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
795         for pid_line in pid_lines:
796             if not pid_line.strip(): continue
797             # expect <build>/<nodename>/qemu.pid:<pid>pid
798             try:
799                 (_,__,buildname,nodename,tail)=pid_line.split('/')
800                 (_,pid)=tail.split(':')
801                 q=self.qemu_instance_by_pid (pid)
802                 if not q: continue
803                 q.set_buildname(buildname)
804                 live_builds.append(buildname)
805             except: print 'WARNING, could not parse pid line',pid_line
806         # retrieve timestamps
807         if not live_builds: return
808         command=   ['grep','.']
809         command += ['/vservers/%s/*/timestamp'%b for b in live_builds]
810         command += ['/dev/null']
811         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
812         for ts_line in ts_lines:
813             if not ts_line.strip(): continue
814             # expect <build>/<nodename>/timestamp:<timestamp>
815             try:
816                 (_,__,buildname,nodename,tail)=ts_line.split('/')
817                 nodename=nodename.replace('qemu-','')
818                 (_,timestamp)=tail.split(':')
819                 timestamp=int(timestamp)
820                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
821                 if not q: 
822                     print 'WARNING zombie qemu',self.hostname,ts_line
823                     print '... was expecting (',short_hostname(nodename),buildname,') in',\
824                         [ (short_hostname(i.nodename),i.buildname) for i in self.qemu_instances ]
825                     continue
826                 q.set_timestamp(timestamp)
827             except:  print 'WARNING, could not parse ts line',ts_line
828
829 ####################
830 class TestInstance:
831     def __init__ (self, buildname, pid=0):
832         self.pids=[]
833         if pid!=0: self.pid.append(pid)
834         self.buildname=buildname
835         # latest trace line
836         self.trace=''
837         # has a KO test
838         self.broken_steps=[]
839         self.timestamp = 0
840
841     def set_timestamp (self,timestamp): self.timestamp=timestamp
842     def set_now (self): self.timestamp=int(time.time())
843     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
844
845     def is_running (self): return len(self.pids) != 0
846
847     def add_pid (self,pid):
848         self.pids.append(pid)
849     def set_broken (self, plcindex, step): 
850         self.broken_steps.append ( (plcindex, step,) )
851
852     def line (self):
853         double='=='
854         if self.pids: double='*'+double[1]
855         if self.broken_steps: double=double[0]+'B'
856         msg = " %s %s =="%(double,self.buildname)
857         if not self.pids:       pass
858         elif len(self.pids)==1: msg += " (pid=%s)"%self.pids[0]
859         else:                   msg += " !!!pids=%s!!!"%self.pids
860         msg += " @%s"%self.pretty_timestamp()
861         if self.broken_steps:
862             # sometimes we have an empty plcindex
863             msg += " [BROKEN=" + " ".join( [ "%s@%s"%(s,i) if i else s for (i,s) in self.broken_steps ] ) + "]"
864         return msg
865
866 class TestBox (Box):
867     def __init__ (self,hostname):
868         Box.__init__(self,hostname)
869         self.starting_ips=[]
870         self.test_instances=[]
871
872     def reboot (self, options):
873         # can't reboot a vserver VM
874         self.run_ssh (['pkill','run_log'],"Terminating current runs",
875                       dry_run=options.dry_run)
876         self.run_ssh (['rm','-f',Starting.location],"Cleaning %s"%Starting.location,
877                       dry_run=options.dry_run)
878
879     def get_test (self, buildname):
880         for i in self.test_instances:
881             if i.buildname==buildname: return i
882
883     # we scan ALL remaining test results, even the ones not running
884     def add_timestamp (self, buildname, timestamp):
885         i=self.get_test(buildname)
886         if i:   
887             i.set_timestamp(timestamp)
888         else:   
889             i=TestInstance(buildname,0)
890             i.set_timestamp(timestamp)
891             self.test_instances.append(i)
892
893     def add_running_test (self, pid, buildname):
894         i=self.get_test(buildname)
895         if not i:
896             self.test_instances.append (TestInstance (buildname,pid))
897             return
898         if i.pids:
899             print "WARNING: 2 concurrent tests run on same build %s"%buildname
900         i.add_pid (pid)
901
902     def add_broken (self, buildname, plcindex, step):
903         i=self.get_test(buildname)
904         if not i:
905             i=TestInstance(buildname)
906             self.test_instances.append(i)
907         i.set_broken(plcindex, step)
908
909     matcher_proc=re.compile (".*/proc/(?P<pid>[0-9]+)/cwd.*/root/(?P<buildname>[^/]+)$")
910     matcher_grep=re.compile ("/root/(?P<buildname>[^/]+)/logs/trace.*:TRACE:\s*(?P<plcindex>[0-9]+).*step=(?P<step>\S+).*")
911     matcher_grep_missing=re.compile ("grep: /root/(?P<buildname>[^/]+)/logs/trace: No such file or directory")
912     def sense (self, options):
913         print 'tm',
914         self.starting_ips=[x for x in self.backquote_ssh(['cat',Starting.location], trash_err=True).strip().split('\n') if x]
915
916         # scan timestamps on all tests
917         # this is likely to not invoke ssh so we need to be a bit smarter to get * expanded
918         # xxx would make sense above too
919         command=['bash','-c',"grep . /root/*/timestamp /dev/null"]
920         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
921         for ts_line in ts_lines:
922             if not ts_line.strip(): continue
923             # expect /root/<buildname>/timestamp:<timestamp>
924             try:
925                 (ts_file,timestamp)=ts_line.split(':')
926                 ts_file=os.path.dirname(ts_file)
927                 buildname=os.path.basename(ts_file)
928                 timestamp=int(timestamp)
929                 t=self.add_timestamp(buildname,timestamp)
930             except:  print 'WARNING, could not parse ts line',ts_line
931
932         # let's try to be robust here -- tests that fail very early like e.g.
933         # "Cannot make space for a PLC instance: vplc IP pool exhausted", that occurs as part of provision
934         # will result in a 'trace' symlink to an inexisting 'trace-<>.txt' because no step has gone through
935         # simple 'trace' sohuld exist though as it is created by run_log
936         command=['bash','-c',"grep KO /root/*/logs/trace /dev/null 2>&1" ]
937         trace_lines=self.backquote_ssh (command).split('\n')
938         for line in trace_lines:
939             if not line.strip(): continue
940             m=TestBox.matcher_grep_missing.match(line)
941             if m:
942                 buildname=m.group('buildname')
943                 self.add_broken(buildname,'','NO STEP DONE')
944                 continue
945             m=TestBox.matcher_grep.match(line)
946             if m: 
947                 buildname=m.group('buildname')
948                 plcindex=m.group('plcindex')
949                 step=m.group('step')
950                 self.add_broken(buildname,plcindex, step)
951                 continue
952             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
953             header(">>%s<<"%line)
954
955         pids = self.backquote_ssh (['pgrep','run_log'],trash_err=True)
956         if not pids: return
957         command=['ls','-ld'] + ["/proc/%s/cwd"%pid for pid in pids.split("\n") if pid]
958         ps_lines=self.backquote_ssh (command).split('\n')
959         for line in ps_lines:
960             if not line.strip(): continue
961             m=TestBox.matcher_proc.match(line)
962             if m: 
963                 pid=m.group('pid')
964                 buildname=m.group('buildname')
965                 self.add_running_test(pid, buildname)
966                 continue
967             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
968             header(">>%s<<"%line)
969         
970         
971     def line (self):
972         return self.hostname_fedora()
973
974     def list (self, verbose=False):
975         # verbose shows all tests
976         if verbose:
977             instances = self.test_instances
978             msg="tests"
979         else:
980             instances = [ i for i in self.test_instances if i.is_running() ]
981             msg="running tests"
982
983         if not instances:
984             header ("No %s on %s"%(msg,self.line()))
985         else:
986             header ("%s on %s"%(msg,self.line()))
987             instances.sort(timestamp_sort)
988             for i in instances: print i.line()
989         # show 'starting' regardless of verbose
990         if self.starting_ips:
991             header ("Starting IP addresses on %s"%self.line())
992             self.starting_ips.sort()
993             for starting in self.starting_ips: print starting
994         else:
995             header ("Empty 'starting' on %s"%self.line())
996
997 ############################################################
998 class Options: pass
999
1000 class Substrate:
1001
1002     def __init__ (self, plcs_on_vs=True, plcs_on_lxc=False):
1003         self.options=Options()
1004         self.options.dry_run=False
1005         self.options.verbose=False
1006         self.options.reboot=False
1007         self.options.soft=False
1008         self.test_box = TestBox (self.test_box_spec())
1009         self.build_vs_boxes = [ BuildVsBox(h) for h in self.build_vs_boxes_spec() ]
1010         self.build_lxc_boxes = [ BuildLxcBox(h) for h in self.build_lxc_boxes_spec() ]
1011         self.plc_vs_boxes = [ PlcVsBox (h,m) for (h,m) in self.plc_vs_boxes_spec ()]
1012         self.plc_lxc_boxes = [ PlcLxcBox (h,m) for (h,m) in self.plc_lxc_boxes_spec ()]
1013         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
1014         self._sensed=False
1015
1016         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs",self)
1017         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes",self)
1018         
1019         self.rescope (plcs_on_vs=plcs_on_vs, plcs_on_lxc=plcs_on_lxc)
1020
1021     # which plc boxes are we interested in ?
1022     def rescope (self, plcs_on_vs, plcs_on_lxc):
1023         self.build_boxes = self.build_vs_boxes + self.build_lxc_boxes
1024         self.plc_boxes=[]
1025         if plcs_on_vs: self.plc_boxes += self.plc_vs_boxes
1026         if plcs_on_lxc: self.plc_boxes += self.plc_lxc_boxes
1027         self.default_boxes = self.plc_boxes + self.qemu_boxes
1028         self.all_boxes = self.build_boxes + [ self.test_box ] + self.plc_boxes + self.qemu_boxes
1029
1030     def summary_line (self):
1031         msg  = "["
1032         msg += " %d vp"%len(self.plc_vs_boxes)
1033         msg += " %d xp"%len(self.plc_lxc_boxes)
1034         msg += " %d tried plc boxes"%len(self.plc_boxes)
1035         msg += "]"
1036         return msg
1037
1038     def fqdn (self, hostname):
1039         if hostname.find('.')<0: return "%s.%s"%(hostname,self.domain())
1040         return hostname
1041
1042     # return True if actual sensing takes place
1043     def sense (self,force=False):
1044         if self._sensed and not force: return False
1045         print 'Sensing local substrate...',
1046         for b in self.default_boxes: b.sense(self.options)
1047         print 'Done'
1048         self._sensed=True
1049         return True
1050
1051     def list (self, verbose=False):
1052         for b in self.default_boxes:
1053             b.list()
1054
1055     def add_dummy_plc (self, plc_boxname, plcname):
1056         for pb in self.plc_boxes:
1057             if pb.hostname==plc_boxname:
1058                 pb.add_dummy(plcname)
1059                 return True
1060     def add_dummy_qemu (self, qemu_boxname, qemuname):
1061         for qb in self.qemu_boxes:
1062             if qb.hostname==qemu_boxname:
1063                 qb.add_dummy(qemuname)
1064                 return True
1065
1066     def add_starting_dummy (self, bname, vname):
1067         return self.add_dummy_plc (bname, vname) or self.add_dummy_qemu (bname, vname)
1068
1069     ########## 
1070     def provision (self,plcs,options):
1071         try:
1072             # attach each plc to a plc box and an IP address
1073             plcs = [ self.provision_plc (plc,options) for plc in plcs ]
1074             # attach each node/qemu to a qemu box with an IP address
1075             plcs = [ self.provision_qemus (plc,options) for plc in plcs ]
1076             # update the SFA spec accordingly
1077             plcs = [ self.localize_sfa_rspec(plc,options) for plc in plcs ]
1078             self.list()
1079             return plcs
1080         except Exception, e:
1081             print '* Could not provision this test on current substrate','--',e,'--','exiting'
1082             traceback.print_exc()
1083             sys.exit(1)
1084
1085     # it is expected that a couple of options like ips_bplc and ips_vplc 
1086     # are set or unset together
1087     @staticmethod
1088     def check_options (x,y):
1089         if not x and not y: return True
1090         return len(x)==len(y)
1091
1092     # find an available plc box (or make space)
1093     # and a free IP address (using options if present)
1094     def provision_plc (self, plc, options):
1095         
1096         assert Substrate.check_options (options.ips_bplc, options.ips_vplc)
1097
1098         #### let's find an IP address for that plc
1099         # look in options 
1100         if options.ips_vplc:
1101             # this is a rerun
1102             # we don't check anything here, 
1103             # it is the caller's responsability to cleanup and make sure this makes sense
1104             plc_boxname = options.ips_bplc.pop()
1105             vplc_hostname=options.ips_vplc.pop()
1106         else:
1107             if self.sense(): self.list()
1108             plc_boxname=None
1109             vplc_hostname=None
1110             # try to find an available IP 
1111             self.vplc_pool.sense()
1112             couple=self.vplc_pool.next_free()
1113             if couple:
1114                 (vplc_hostname,unused)=couple
1115             #### we need to find one plc box that still has a slot
1116             max_free=0
1117             # use the box that has max free spots for load balancing
1118             for pb in self.plc_boxes:
1119                 free=pb.free_slots()
1120                 if free>max_free:
1121                     plc_boxname=pb.hostname
1122                     max_free=free
1123             # if there's no available slot in the plc_boxes, or we need a free IP address
1124             # make space by killing the oldest running instance
1125             if not plc_boxname or not vplc_hostname:
1126                 # find the oldest of all our instances
1127                 all_plc_instances=reduce(lambda x, y: x+y, 
1128                                          [ pb.plc_instances for pb in self.plc_boxes ],
1129                                          [])
1130                 all_plc_instances.sort(timestamp_sort)
1131                 try:
1132                     plc_instance_to_kill=all_plc_instances[0]
1133                 except:
1134                     msg=""
1135                     if not plc_boxname: msg += " PLC boxes are full"
1136                     if not vplc_hostname: msg += " vplc IP pool exhausted"
1137                     msg += " %s"%self.summary_line()
1138                     raise Exception,"Cannot make space for a PLC instance:"+msg
1139                 freed_plc_boxname=plc_instance_to_kill.plc_box.hostname
1140                 freed_vplc_hostname=plc_instance_to_kill.vplcname()
1141                 message='killing oldest plc instance = %s on %s'%(plc_instance_to_kill.line(),
1142                                                                   freed_plc_boxname)
1143                 plc_instance_to_kill.kill()
1144                 # use this new plcbox if that was the problem
1145                 if not plc_boxname:
1146                     plc_boxname=freed_plc_boxname
1147                 # ditto for the IP address
1148                 if not vplc_hostname:
1149                     vplc_hostname=freed_vplc_hostname
1150                     # record in pool as mine
1151                     self.vplc_pool.set_mine(vplc_hostname)
1152
1153         # 
1154         self.add_dummy_plc(plc_boxname,plc['name'])
1155         vplc_ip = self.vplc_pool.get_ip(vplc_hostname)
1156         self.vplc_pool.add_starting(vplc_hostname, plc_boxname)
1157
1158         #### compute a helpful vserver name
1159         # remove domain in hostname
1160         vplc_short = short_hostname(vplc_hostname)
1161         vservername = "%s-%d-%s" % (options.buildname,plc['index'],vplc_short)
1162         plc_name = "%s_%s"%(plc['name'],vplc_short)
1163
1164         utils.header( 'PROVISION plc %s in box %s at IP %s as %s'%\
1165                           (plc['name'],plc_boxname,vplc_hostname,vservername))
1166
1167         #### apply in the plc_spec
1168         # # informative
1169         # label=options.personality.replace("linux","")
1170         mapper = {'plc': [ ('*' , {'host_box':plc_boxname,
1171                                    # 'name':'%s-'+label,
1172                                    'name': plc_name,
1173                                    'vservername':vservername,
1174                                    'vserverip':vplc_ip,
1175                                    'PLC_DB_HOST':vplc_hostname,
1176                                    'PLC_API_HOST':vplc_hostname,
1177                                    'PLC_BOOT_HOST':vplc_hostname,
1178                                    'PLC_WWW_HOST':vplc_hostname,
1179                                    'PLC_NET_DNS1' : self.network_settings() [ 'interface_fields:dns1' ],
1180                                    'PLC_NET_DNS2' : self.network_settings() [ 'interface_fields:dns2' ],
1181                                    } ) ]
1182                   }
1183
1184
1185         # mappers only work on a list of plcs
1186         return TestMapper([plc],options).map(mapper)[0]
1187
1188     ##########
1189     def provision_qemus (self, plc, options):
1190
1191         assert Substrate.check_options (options.ips_bnode, options.ips_vnode)
1192
1193         test_mapper = TestMapper ([plc], options)
1194         nodenames = test_mapper.node_names()
1195         maps=[]
1196         for nodename in nodenames:
1197
1198             if options.ips_vnode:
1199                 # as above, it's a rerun, take it for granted
1200                 qemu_boxname=options.ips_bnode.pop()
1201                 vnode_hostname=options.ips_vnode.pop()
1202             else:
1203                 if self.sense(): self.list()
1204                 qemu_boxname=None
1205                 vnode_hostname=None
1206                 # try to find an available IP 
1207                 self.vnode_pool.sense()
1208                 couple=self.vnode_pool.next_free()
1209                 if couple:
1210                     (vnode_hostname,unused)=couple
1211                 # find a physical box
1212                 max_free=0
1213                 # use the box that has max free spots for load balancing
1214                 for qb in self.qemu_boxes:
1215                     free=qb.free_slots()
1216                     if free>max_free:
1217                         qemu_boxname=qb.hostname
1218                         max_free=free
1219                 # if we miss the box or the IP, kill the oldest instance
1220                 if not qemu_boxname or not vnode_hostname:
1221                 # find the oldest of all our instances
1222                     all_qemu_instances=reduce(lambda x, y: x+y, 
1223                                               [ qb.qemu_instances for qb in self.qemu_boxes ],
1224                                               [])
1225                     all_qemu_instances.sort(timestamp_sort)
1226                     try:
1227                         qemu_instance_to_kill=all_qemu_instances[0]
1228                     except:
1229                         msg=""
1230                         if not qemu_boxname: msg += " QEMU boxes are full"
1231                         if not vnode_hostname: msg += " vnode IP pool exhausted" 
1232                         msg += " %s"%self.summary_line()
1233                         raise Exception,"Cannot make space for a QEMU instance:"+msg
1234                     freed_qemu_boxname=qemu_instance_to_kill.qemu_box.hostname
1235                     freed_vnode_hostname=short_hostname(qemu_instance_to_kill.nodename)
1236                     # kill it
1237                     message='killing oldest qemu node = %s on %s'%(qemu_instance_to_kill.line(),
1238                                                                    freed_qemu_boxname)
1239                     qemu_instance_to_kill.kill()
1240                     # use these freed resources where needed
1241                     if not qemu_boxname:
1242                         qemu_boxname=freed_qemu_boxname
1243                     if not vnode_hostname:
1244                         vnode_hostname=freed_vnode_hostname
1245                         self.vnode_pool.set_mine(vnode_hostname)
1246
1247             self.add_dummy_qemu (qemu_boxname,vnode_hostname)
1248             mac=self.vnode_pool.retrieve_userdata(vnode_hostname)
1249             ip=self.vnode_pool.get_ip (vnode_hostname)
1250             self.vnode_pool.add_starting(vnode_hostname,qemu_boxname)
1251
1252             vnode_fqdn = self.fqdn(vnode_hostname)
1253             nodemap={'host_box':qemu_boxname,
1254                      'node_fields:hostname':vnode_fqdn,
1255                      'interface_fields:ip':ip, 
1256                      'ipaddress_fields:ip_addr':ip, 
1257                      'interface_fields:mac':mac,
1258                      }
1259             nodemap.update(self.network_settings())
1260             maps.append ( (nodename, nodemap) )
1261
1262             utils.header("PROVISION node %s in box %s at IP %s with MAC %s"%\
1263                              (nodename,qemu_boxname,vnode_hostname,mac))
1264
1265         return test_mapper.map({'node':maps})[0]
1266
1267     def localize_sfa_rspec (self,plc,options):
1268        
1269         plc['sfa']['SFA_REGISTRY_HOST'] = plc['PLC_DB_HOST']
1270         plc['sfa']['SFA_AGGREGATE_HOST'] = plc['PLC_DB_HOST']
1271         plc['sfa']['SFA_SM_HOST'] = plc['PLC_DB_HOST']
1272         plc['sfa']['SFA_DB_HOST'] = plc['PLC_DB_HOST']
1273         plc['sfa']['SFA_PLC_URL'] = 'https://' + plc['PLC_API_HOST'] + ':443/PLCAPI/' 
1274         return plc
1275
1276     #################### release:
1277     def release (self,options):
1278         self.vplc_pool.release_my_starting()
1279         self.vnode_pool.release_my_starting()
1280         pass
1281
1282     #################### show results for interactive mode
1283     def get_box (self,boxname):
1284         for b in self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box] :
1285             if b.shortname()==boxname:                          return b
1286             try:
1287                 if b.shortname()==boxname.split('.')[0]:        return b
1288             except: pass
1289         print "Could not find box %s"%boxname
1290         return None
1291
1292     def list_boxes(self,box_or_names):
1293         print 'Sensing',
1294         for box in box_or_names:
1295             if not isinstance(box,Box): box=self.get_box(box)
1296             if not box: continue
1297             box.sense(self.options)
1298         print 'Done'
1299         for box in box_or_names:
1300             if not isinstance(box,Box): box=self.get_box(box)
1301             if not box: continue
1302             box.list(self.options.verbose)
1303
1304     def reboot_boxes(self,box_or_names):
1305         for box in box_or_names:
1306             if not isinstance(box,Box): box=self.get_box(box)
1307             if not box: continue
1308             box.reboot(self.options)
1309
1310     ####################
1311     # can be run as a utility to probe/display/manage the local infrastructure
1312     def main (self):
1313         parser=OptionParser()
1314         parser.add_option ('-r',"--reboot",action='store_true',dest='reboot',default=False,
1315                            help='reboot mode (use shutdown -r)')
1316         parser.add_option ('-s',"--soft",action='store_true',dest='soft',default=False,
1317                            help='soft mode for reboot (vserver stop or kill qemus)')
1318         parser.add_option ('-t',"--testbox",action='store_true',dest='testbox',default=False,
1319                            help='add test box') 
1320         parser.add_option ('-b',"--build",action='store_true',dest='builds',default=False,
1321                            help='add build boxes')
1322         parser.add_option ('-p',"--plc",action='store_true',dest='plcs',default=False,
1323                            help='add plc boxes')
1324         parser.add_option ('-q',"--qemu",action='store_true',dest='qemus',default=False,
1325                            help='add qemu boxes') 
1326         parser.add_option ('-a',"--all",action='store_true',dest='all',default=False,
1327                            help='address all known  boxes, like -b -t -p -q')
1328         parser.add_option ('-v',"--verbose",action='store_true',dest='verbose',default=False,
1329                            help='verbose mode')
1330         parser.add_option ('-n',"--dry_run",action='store_true',dest='dry_run',default=False,
1331                            help='dry run mode')
1332         (self.options,args)=parser.parse_args()
1333
1334         self.rescope (plcs_on_vs=True, plcs_on_lxc=True)
1335
1336         boxes=args
1337         if self.options.testbox: boxes += [self.test_box]
1338         if self.options.builds: boxes += self.build_boxes
1339         if self.options.plcs: boxes += self.plc_boxes
1340         if self.options.qemus: boxes += self.qemu_boxes
1341         if self.options.all: boxes += self.all_boxes
1342         
1343         global verbose
1344         verbose=self.options.verbose
1345         # default scope is -b -p -q -t
1346         if not boxes:
1347             boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box]
1348
1349         if self.options.reboot: self.reboot_boxes (boxes)
1350         else:                   self.list_boxes (boxes)