cosmetic changes to manage's output wrt one-line summary of lxc and qemu boxes -...
[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             command=['pkill','vbuild']
406             self.run_ssh(command,"Terminating vbuild processes",dry_run=options.dry_run)
407
408     # inspect box and find currently running builds
409     matcher=re.compile("\s*(?P<pid>[0-9]+).*-[bo]\s+(?P<buildname>[^\s]+)(\s|\Z)")
410     matcher_building_vm=re.compile("\s*(?P<pid>[0-9]+).*init-vserver.*\s+(?P<buildname>[^\s]+)\s*\Z")
411     def sense(self, options):
412         print 'bb',
413         pids=self.backquote_ssh(['pgrep','vbuild'],trash_err=True)
414         if not pids: return
415         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
416         ps_lines=self.backquote_ssh (command).split('\n')
417         for line in ps_lines:
418             if not line.strip() or line.find('PID')>=0: continue
419             m=BuildBox.matcher.match(line)
420             if m: 
421                 date=time.strftime('%Y-%m-%d',time.localtime(time.time()))
422                 buildname=m.group('buildname').replace('@DATE@',date)
423                 self.add_build (buildname,m.group('pid'))
424                 continue
425             m=BuildBox.matcher_building_vm.match(line)
426             if m: 
427                 # buildname is expansed here
428                 self.add_build (buildname,m.group('pid'))
429                 continue
430             header('BuildBox.sense: command %r returned line that failed to match'%command)
431             header(">>%s<<"%line)
432
433 ############################################################
434 class PlcInstance:
435     def __init__ (self, plcbox):
436         self.plc_box=plcbox
437         # unknown yet
438         self.timestamp=0
439         
440     def set_timestamp (self,timestamp): self.timestamp=timestamp
441     def set_now (self): self.timestamp=int(time.time())
442     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
443
444 class PlcVsInstance (PlcInstance):
445     def __init__ (self, plcbox, vservername, ctxid):
446         PlcInstance.__init__(self,plcbox)
447         self.vservername=vservername
448         self.ctxid=ctxid
449
450     def vplcname (self):
451         return self.vservername.split('-')[-1]
452     def buildname (self):
453         return self.vservername.rsplit('-',2)[0]
454
455     def line (self):
456         msg="== %s =="%(self.vplcname())
457         msg += " [=%s]"%self.vservername
458         if self.ctxid==0:  msg+=" not (yet?) running"
459         else:              msg+=" (ctx=%s)"%self.ctxid     
460         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
461         else:              msg += " *unknown timestamp*"
462         return msg
463
464     def kill (self):
465         msg="vserver stopping %s on %s"%(self.vservername,self.plc_box.hostname)
466         self.plc_box.run_ssh(['vserver',self.vservername,'stop'],msg)
467         self.plc_box.forget(self)
468
469 class PlcLxcInstance (PlcInstance):
470     # does lxc have a context id of any kind ?
471     def __init__ (self, plcbox, lxcname, pid):
472         PlcInstance.__init__(self, plcbox)
473         self.lxcname = lxcname
474         self.pid = pid
475
476     def vplcname (self):
477         return self.lxcname.split('-')[-1]
478     def buildname (self):
479         return self.lxcname.rsplit('-',2)[0]
480
481     def line (self):
482         msg="== %s =="%(self.vplcname())
483         msg += " [=%s]"%self.lxcname
484         if self.pid==-1:  msg+=" not (yet?) running"
485         else:              msg+=" (pid=%s)"%self.pid
486         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
487         else:              msg += " *unknown timestamp*"
488         return msg
489
490     def kill (self):
491         command="rsync lxc-driver.sh  %s:/root"%self.plc_box.hostname
492         commands.getstatusoutput(command)
493         msg="lxc container stopping %s on %s"%(self.lxcname,self.plc_box.hostname)
494         self.plc_box.run_ssh(['/root/lxc-driver.sh','-c','stop_lxc','-n',self.lxcname],msg)
495         self.plc_box.forget(self)
496
497 ##########
498 class PlcBox (Box):
499     def __init__ (self, hostname, max_plcs):
500         Box.__init__(self,hostname)
501         self.plc_instances=[]
502         self.max_plcs=max_plcs
503
504     def free_slots (self):
505         return self.max_plcs - len(self.plc_instances)
506
507     # fill one slot even though this one is not started yet
508     def add_dummy (self, plcname):
509         dummy=PlcVsInstance(self,'dummy_'+plcname,0)
510         dummy.set_now()
511         self.plc_instances.append(dummy)
512
513     def forget (self, plc_instance):
514         self.plc_instances.remove(plc_instance)
515
516     def reboot (self, options):
517         if not options.soft:
518             Box.reboot(self,options)
519         else:
520             self.soft_reboot (options)
521
522     def list(self, verbose=False):
523         if not self.plc_instances: 
524             header ('No plc running on %s'%(self.line()))
525         else:
526             header ("Active plc VMs on %s"%self.line())
527             self.plc_instances.sort(timestamp_sort)
528             for p in self.plc_instances: 
529                 header (p.line(),banner=False)
530
531 # we do not this at INRIA any more
532 class PlcVsBox (PlcBox):
533
534     def add_vserver (self,vservername,ctxid):
535         for plc in self.plc_instances:
536             if plc.vservername==vservername: 
537                 header("WARNING, duplicate myplc %s running on %s"%\
538                            (vservername,self.hostname),banner=False)
539                 return
540         self.plc_instances.append(PlcVsInstance(self,vservername,ctxid))
541     
542     def line(self): 
543         msg="%s [max=%d,free=%d] (%s)"%(self.hostname_fedora(virt="vs"), self.max_plcs,self.free_slots(),self.uptime())
544         return msg
545         
546     def plc_instance_by_vservername (self, vservername):
547         for p in self.plc_instances:
548             if p.vservername==vservername: return p
549         return None
550
551     def soft_reboot (self, options):
552         self.run_ssh(['service','util-vserver','stop'],"Stopping all running vservers on %s"%(self.hostname,),
553                      dry_run=options.dry_run)
554
555     def sense (self, options):
556         print 'vp',
557         # try to find fullname (vserver_stat truncates to a ridiculously short name)
558         # fetch the contexts for all vservers on that box
559         map_command=['grep','.','/etc/vservers/*/context','/dev/null',]
560         context_map=self.backquote_ssh (map_command)
561         # at this point we have a set of lines like
562         # /etc/vservers/2010.01.20--k27-f12-32-vplc03/context:40144
563         ctx_dict={}
564         for map_line in context_map.split("\n"):
565             if not map_line: continue
566             [path,xid] = map_line.split(':')
567             ctx_dict[xid]=os.path.basename(os.path.dirname(path))
568         # at this point ctx_id maps context id to vservername
569
570         command=['vserver-stat']
571         vserver_stat = self.backquote_ssh (command)
572         for vserver_line in vserver_stat.split("\n"):
573             if not vserver_line: continue
574             context=vserver_line.split()[0]
575             if context=="CTX": continue
576             try:
577                 longname=ctx_dict[context]
578                 self.add_vserver(longname,context)
579             except:
580                 print 'WARNING: found ctx %s in vserver_stat but was unable to figure a corresp. vserver'%context
581
582         # scan timestamps 
583         running_vsnames = [ i.vservername for i in self.plc_instances ]
584         command=   ['grep','.']
585         command += ['/vservers/%s.timestamp'%vs for vs in running_vsnames]
586         command += ['/dev/null']
587         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
588         for ts_line in ts_lines:
589             if not ts_line.strip(): continue
590             # expect /vservers/<vservername>.timestamp:<timestamp>
591             try:
592                 (ts_file,timestamp)=ts_line.split(':')
593                 ts_file=os.path.basename(ts_file)
594                 (vservername,_)=os.path.splitext(ts_file)
595                 timestamp=int(timestamp)
596                 p=self.plc_instance_by_vservername(vservername)
597                 if not p: 
598                     print 'WARNING zombie plc',self.hostname,ts_line
599                     print '... was expecting',vservername,'in',[i.vservername for i in self.plc_instances]
600                     continue
601                 p.set_timestamp(timestamp)
602             except:  print 'WARNING, could not parse ts line',ts_line
603         
604
605 class PlcLxcBox (PlcBox):
606
607     def add_lxc (self,lxcname,pid):
608         for plc in self.plc_instances:
609             if plc.lxcname==lxcname:
610                 header("WARNING, duplicate myplc %s running on %s"%\
611                            (lxcname,self.hostname),banner=False)
612                 return
613         self.plc_instances.append(PlcLxcInstance(self,lxcname,pid))    
614
615
616     # a line describing the box
617     def line(self): 
618         return "%s [max=%d,free=%d] (%s)"%(self.hostname_fedora(virt="lxc"), 
619                                            self.max_plcs,self.free_slots(),
620                                            self.uptime(),
621                                            )
622     
623     def plc_instance_by_lxcname (self, lxcname):
624         for p in self.plc_instances:
625             if p.lxcname==lxcname: return p
626         return None
627     
628     # essentially shutdown all running containers
629     def soft_reboot (self, options):
630         command="rsync lxc-driver.sh  %s:/root"%self.hostname
631         commands.getstatusoutput(command)
632         self.run_ssh(['/root/lxc-driver.sh','-c','stop_all'],"Stopping all running lxc containers on %s"%(self.hostname,),
633                      dry_run=options.dry_run)
634
635
636     # sense is expected to fill self.plc_instances with PlcLxcInstance's 
637     # to describe the currently running VM's
638     def sense (self, options):
639         print "xp",
640         command="rsync lxc-driver.sh  %s:/root"%self.hostname
641         commands.getstatusoutput(command)
642         command=['/root/lxc-driver.sh','-c','sense_all']
643         lxc_stat = self.backquote_ssh (command)
644         for lxc_line in lxc_stat.split("\n"):
645             if not lxc_line: continue
646             lxcname=lxc_line.split(";")[0]
647             pid=lxc_line.split(";")[1]
648             timestamp=lxc_line.split(";")[2]
649             self.add_lxc(lxcname,pid)
650             try: timestamp=int(timestamp)
651             except: timestamp=0
652             p=self.plc_instance_by_lxcname(lxcname)
653             if not p:
654                 print 'WARNING zombie plc',self.hostname,lxcname
655                 print '... was expecting',lxcname,'in',[i.lxcname for i in self.plc_instances]
656                 continue
657             p.set_timestamp(timestamp)
658
659 ############################################################
660 class QemuInstance: 
661     def __init__ (self, nodename, pid, qemubox):
662         self.nodename=nodename
663         self.pid=pid
664         self.qemu_box=qemubox
665         # not known yet
666         self.buildname=None
667         self.timestamp=0
668         
669     def set_buildname (self,buildname): self.buildname=buildname
670     def set_timestamp (self,timestamp): self.timestamp=timestamp
671     def set_now (self): self.timestamp=int(time.time())
672     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
673     
674     def line (self):
675         msg = "== %s =="%(short_hostname(self.nodename))
676         msg += " [=%s]"%self.buildname
677         if self.pid:       msg += " (pid=%s)"%self.pid
678         else:              msg += " not (yet?) running"
679         if self.timestamp: msg += " @ %s"%self.pretty_timestamp()
680         else:              msg += " *unknown timestamp*"
681         return msg
682     
683     def kill(self):
684         if self.pid==0: 
685             print "cannot kill qemu %s with pid==0"%self.nodename
686             return
687         msg="Killing qemu %s with pid=%s on box %s"%(self.nodename,self.pid,self.qemu_box.hostname)
688         self.qemu_box.run_ssh(['kill',"%s"%self.pid],msg)
689         self.qemu_box.forget(self)
690
691
692 class QemuBox (Box):
693     def __init__ (self, hostname, max_qemus):
694         Box.__init__(self,hostname)
695         self.qemu_instances=[]
696         self.max_qemus=max_qemus
697
698     def add_node (self,nodename,pid):
699         for qemu in self.qemu_instances:
700             if qemu.nodename==nodename: 
701                 header("WARNING, duplicate qemu %s running on %s"%\
702                            (nodename,self.hostname), banner=False)
703                 return
704         self.qemu_instances.append(QemuInstance(nodename,pid,self))
705
706     def forget (self, qemu_instance):
707         self.qemu_instances.remove(qemu_instance)
708
709     # fill one slot even though this one is not started yet
710     def add_dummy (self, nodename):
711         dummy=QemuInstance('dummy_'+nodename,0,self)
712         dummy.set_now()
713         self.qemu_instances.append(dummy)
714
715     def line (self):
716         return "%s [max=%d,free=%d] (%s) %s"%(
717             self.hostname_fedora(virt="qemu"), self.max_qemus,self.free_slots(),
718             self.uptime(),self.driver())
719
720     def list(self, verbose=False):
721         if not self.qemu_instances: 
722             header ('No qemu on %s'%(self.line()))
723         else:
724             header ("Qemus on %s"%(self.line()))
725             self.qemu_instances.sort(timestamp_sort)
726             for q in self.qemu_instances: 
727                 header (q.line(),banner=False)
728
729     def free_slots (self):
730         return self.max_qemus - len(self.qemu_instances)
731
732     def driver(self):
733         if hasattr(self,'_driver') and self._driver: return self._driver
734         return '*undef* driver'
735
736     def qemu_instance_by_pid (self,pid):
737         for q in self.qemu_instances:
738             if q.pid==pid: return q
739         return None
740
741     def qemu_instance_by_nodename_buildname (self,nodename,buildname):
742         for q in self.qemu_instances:
743             if q.nodename==nodename and q.buildname==buildname:
744                 return q
745         return None
746
747     def reboot (self, options):
748         if not options.soft:
749             Box.reboot(self,options)
750         else:
751             self.run_ssh(['pkill','qemu'],"Killing qemu instances",
752                          dry_run=options.dry_run)
753
754     matcher=re.compile("\s*(?P<pid>[0-9]+).*-cdrom\s+(?P<nodename>[^\s]+)\.iso")
755     def sense(self, options):
756         print 'qn',
757         modules=self.backquote_ssh(['lsmod']).split('\n')
758         self._driver='*NO kqemu/kvm_intel MODULE LOADED*'
759         for module in modules:
760             if module.find('kqemu')==0:
761                 self._driver='kqemu module loaded'
762             # kvm might be loaded without kvm_intel (we dont have AMD)
763             elif module.find('kvm_intel')==0:
764                 self._driver='kvm_intel OK'
765         ########## find out running pids
766         pids=self.backquote_ssh(['pgrep','qemu'])
767         if not pids: return
768         command=['ps','-o','pid,command'] + [ pid for pid in pids.split("\n") if pid]
769         ps_lines = self.backquote_ssh (command).split("\n")
770         for line in ps_lines:
771             if not line.strip() or line.find('PID') >=0 : continue
772             m=QemuBox.matcher.match(line)
773             if m: 
774                 self.add_node (m.group('nodename'),m.group('pid'))
775                 continue
776             header('QemuBox.sense: command %r returned line that failed to match'%command)
777             header(">>%s<<"%line)
778         ########## retrieve alive instances and map to build
779         live_builds=[]
780         command=['grep','.','/vservers/*/*/qemu.pid','/dev/null']
781         pid_lines=self.backquote_ssh(command,trash_err=True).split('\n')
782         for pid_line in pid_lines:
783             if not pid_line.strip(): continue
784             # expect <build>/<nodename>/qemu.pid:<pid>pid
785             try:
786                 (_,__,buildname,nodename,tail)=pid_line.split('/')
787                 (_,pid)=tail.split(':')
788                 q=self.qemu_instance_by_pid (pid)
789                 if not q: continue
790                 q.set_buildname(buildname)
791                 live_builds.append(buildname)
792             except: print 'WARNING, could not parse pid line',pid_line
793         # retrieve timestamps
794         if not live_builds: return
795         command=   ['grep','.']
796         command += ['/vservers/%s/*/timestamp'%b for b in live_builds]
797         command += ['/dev/null']
798         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
799         for ts_line in ts_lines:
800             if not ts_line.strip(): continue
801             # expect <build>/<nodename>/timestamp:<timestamp>
802             try:
803                 (_,__,buildname,nodename,tail)=ts_line.split('/')
804                 nodename=nodename.replace('qemu-','')
805                 (_,timestamp)=tail.split(':')
806                 timestamp=int(timestamp)
807                 q=self.qemu_instance_by_nodename_buildname(nodename,buildname)
808                 if not q: 
809                     print 'WARNING zombie qemu',self.hostname,ts_line
810                     print '... was expecting (',short_hostname(nodename),buildname,') in',\
811                         [ (short_hostname(i.nodename),i.buildname) for i in self.qemu_instances ]
812                     continue
813                 q.set_timestamp(timestamp)
814             except:  print 'WARNING, could not parse ts line',ts_line
815
816 ####################
817 class TestInstance:
818     def __init__ (self, buildname, pid=0):
819         self.pids=[]
820         if pid!=0: self.pid.append(pid)
821         self.buildname=buildname
822         # latest trace line
823         self.trace=''
824         # has a KO test
825         self.broken_steps=[]
826         self.timestamp = 0
827
828     def set_timestamp (self,timestamp): self.timestamp=timestamp
829     def set_now (self): self.timestamp=int(time.time())
830     def pretty_timestamp (self): return time.strftime("%Y-%m-%d:%H-%M",time.localtime(self.timestamp))
831
832     def is_running (self): return len(self.pids) != 0
833
834     def add_pid (self,pid):
835         self.pids.append(pid)
836     def set_broken (self, plcindex, step): 
837         self.broken_steps.append ( (plcindex, step,) )
838
839     def line (self):
840         double='=='
841         if self.pids: double='*'+double[1]
842         if self.broken_steps: double=double[0]+'B'
843         msg = " %s %s =="%(double,self.buildname)
844         if not self.pids:       pass
845         elif len(self.pids)==1: msg += " (pid=%s)"%self.pids[0]
846         else:                   msg += " !!!pids=%s!!!"%self.pids
847         msg += " @%s"%self.pretty_timestamp()
848         if self.broken_steps:
849             # sometimes we have an empty plcindex
850             msg += " [BROKEN=" + " ".join( [ "%s@%s"%(s,i) if i else s for (i,s) in self.broken_steps ] ) + "]"
851         return msg
852
853 class TestBox (Box):
854     def __init__ (self,hostname):
855         Box.__init__(self,hostname)
856         self.starting_ips=[]
857         self.test_instances=[]
858
859     def reboot (self, options):
860         # can't reboot a vserver VM
861         self.run_ssh (['pkill','run_log'],"Terminating current runs",
862                       dry_run=options.dry_run)
863         self.run_ssh (['rm','-f',Starting.location],"Cleaning %s"%Starting.location,
864                       dry_run=options.dry_run)
865
866     def get_test (self, buildname):
867         for i in self.test_instances:
868             if i.buildname==buildname: return i
869
870     # we scan ALL remaining test results, even the ones not running
871     def add_timestamp (self, buildname, timestamp):
872         i=self.get_test(buildname)
873         if i:   
874             i.set_timestamp(timestamp)
875         else:   
876             i=TestInstance(buildname,0)
877             i.set_timestamp(timestamp)
878             self.test_instances.append(i)
879
880     def add_running_test (self, pid, buildname):
881         i=self.get_test(buildname)
882         if not i:
883             self.test_instances.append (TestInstance (buildname,pid))
884             return
885         if i.pids:
886             print "WARNING: 2 concurrent tests run on same build %s"%buildname
887         i.add_pid (pid)
888
889     def add_broken (self, buildname, plcindex, step):
890         i=self.get_test(buildname)
891         if not i:
892             i=TestInstance(buildname)
893             self.test_instances.append(i)
894         i.set_broken(plcindex, step)
895
896     matcher_proc=re.compile (".*/proc/(?P<pid>[0-9]+)/cwd.*/root/(?P<buildname>[^/]+)$")
897     matcher_grep=re.compile ("/root/(?P<buildname>[^/]+)/logs/trace.*:TRACE:\s*(?P<plcindex>[0-9]+).*step=(?P<step>\S+).*")
898     matcher_grep_missing=re.compile ("grep: /root/(?P<buildname>[^/]+)/logs/trace: No such file or directory")
899     def sense (self, options):
900         print 'tm',
901         self.starting_ips=[x for x in self.backquote_ssh(['cat',Starting.location], trash_err=True).strip().split('\n') if x]
902
903         # scan timestamps on all tests
904         # this is likely to not invoke ssh so we need to be a bit smarter to get * expanded
905         # xxx would make sense above too
906         command=['bash','-c',"grep . /root/*/timestamp /dev/null"]
907         ts_lines=self.backquote_ssh(command,trash_err=True).split('\n')
908         for ts_line in ts_lines:
909             if not ts_line.strip(): continue
910             # expect /root/<buildname>/timestamp:<timestamp>
911             try:
912                 (ts_file,timestamp)=ts_line.split(':')
913                 ts_file=os.path.dirname(ts_file)
914                 buildname=os.path.basename(ts_file)
915                 timestamp=int(timestamp)
916                 t=self.add_timestamp(buildname,timestamp)
917             except:  print 'WARNING, could not parse ts line',ts_line
918
919         # let's try to be robust here -- tests that fail very early like e.g.
920         # "Cannot make space for a PLC instance: vplc IP pool exhausted", that occurs as part of provision
921         # will result in a 'trace' symlink to an inexisting 'trace-<>.txt' because no step has gone through
922         # simple 'trace' sohuld exist though as it is created by run_log
923         command=['bash','-c',"grep KO /root/*/logs/trace /dev/null 2>&1" ]
924         trace_lines=self.backquote_ssh (command).split('\n')
925         for line in trace_lines:
926             if not line.strip(): continue
927             m=TestBox.matcher_grep_missing.match(line)
928             if m:
929                 buildname=m.group('buildname')
930                 self.add_broken(buildname,'','NO STEP DONE')
931                 continue
932             m=TestBox.matcher_grep.match(line)
933             if m: 
934                 buildname=m.group('buildname')
935                 plcindex=m.group('plcindex')
936                 step=m.group('step')
937                 self.add_broken(buildname,plcindex, step)
938                 continue
939             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
940             header(">>%s<<"%line)
941
942         pids = self.backquote_ssh (['pgrep','run_log'],trash_err=True)
943         if not pids: return
944         command=['ls','-ld'] + ["/proc/%s/cwd"%pid for pid in pids.split("\n") if pid]
945         ps_lines=self.backquote_ssh (command).split('\n')
946         for line in ps_lines:
947             if not line.strip(): continue
948             m=TestBox.matcher_proc.match(line)
949             if m: 
950                 pid=m.group('pid')
951                 buildname=m.group('buildname')
952                 self.add_running_test(pid, buildname)
953                 continue
954             header("TestBox.sense: command %r returned line that failed to match\n%s"%(command,line))
955             header(">>%s<<"%line)
956         
957         
958     def line (self):
959         return self.hostname_fedora()
960
961     def list (self, verbose=False):
962         # verbose shows all tests
963         if verbose:
964             instances = self.test_instances
965             msg="tests"
966         else:
967             instances = [ i for i in self.test_instances if i.is_running() ]
968             msg="running tests"
969
970         if not instances:
971             header ("No %s on %s"%(msg,self.line()))
972         else:
973             header ("%s on %s"%(msg,self.line()))
974             instances.sort(timestamp_sort)
975             for i in instances: print i.line()
976         # show 'starting' regardless of verbose
977         if self.starting_ips:
978             header ("Starting IP addresses on %s"%self.line())
979             self.starting_ips.sort()
980             for starting in self.starting_ips: print starting
981         else:
982             header ("Empty 'starting' on %s"%self.line())
983
984 ############################################################
985 class Options: pass
986
987 class Substrate:
988
989     def __init__ (self, plcs_on_vs=True, plcs_on_lxc=False):
990         self.options=Options()
991         self.options.dry_run=False
992         self.options.verbose=False
993         self.options.reboot=False
994         self.options.soft=False
995         self.test_box = TestBox (self.test_box_spec())
996         self.build_boxes = [ BuildBox(h) for h in self.build_boxes_spec() ]
997         # for compat with older LocalSubstrate
998         try:
999             self.plc_vs_boxes = [ PlcVsBox (h,m) for (h,m) in self.plc_vs_boxes_spec ()]
1000             self.plc_lxc_boxes = [ PlcLxcBox (h,m) for (h,m) in self.plc_lxc_boxes_spec ()]
1001         except:
1002             self.plc_vs_boxes = [ PlcVsBox (h,m) for (h,m) in self.plc_boxes_spec ()]
1003             self.plc_lxc_boxes = [ ]
1004         self.qemu_boxes = [ QemuBox (h,m) for (h,m) in self.qemu_boxes_spec ()]
1005         self._sensed=False
1006
1007         self.vplc_pool = Pool (self.vplc_ips(),"for vplcs",self)
1008         self.vnode_pool = Pool (self.vnode_ips(),"for vnodes",self)
1009         
1010         self.rescope (plcs_on_vs=plcs_on_vs, plcs_on_lxc=plcs_on_lxc)
1011
1012     # which plc boxes are we interested in ?
1013     def rescope (self, plcs_on_vs, plcs_on_lxc):
1014         self.plc_boxes=[]
1015         if plcs_on_vs: self.plc_boxes += self.plc_vs_boxes
1016         if plcs_on_lxc: self.plc_boxes += self.plc_lxc_boxes
1017         self.default_boxes = self.plc_boxes + self.qemu_boxes
1018         self.all_boxes = self.build_boxes + [ self.test_box ] + self.plc_boxes + self.qemu_boxes
1019
1020     def summary_line (self):
1021         msg  = "["
1022         msg += " %d vp"%len(self.plc_vs_boxes)
1023         msg += " %d xp"%len(self.plc_lxc_boxes)
1024         msg += " %d tried plc boxes"%len(self.plc_boxes)
1025         msg += "]"
1026         return msg
1027
1028     def fqdn (self, hostname):
1029         if hostname.find('.')<0: return "%s.%s"%(hostname,self.domain())
1030         return hostname
1031
1032     # return True if actual sensing takes place
1033     def sense (self,force=False):
1034         if self._sensed and not force: return False
1035         print 'Sensing local substrate...',
1036         for b in self.default_boxes: b.sense(self.options)
1037         print 'Done'
1038         self._sensed=True
1039         return True
1040
1041     def list (self, verbose=False):
1042         for b in self.default_boxes:
1043             b.list()
1044
1045     def add_dummy_plc (self, plc_boxname, plcname):
1046         for pb in self.plc_boxes:
1047             if pb.hostname==plc_boxname:
1048                 pb.add_dummy(plcname)
1049                 return True
1050     def add_dummy_qemu (self, qemu_boxname, qemuname):
1051         for qb in self.qemu_boxes:
1052             if qb.hostname==qemu_boxname:
1053                 qb.add_dummy(qemuname)
1054                 return True
1055
1056     def add_starting_dummy (self, bname, vname):
1057         return self.add_dummy_plc (bname, vname) or self.add_dummy_qemu (bname, vname)
1058
1059     ########## 
1060     def provision (self,plcs,options):
1061         try:
1062             # attach each plc to a plc box and an IP address
1063             plcs = [ self.provision_plc (plc,options) for plc in plcs ]
1064             # attach each node/qemu to a qemu box with an IP address
1065             plcs = [ self.provision_qemus (plc,options) for plc in plcs ]
1066             # update the SFA spec accordingly
1067             plcs = [ self.localize_sfa_rspec(plc,options) for plc in plcs ]
1068             self.list()
1069             return plcs
1070         except Exception, e:
1071             print '* Could not provision this test on current substrate','--',e,'--','exiting'
1072             traceback.print_exc()
1073             sys.exit(1)
1074
1075     # it is expected that a couple of options like ips_bplc and ips_vplc 
1076     # are set or unset together
1077     @staticmethod
1078     def check_options (x,y):
1079         if not x and not y: return True
1080         return len(x)==len(y)
1081
1082     # find an available plc box (or make space)
1083     # and a free IP address (using options if present)
1084     def provision_plc (self, plc, options):
1085         
1086         assert Substrate.check_options (options.ips_bplc, options.ips_vplc)
1087
1088         #### let's find an IP address for that plc
1089         # look in options 
1090         if options.ips_vplc:
1091             # this is a rerun
1092             # we don't check anything here, 
1093             # it is the caller's responsability to cleanup and make sure this makes sense
1094             plc_boxname = options.ips_bplc.pop()
1095             vplc_hostname=options.ips_vplc.pop()
1096         else:
1097             if self.sense(): self.list()
1098             plc_boxname=None
1099             vplc_hostname=None
1100             # try to find an available IP 
1101             self.vplc_pool.sense()
1102             couple=self.vplc_pool.next_free()
1103             if couple:
1104                 (vplc_hostname,unused)=couple
1105             #### we need to find one plc box that still has a slot
1106             max_free=0
1107             # use the box that has max free spots for load balancing
1108             for pb in self.plc_boxes:
1109                 free=pb.free_slots()
1110                 if free>max_free:
1111                     plc_boxname=pb.hostname
1112                     max_free=free
1113             # if there's no available slot in the plc_boxes, or we need a free IP address
1114             # make space by killing the oldest running instance
1115             if not plc_boxname or not vplc_hostname:
1116                 # find the oldest of all our instances
1117                 all_plc_instances=reduce(lambda x, y: x+y, 
1118                                          [ pb.plc_instances for pb in self.plc_boxes ],
1119                                          [])
1120                 all_plc_instances.sort(timestamp_sort)
1121                 try:
1122                     plc_instance_to_kill=all_plc_instances[0]
1123                 except:
1124                     msg=""
1125                     if not plc_boxname: msg += " PLC boxes are full"
1126                     if not vplc_hostname: msg += " vplc IP pool exhausted"
1127                     msg += " %s"%self.summary_line()
1128                     raise Exception,"Cannot make space for a PLC instance:"+msg
1129                 freed_plc_boxname=plc_instance_to_kill.plc_box.hostname
1130                 freed_vplc_hostname=plc_instance_to_kill.vplcname()
1131                 message='killing oldest plc instance = %s on %s'%(plc_instance_to_kill.line(),
1132                                                                   freed_plc_boxname)
1133                 plc_instance_to_kill.kill()
1134                 # use this new plcbox if that was the problem
1135                 if not plc_boxname:
1136                     plc_boxname=freed_plc_boxname
1137                 # ditto for the IP address
1138                 if not vplc_hostname:
1139                     vplc_hostname=freed_vplc_hostname
1140                     # record in pool as mine
1141                     self.vplc_pool.set_mine(vplc_hostname)
1142
1143         # 
1144         self.add_dummy_plc(plc_boxname,plc['name'])
1145         vplc_ip = self.vplc_pool.get_ip(vplc_hostname)
1146         self.vplc_pool.add_starting(vplc_hostname, plc_boxname)
1147
1148         #### compute a helpful vserver name
1149         # remove domain in hostname
1150         vplc_short = short_hostname(vplc_hostname)
1151         vservername = "%s-%d-%s" % (options.buildname,plc['index'],vplc_short)
1152         plc_name = "%s_%s"%(plc['name'],vplc_short)
1153
1154         utils.header( 'PROVISION plc %s in box %s at IP %s as %s'%\
1155                           (plc['name'],plc_boxname,vplc_hostname,vservername))
1156
1157         #### apply in the plc_spec
1158         # # informative
1159         # label=options.personality.replace("linux","")
1160         mapper = {'plc': [ ('*' , {'host_box':plc_boxname,
1161                                    # 'name':'%s-'+label,
1162                                    'name': plc_name,
1163                                    'vservername':vservername,
1164                                    'vserverip':vplc_ip,
1165                                    'PLC_DB_HOST':vplc_hostname,
1166                                    'PLC_API_HOST':vplc_hostname,
1167                                    'PLC_BOOT_HOST':vplc_hostname,
1168                                    'PLC_WWW_HOST':vplc_hostname,
1169                                    'PLC_NET_DNS1' : self.network_settings() [ 'interface_fields:dns1' ],
1170                                    'PLC_NET_DNS2' : self.network_settings() [ 'interface_fields:dns2' ],
1171                                    } ) ]
1172                   }
1173
1174
1175         # mappers only work on a list of plcs
1176         return TestMapper([plc],options).map(mapper)[0]
1177
1178     ##########
1179     def provision_qemus (self, plc, options):
1180
1181         assert Substrate.check_options (options.ips_bnode, options.ips_vnode)
1182
1183         test_mapper = TestMapper ([plc], options)
1184         nodenames = test_mapper.node_names()
1185         maps=[]
1186         for nodename in nodenames:
1187
1188             if options.ips_vnode:
1189                 # as above, it's a rerun, take it for granted
1190                 qemu_boxname=options.ips_bnode.pop()
1191                 vnode_hostname=options.ips_vnode.pop()
1192             else:
1193                 if self.sense(): self.list()
1194                 qemu_boxname=None
1195                 vnode_hostname=None
1196                 # try to find an available IP 
1197                 self.vnode_pool.sense()
1198                 couple=self.vnode_pool.next_free()
1199                 if couple:
1200                     (vnode_hostname,unused)=couple
1201                 # find a physical box
1202                 max_free=0
1203                 # use the box that has max free spots for load balancing
1204                 for qb in self.qemu_boxes:
1205                     free=qb.free_slots()
1206                     if free>max_free:
1207                         qemu_boxname=qb.hostname
1208                         max_free=free
1209                 # if we miss the box or the IP, kill the oldest instance
1210                 if not qemu_boxname or not vnode_hostname:
1211                 # find the oldest of all our instances
1212                     all_qemu_instances=reduce(lambda x, y: x+y, 
1213                                               [ qb.qemu_instances for qb in self.qemu_boxes ],
1214                                               [])
1215                     all_qemu_instances.sort(timestamp_sort)
1216                     try:
1217                         qemu_instance_to_kill=all_qemu_instances[0]
1218                     except:
1219                         msg=""
1220                         if not qemu_boxname: msg += " QEMU boxes are full"
1221                         if not vnode_hostname: msg += " vnode IP pool exhausted" 
1222                         msg += " %s"%self.summary_line()
1223                         raise Exception,"Cannot make space for a QEMU instance:"+msg
1224                     freed_qemu_boxname=qemu_instance_to_kill.qemu_box.hostname
1225                     freed_vnode_hostname=short_hostname(qemu_instance_to_kill.nodename)
1226                     # kill it
1227                     message='killing oldest qemu node = %s on %s'%(qemu_instance_to_kill.line(),
1228                                                                    freed_qemu_boxname)
1229                     qemu_instance_to_kill.kill()
1230                     # use these freed resources where needed
1231                     if not qemu_boxname:
1232                         qemu_boxname=freed_qemu_boxname
1233                     if not vnode_hostname:
1234                         vnode_hostname=freed_vnode_hostname
1235                         self.vnode_pool.set_mine(vnode_hostname)
1236
1237             self.add_dummy_qemu (qemu_boxname,vnode_hostname)
1238             mac=self.vnode_pool.retrieve_userdata(vnode_hostname)
1239             ip=self.vnode_pool.get_ip (vnode_hostname)
1240             self.vnode_pool.add_starting(vnode_hostname,qemu_boxname)
1241
1242             vnode_fqdn = self.fqdn(vnode_hostname)
1243             nodemap={'host_box':qemu_boxname,
1244                      'node_fields:hostname':vnode_fqdn,
1245                      'interface_fields:ip':ip, 
1246                      'ipaddress_fields:ip_addr':ip, 
1247                      'interface_fields:mac':mac,
1248                      }
1249             nodemap.update(self.network_settings())
1250             maps.append ( (nodename, nodemap) )
1251
1252             utils.header("PROVISION node %s in box %s at IP %s with MAC %s"%\
1253                              (nodename,qemu_boxname,vnode_hostname,mac))
1254
1255         return test_mapper.map({'node':maps})[0]
1256
1257     def localize_sfa_rspec (self,plc,options):
1258        
1259         plc['sfa']['SFA_REGISTRY_HOST'] = plc['PLC_DB_HOST']
1260         plc['sfa']['SFA_AGGREGATE_HOST'] = plc['PLC_DB_HOST']
1261         plc['sfa']['SFA_SM_HOST'] = plc['PLC_DB_HOST']
1262         plc['sfa']['SFA_DB_HOST'] = plc['PLC_DB_HOST']
1263         plc['sfa']['SFA_PLC_URL'] = 'https://' + plc['PLC_API_HOST'] + ':443/PLCAPI/' 
1264         return plc
1265
1266     #################### release:
1267     def release (self,options):
1268         self.vplc_pool.release_my_starting()
1269         self.vnode_pool.release_my_starting()
1270         pass
1271
1272     #################### show results for interactive mode
1273     def get_box (self,boxname):
1274         for b in self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box] :
1275             if b.shortname()==boxname:                          return b
1276             try:
1277                 if b.shortname()==boxname.split('.')[0]:        return b
1278             except: pass
1279         print "Could not find box %s"%boxname
1280         return None
1281
1282     def list_boxes(self,box_or_names):
1283         print 'Sensing',
1284         for box in box_or_names:
1285             if not isinstance(box,Box): box=self.get_box(box)
1286             if not box: continue
1287             box.sense(self.options)
1288         print 'Done'
1289         for box in box_or_names:
1290             if not isinstance(box,Box): box=self.get_box(box)
1291             if not box: continue
1292             box.list(self.options.verbose)
1293
1294     def reboot_boxes(self,box_or_names):
1295         for box in box_or_names:
1296             if not isinstance(box,Box): box=self.get_box(box)
1297             if not box: continue
1298             box.reboot(self.options)
1299
1300     ####################
1301     # can be run as a utility to probe/display/manage the local infrastructure
1302     def main (self):
1303         parser=OptionParser()
1304         parser.add_option ('-r',"--reboot",action='store_true',dest='reboot',default=False,
1305                            help='reboot mode (use shutdown -r)')
1306         parser.add_option ('-s',"--soft",action='store_true',dest='soft',default=False,
1307                            help='soft mode for reboot (vserver stop or kill qemus)')
1308         parser.add_option ('-t',"--testbox",action='store_true',dest='testbox',default=False,
1309                            help='add test box') 
1310         parser.add_option ('-b',"--build",action='store_true',dest='builds',default=False,
1311                            help='add build boxes')
1312         parser.add_option ('-p',"--plc",action='store_true',dest='plcs',default=False,
1313                            help='add plc boxes')
1314         parser.add_option ('-q',"--qemu",action='store_true',dest='qemus',default=False,
1315                            help='add qemu boxes') 
1316         parser.add_option ('-a',"--all",action='store_true',dest='all',default=False,
1317                            help='address all known  boxes, like -b -t -p -q')
1318         parser.add_option ('-v',"--verbose",action='store_true',dest='verbose',default=False,
1319                            help='verbose mode')
1320         parser.add_option ('-n',"--dry_run",action='store_true',dest='dry_run',default=False,
1321                            help='dry run mode')
1322         (self.options,args)=parser.parse_args()
1323
1324         self.rescope (plcs_on_vs=True, plcs_on_lxc=True)
1325
1326         boxes=args
1327         if self.options.testbox: boxes += [self.test_box]
1328         if self.options.builds: boxes += self.build_boxes
1329         if self.options.plcs: boxes += self.plc_boxes
1330         if self.options.qemus: boxes += self.qemu_boxes
1331         if self.options.all: boxes += self.all_boxes
1332         
1333         global verbose
1334         verbose=self.options.verbose
1335         # default scope is -b -p -q -t
1336         if not boxes:
1337             boxes = self.build_boxes + self.plc_boxes + self.qemu_boxes + [self.test_box]
1338
1339         if self.options.reboot: self.reboot_boxes (boxes)
1340         else:                   self.list_boxes (boxes)