Log when applications start building (for better progress monitoring)
[nepi.git] / src / nepi / testbeds / planetlab / application.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from constants import TESTBED_ID
5 import plcapi
6 import operator
7 import os
8 import os.path
9 import sys
10 import nepi.util.server as server
11 import cStringIO
12 import subprocess
13 import rspawn
14 import random
15 import time
16 import socket
17 import threading
18
19 from nepi.util.constants import STATUS_NOT_STARTED, STATUS_RUNNING, \
20         STATUS_FINISHED
21
22 class Dependency(object):
23     """
24     A Dependency is in every respect like an application.
25     
26     It depends on some packages, it may require building binaries, it must deploy
27     them...
28     
29     But it has no command. Dependencies aren't ever started, or stopped, and have
30     no status.
31     """
32
33     TRACES = ('buildlog')
34
35     def __init__(self, api=None):
36         if not api:
37             api = plcapi.PLCAPI()
38         self._api = api
39         
40         # Attributes
41         self.command = None
42         self.sudo = False
43         
44         self.build = None
45         self.install = None
46         self.depends = None
47         self.buildDepends = None
48         self.sources = None
49         self.rpmFusion = False
50         self.env = {}
51         
52         self.stdin = None
53         self.stdout = None
54         self.stderr = None
55         self.buildlog = None
56         
57         self.add_to_path = True
58         
59         # Those are filled when the app is configured
60         self.home_path = None
61         
62         # Those are filled when an actual node is connected
63         self.node = None
64         
65         # Those are filled when the app is started
66         #   Having both pid and ppid makes it harder
67         #   for pid rollover to induce tracking mistakes
68         self._started = False
69         self._setup = False
70         self._setuper = None
71         self._pid = None
72         self._ppid = None
73
74         # Spanning tree deployment
75         self._master = None
76         self._master_passphrase = None
77         self._master_prk = None
78         self._master_puk = None
79         self._master_token = ''.join(map(chr,[rng.randint(0,255) 
80                                       for rng in (random.SystemRandom(),)
81                                       for i in xrange(8)] )).encode("hex")
82         self._build_pid = None
83         self._build_ppid = None
84         
85     
86     def __str__(self):
87         return "%s<%s>" % (
88             self.__class__.__name__,
89             ' '.join(filter(bool,(self.depends, self.sources)))
90         )
91     
92     def validate(self):
93         if self.home_path is None:
94             raise AssertionError, "Misconfigured application: missing home path"
95         if self.node.ident_path is None or not os.access(self.node.ident_path, os.R_OK):
96             raise AssertionError, "Misconfigured application: missing slice SSH key"
97         if self.node is None:
98             raise AssertionError, "Misconfigured application: unconnected node"
99         if self.node.hostname is None:
100             raise AssertionError, "Misconfigured application: misconfigured node"
101         if self.node.slicename is None:
102             raise AssertionError, "Misconfigured application: unspecified slice"
103     
104     def remote_trace_path(self, whichtrace):
105         if whichtrace in self.TRACES:
106             tracefile = os.path.join(self.home_path, whichtrace)
107         else:
108             tracefile = None
109         
110         return tracefile
111     
112     def sync_trace(self, local_dir, whichtrace):
113         tracefile = self.remote_trace_path(whichtrace)
114         if not tracefile:
115             return None
116         
117         local_path = os.path.join(local_dir, tracefile)
118         
119         # create parent local folders
120         proc = subprocess.Popen(
121             ["mkdir", "-p", os.path.dirname(local_path)],
122             stdout = open("/dev/null","w"),
123             stdin = open("/dev/null","r"))
124
125         if proc.wait():
126             raise RuntimeError, "Failed to synchronize trace"
127         
128         # sync files
129         try:
130             self._popen_scp(
131                 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
132                     tracefile),
133                 local_path
134                 )
135         except RuntimeError, e:
136             raise RuntimeError, "Failed to synchronize trace: %s %s" \
137                     % (e.args[0], e.args[1],)
138         
139         return local_path
140
141     def setup(self):
142         print >>sys.stderr, "Setting up", self
143         self._make_home()
144         self._launch_build()
145         self._finish_build()
146         self._setup = True
147     
148     def async_setup(self):
149         if not self._setuper:
150             def setuper():
151                 try:
152                     self.setup()
153                 except:
154                     self._setuper._exc.append(sys.exc_info())
155             self._setuper = threading.Thread(
156                 target = setuper)
157             self._setuper._exc = []
158             self._setuper.start()
159     
160     def async_setup_wait(self):
161         if not self._setup:
162             print >>sys.stderr, "Waiting for", self, "to be setup"
163             if self._setuper:
164                 self._setuper.join()
165                 if not self._setup:
166                     if self._setuper._exc:
167                         exctyp,exval,exctrace = self._setuper._exc[0]
168                         raise exctyp,exval,exctrace
169                     else:
170                         raise RuntimeError, "Failed to setup application"
171             else:
172                 self.setup()
173         
174     def _make_home(self):
175         # Make sure all the paths are created where 
176         # they have to be created for deployment
177         # sync files
178         try:
179             self._popen_ssh_command(
180                 "mkdir -p %(home)s && ( rm -f %(home)s/{pid,build-pid,nepi-build.sh} >/dev/null 2>&1 || /bin/true )" \
181                     % { 'home' : server.shell_escape(self.home_path) }
182                 )
183         except RuntimeError, e:
184             raise RuntimeError, "Failed to set up application %s: %s %s" % (self.home_path, e.args[0], e.args[1],)
185         
186         if self.stdin:
187             # Write program input
188             try:
189                 self._popen_scp(
190                     cStringIO.StringIO(self.stdin),
191                     '%s@%s:%s' % (self.node.slicename, self.node.hostname, 
192                         os.path.join(self.home_path, 'stdin') ),
193                     )
194             except RuntimeError, e:
195                 raise RuntimeError, "Failed to set up application %s: %s %s" \
196                         % (self.home_path, e.args[0], e.args[1],)
197
198     def _replace_paths(self, command):
199         """
200         Replace all special path tags with shell-escaped actual paths.
201         """
202         # need to append ${HOME} if paths aren't absolute, to MAKE them absolute.
203         root = '' if self.home_path.startswith('/') else "${HOME}/"
204         return ( command
205             .replace("${SOURCES}", root+server.shell_escape(self.home_path))
206             .replace("${BUILD}", root+server.shell_escape(os.path.join(self.home_path,'build'))) )
207
208     def _launch_build(self):
209         if self._master is not None:
210             self._do_install_keys()
211             buildscript = self._do_build_slave()
212         else:
213             buildscript = self._do_build_master()
214             
215         if buildscript is not None:
216             print "Building", self
217             
218             # upload build script
219             try:
220                 self._popen_scp(
221                     buildscript,
222                     '%s@%s:%s' % (self.node.slicename, self.node.hostname, 
223                         os.path.join(self.home_path, 'nepi-build.sh') )
224                     )
225             except RuntimeError, e:
226                 raise RuntimeError, "Failed to set up application %s: %s %s" \
227                         % (self.home_path, e.args[0], e.args[1],)
228             
229             # launch build
230             self._do_launch_build()
231     
232     def _finish_build(self):
233         self._do_wait_build()
234         self._do_install()
235
236     def _do_build_slave(self):
237         if not self.sources and not self.build:
238             return None
239             
240         # Create build script
241         files = set()
242         
243         if self.sources:
244             sources = self.sources.split(' ')
245             files.update(
246                 "%s@%s:%s" % (self._master.node.slicename, self._master.node.hostname, 
247                     os.path.join(self._master.home_path, os.path.basename(source)),)
248                 for source in sources
249             )
250         
251         if self.build:
252             files.add(
253                 "%s@%s:%s" % (self._master.node.slicename, self._master.node.hostname, 
254                     os.path.join(self._master.home_path, 'build.tar.gz'),)
255             )
256         
257         launch_agent = "{ ( echo -e '#!/bin/sh\\ncat' > .ssh-askpass ) && chmod u+x .ssh-askpass"\
258                         " && export SSH_ASKPASS=$(pwd)/.ssh-askpass "\
259                         " && ssh-agent > .ssh-agent.sh ; } && . ./.ssh-agent.sh && ( echo $NEPI_MASTER_PASSPHRASE | ssh-add %(prk)s ) && rm -rf %(prk)s %(puk)s" %  \
260         {
261             'prk' : server.shell_escape(self._master_prk_name),
262             'puk' : server.shell_escape(self._master_puk_name),
263         }
264         
265         kill_agent = "kill $SSH_AGENT_PID"
266         
267         waitmaster = "{ . ./.ssh-agent.sh ; while [[ $(ssh -q -o UserKnownHostsFile=%(hostkey)s %(master)s cat %(token_path)s) != %(token)s ]] ; do sleep 5 ; done ; }" % {
268             'hostkey' : 'master_known_hosts',
269             'master' : "%s@%s" % (self._master.node.slicename, self._master.node.hostname),
270             'token_path' : os.path.join(self._master.home_path, 'build.token'),
271             'token' : server.shell_escape(self._master._master_token),
272         }
273         
274         syncfiles = "scp -p -o UserKnownHostsFile=%(hostkey)s %(files)s ." % {
275             'hostkey' : 'master_known_hosts',
276             'files' : ' '.join(files),
277         }
278         if self.build:
279             syncfiles += " && tar xzf build.tar.gz"
280         syncfiles += " && ( echo %s > build.token )" % (server.shell_escape(self._master_token),)
281         syncfiles = "{ . ./.ssh-agent.sh ; %s ; }" % (syncfiles,)
282         
283         cleanup = "{ . ./.ssh-agent.sh ; kill $SSH_AGENT_PID ; rm -rf %(prk)s %(puk)s master_known_hosts .ssh-askpass ; }" % {
284             'prk' : server.shell_escape(self._master_prk_name),
285             'puk' : server.shell_escape(self._master_puk_name),
286         }
287         
288         slavescript = "( ( %(launch_agent)s && %(waitmaster)s && %(syncfiles)s && %(kill_agent)s && %(cleanup)s ) || %(cleanup)s )" % {
289             'waitmaster' : waitmaster,
290             'syncfiles' : syncfiles,
291             'cleanup' : cleanup,
292             'kill_agent' : kill_agent,
293             'launch_agent' : launch_agent,
294             'home' : server.shell_escape(self.home_path),
295         }
296         
297         return cStringIO.StringIO(slavescript)
298          
299     def _do_launch_build(self):
300         script = "bash ./nepi-build.sh"
301         if self._master_passphrase:
302             script = "NEPI_MASTER_PASSPHRASE=%s %s" % (
303                 server.shell_escape(self._master_passphrase),
304                 script
305             )
306         (out,err),proc = rspawn.remote_spawn(
307             script,
308             pidfile = 'build-pid',
309             home = self.home_path,
310             stdin = '/dev/null',
311             stdout = 'buildlog',
312             stderr = rspawn.STDOUT,
313             
314             host = self.node.hostname,
315             port = None,
316             user = self.node.slicename,
317             agent = None,
318             ident_key = self.node.ident_path,
319             server_key = self.node.server_key
320             )
321         
322         if proc.wait():
323             raise RuntimeError, "Failed to set up build slave %s: %s %s" % (self.home_path, out,err,)
324         
325         
326         pid = ppid = None
327         delay = 1.0
328         for i in xrange(5):
329             pidtuple = rspawn.remote_check_pid(
330                 os.path.join(self.home_path,'build-pid'),
331                 host = self.node.hostname,
332                 port = None,
333                 user = self.node.slicename,
334                 agent = None,
335                 ident_key = self.node.ident_path,
336                 server_key = self.node.server_key
337                 )
338             
339             if pidtuple:
340                 pid, ppid = pidtuple
341                 self._build_pid, self._build_ppid = pidtuple
342                 break
343             else:
344                 time.sleep(delay)
345                 delay = min(30,delay*1.2)
346         else:
347             raise RuntimeError, "Failed to set up build slave %s: cannot get pid" % (self.home_path,)
348
349         print >>sys.stderr, "Deploying", self
350         
351     def _do_wait_build(self):
352         pid = self._build_pid
353         ppid = self._build_ppid
354         
355         if pid and ppid:
356             delay = 1.0
357             first = True
358             while True:
359                 status = rspawn.remote_status(
360                     pid, ppid,
361                     host = self.node.hostname,
362                     port = None,
363                     user = self.node.slicename,
364                     agent = None,
365                     ident_key = self.node.ident_path,
366                     server_key = self.node.server_key
367                     )
368                 
369                 if status is not rspawn.RUNNING:
370                     self._build_pid = self._build_ppid = None
371                     break
372                 else:
373                     if first:
374                         print >>sys.stderr, "Waiting for", self, "to finish building",
375                         if self._master is not None:
376                             print >>sys.stderr, "(build master)"
377                         else:
378                             print >>sys.stderr, "(build slave)"
379                         
380                         first = False
381                     time.sleep(delay*(0.5+random.random()))
382                     delay = min(30,delay*1.2)
383             
384             # check build token
385             (out, err), proc = self._popen_ssh_command(
386                 "cat %(token_path)s" % {
387                     'token_path' : os.path.join(self.home_path, 'build.token'),
388                 },
389                 noerrors = True)
390             slave_token = ""
391             if not proc.wait() and out:
392                 slave_token = out.strip()
393             
394             if slave_token != self._master_token:
395                 # Get buildlog for the error message
396
397                 (buildlog, err), proc = self._popen_ssh_command(
398                     "cat %(buildlog)s" % {
399                         'buildlog' : os.path.join(self.home_path, 'buildlog'),
400                         'buildscript' : os.path.join(self.home_path, 'nepi-build.sh'),
401                     },
402                     noerrors = True)
403                 
404                 proc.wait()
405                 
406                 raise RuntimeError, "Failed to set up application %s: "\
407                         "build failed, got wrong token from pid %s/%s "\
408                         "(expected %r, got %r), see buildlog: %s" % (
409                     self.home_path, pid, ppid, self._master_token, slave_token, buildlog)
410
411             print >>sys.stderr, "Built", self
412
413     def _do_kill_build(self):
414         pid = self._build_pid
415         ppid = self._build_ppid
416         
417         if pid and ppid:
418             print >>sys.stderr, "Killing build of", self
419             rspawn.remote_kill(
420                 pid, ppid,
421                 host = self.node.hostname,
422                 port = None,
423                 user = self.node.slicename,
424                 agent = None,
425                 ident_key = self.node.ident_path
426                 )
427         
428         
429     def _do_build_master(self):
430         if not self.sources and not self.build and not self.buildDepends:
431             return None
432             
433         if self.sources:
434             sources = self.sources.split(' ')
435             
436             # Copy all sources
437             try:
438                 self._popen_scp(
439                     sources,
440                     "%s@%s:%s" % (self.node.slicename, self.node.hostname, 
441                         os.path.join(self.home_path,'.'),)
442                     )
443             except RuntimeError, e:
444                 raise RuntimeError, "Failed upload source file %r: %s %s" \
445                         % (sources, e.args[0], e.args[1],)
446             
447         buildscript = cStringIO.StringIO()
448         
449         if self.buildDepends:
450             # Install build dependencies
451             buildscript.write(
452                 "sudo -S yum -y install %(packages)s\n" % {
453                     'packages' : self.buildDepends
454                 }
455             )
456         
457             
458         if self.build:
459             # Build sources
460             buildscript.write(
461                 "mkdir -p build && ( cd build && ( %(command)s ) )\n" % {
462                     'command' : self._replace_paths(self.build),
463                     'home' : server.shell_escape(self.home_path),
464                 }
465             )
466         
467             # Make archive
468             buildscript.write("tar czf build.tar.gz build\n")
469         
470         # Write token
471         buildscript.write("echo %(master_token)s > build.token" % {
472             'master_token' : server.shell_escape(self._master_token)
473         })
474         
475         buildscript.seek(0)
476
477         return buildscript
478
479     def _do_install(self):
480         if self.install:
481             print >>sys.stderr, "Installing", self
482             
483             # Install application
484             try:
485                 self._popen_ssh_command(
486                     "cd %(home)s && cd build && ( %(command)s ) > ${HOME}/%(home)s/installlog 2>&1 || ( tail ${HOME}/%(home)s/installlog >&2 && false )" % \
487                         {
488                         'command' : self._replace_paths(self.install),
489                         'home' : server.shell_escape(self.home_path),
490                         },
491                     )
492             except RuntimeError, e:
493                 raise RuntimeError, "Failed install build sources: %s %s" % (e.args[0], e.args[1],)
494
495     def set_master(self, master):
496         self._master = master
497         
498     def install_keys(self, prk, puk, passphrase):
499         # Install keys
500         self._master_passphrase = passphrase
501         self._master_prk = prk
502         self._master_puk = puk
503         self._master_prk_name = os.path.basename(prk.name)
504         self._master_puk_name = os.path.basename(puk.name)
505         
506     def _do_install_keys(self):
507         prk = self._master_prk
508         puk = self._master_puk
509        
510         try:
511             self._popen_scp(
512                 [ prk.name, puk.name ],
513                 '%s@%s:%s' % (self.node.slicename, self.node.hostname, self.home_path )
514                 )
515         except RuntimeError, e:
516             raise RuntimeError, "Failed to set up application deployment keys: %s %s" \
517                     % (e.args[0], e.args[1],)
518
519         try:
520             self._popen_scp(
521                 cStringIO.StringIO('%s,%s %s\n' % (
522                     self._master.node.hostname, socket.gethostbyname(self._master.node.hostname), 
523                     self._master.node.server_key)),
524                 '%s@%s:%s' % (self.node.slicename, self.node.hostname, 
525                     os.path.join(self.home_path,"master_known_hosts") )
526                 )
527         except RuntimeError, e:
528             raise RuntimeError, "Failed to set up application deployment keys: %s %s" \
529                     % (e.args[0], e.args[1],)
530         
531         # No longer need'em
532         self._master_prk = None
533         self._master_puk = None
534     
535     def cleanup(self):
536         # make sure there's no leftover build processes
537         self._do_kill_build()
538
539     @server.eintr_retry
540     def _popen_scp(self, src, dst, retry = True):
541         (out,err),proc = server.popen_scp(
542             src,
543             dst, 
544             port = None,
545             agent = None,
546             ident_key = self.node.ident_path,
547             server_key = self.node.server_key
548             )
549
550         if server.eintr_retry(proc.wait)():
551             raise RuntimeError, (out, err)
552         return (out, err), proc
553   
554
555     @server.eintr_retry
556     def _popen_ssh_command(self, command, retry = True, noerrors=False):
557         (out,err),proc = server.popen_ssh_command(
558             command,
559             host = self.node.hostname,
560             port = None,
561             user = self.node.slicename,
562             agent = None,
563             ident_key = self.node.ident_path,
564             server_key = self.node.server_key
565             )
566
567         if server.eintr_retry(proc.wait)():
568             if not noerrors:
569                 raise RuntimeError, (out, err)
570         return (out, err), proc
571
572 class Application(Dependency):
573     """
574     An application also has dependencies, but also a command to be ran and monitored.
575     
576     It adds the output of that command as traces.
577     """
578     
579     TRACES = ('stdout','stderr','buildlog')
580     
581     def __init__(self, api=None):
582         super(Application,self).__init__(api)
583         
584         # Attributes
585         self.command = None
586         self.sudo = False
587         
588         self.stdin = None
589         self.stdout = None
590         self.stderr = None
591         
592         # Those are filled when the app is started
593         #   Having both pid and ppid makes it harder
594         #   for pid rollover to induce tracking mistakes
595         self._started = False
596         self._pid = None
597         self._ppid = None
598
599         # Do not add to the python path of nodes
600         self.add_to_path = False
601     
602     def __str__(self):
603         return "%s<command:%s%s>" % (
604             self.__class__.__name__,
605             "sudo " if self.sudo else "",
606             self.command,
607         )
608     
609     def start(self):
610         print >>sys.stderr, "Starting", self
611         
612         # Create shell script with the command
613         # This way, complex commands and scripts can be ran seamlessly
614         # sync files
615         command = cStringIO.StringIO()
616         command.write('export PYTHONPATH=$PYTHONPATH:%s\n' % (
617             ':'.join(["${HOME}/"+server.shell_escape(s) for s in self.node.pythonpath])
618         ))
619         command.write('export PATH=$PATH:%s\n' % (
620             ':'.join(["${HOME}/"+server.shell_escape(s) for s in self.node.pythonpath])
621         ))
622         if self.node.env:
623             for envkey, envvals in self.node.env.iteritems():
624                 for envval in envvals:
625                     command.write('export %s=%s\n' % (envkey, envval))
626         command.write(self.command)
627         command.seek(0)
628
629         try:
630             self._popen_scp(
631                 command,
632                 '%s@%s:%s' % (self.node.slicename, self.node.hostname, 
633                     os.path.join(self.home_path, "app.sh"))
634                 )
635         except RuntimeError, e:
636             raise RuntimeError, "Failed to set up application: %s %s" \
637                     % (e.args[0], e.args[1],)
638         
639         # Start process in a "daemonized" way, using nohup and heavy
640         # stdin/out redirection to avoid connection issues
641         (out,err),proc = rspawn.remote_spawn(
642             self._replace_paths("bash ./app.sh"),
643             
644             pidfile = './pid',
645             home = self.home_path,
646             stdin = 'stdin' if self.stdin is not None else '/dev/null',
647             stdout = 'stdout' if self.stdout else '/dev/null',
648             stderr = 'stderr' if self.stderr else '/dev/null',
649             sudo = self.sudo,
650             
651             host = self.node.hostname,
652             port = None,
653             user = self.node.slicename,
654             agent = None,
655             ident_key = self.node.ident_path,
656             server_key = self.node.server_key
657             )
658         
659         if proc.wait():
660             raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
661
662         self._started = True
663
664     def checkpid(self):            
665         # Get PID/PPID
666         # NOTE: wait a bit for the pidfile to be created
667         if self._started and not self._pid or not self._ppid:
668             pidtuple = rspawn.remote_check_pid(
669                 os.path.join(self.home_path,'pid'),
670                 host = self.node.hostname,
671                 port = None,
672                 user = self.node.slicename,
673                 agent = None,
674                 ident_key = self.node.ident_path,
675                 server_key = self.node.server_key
676                 )
677             
678             if pidtuple:
679                 self._pid, self._ppid = pidtuple
680     
681     def status(self):
682         self.checkpid()
683         if not self._started:
684             return STATUS_NOT_STARTED
685         elif not self._pid or not self._ppid:
686             return STATUS_NOT_STARTED
687         else:
688             status = rspawn.remote_status(
689                 self._pid, self._ppid,
690                 host = self.node.hostname,
691                 port = None,
692                 user = self.node.slicename,
693                 agent = None,
694                 ident_key = self.node.ident_path,
695                 server_key = self.node.server_key
696                 )
697             
698             if status is rspawn.NOT_STARTED:
699                 return STATUS_NOT_STARTED
700             elif status is rspawn.RUNNING:
701                 return STATUS_RUNNING
702             elif status is rspawn.FINISHED:
703                 return STATUS_FINISHED
704             else:
705                 # WTF?
706                 return STATUS_NOT_STARTED
707     
708     def kill(self):
709         status = self.status()
710         if status == STATUS_RUNNING:
711             # kill by ppid+pid - SIGTERM first, then try SIGKILL
712             rspawn.remote_kill(
713                 self._pid, self._ppid,
714                 host = self.node.hostname,
715                 port = None,
716                 user = self.node.slicename,
717                 agent = None,
718                 ident_key = self.node.ident_path,
719                 server_key = self.node.server_key
720                 )
721             print >>sys.stderr, "Killed", self
722
723
724 class NepiDependency(Dependency):
725     """
726     This dependency adds nepi itself to the python path,
727     so that you may run testbeds within PL nodes.
728     """
729     
730     # Class attribute holding a *weak* reference to the shared NEPI tar file
731     # so that they may share it. Don't operate on the file itself, it would
732     # be a mess, just use its path.
733     _shared_nepi_tar = None
734     
735     def __init__(self, api = None):
736         super(NepiDependency, self).__init__(api)
737         
738         self._tarball = None
739         
740         self.depends = 'python python-ipaddr python-setuptools'
741         
742         # our sources are in our ad-hoc tarball
743         self.sources = self.tarball.name
744         
745         tarname = os.path.basename(self.tarball.name)
746         
747         # it's already built - just move the tarball into place
748         self.build = "mv -f ${SOURCES}/%s ." % (tarname,)
749         
750         # unpack it into sources, and we're done
751         self.install = "tar xzf ${BUILD}/%s -C .." % (tarname,)
752     
753     @property
754     def tarball(self):
755         if self._tarball is None:
756             shared_tar = self._shared_nepi_tar and self._shared_nepi_tar()
757             if shared_tar is not None:
758                 self._tarball = shared_tar
759             else:
760                 # Build an ad-hoc tarball
761                 # Prebuilt
762                 import nepi
763                 import tempfile
764                 
765                 shared_tar = tempfile.NamedTemporaryFile(prefix='nepi-src-', suffix='.tar.gz')
766                 
767                 proc = subprocess.Popen(
768                     ["tar", "czf", shared_tar.name, 
769                         '-C', os.path.join(os.path.dirname(os.path.dirname(nepi.__file__)),'.'), 
770                         'nepi'],
771                     stdout = open("/dev/null","w"),
772                     stdin = open("/dev/null","r"))
773
774                 if proc.wait():
775                     raise RuntimeError, "Failed to create nepi tarball"
776                 
777                 self._tarball = self._shared_nepi_tar = shared_tar
778                 
779         return self._tarball
780
781 class NS3Dependency(Dependency):
782     """
783     This dependency adds NS3 libraries to the library paths,
784     so that you may run the NS3 testbed within PL nodes.
785     
786     You'll also need the NepiDependency.
787     """
788     
789     def __init__(self, api = None):
790         super(NS3Dependency, self).__init__(api)
791         
792         self.buildDepends = 'make waf gcc gcc-c++ gccxml unzip'
793         
794         # We have to download the sources, untar, build...
795         pybindgen_source_url = "http://pybindgen.googlecode.com/files/pybindgen-0.15.0.zip"
796         pygccxml_source_url = "http://leaseweb.dl.sourceforge.net/project/pygccxml/pygccxml/pygccxml-1.0/pygccxml-1.0.0.zip"
797         ns3_source_url = "http://yans.pl.sophia.inria.fr/code/hgwebdir.cgi/nepi-ns-3.9/archive/tip.tar.gz"
798         passfd_source_url = "http://yans.pl.sophia.inria.fr/code/hgwebdir.cgi/python-passfd/archive/tip.tar.gz"
799         self.build =(
800             " ( "
801             "  cd .. && "
802             "  python -c 'import pygccxml, pybindgen, passfd' && "
803             "  test -f lib/_ns3.so && "
804             "  test -f lib/libns3.so "
805             " ) || ( "
806                 # Not working, rebuild
807                      "wget -q -c -O pybindgen-src.zip %(pybindgen_source_url)s && " # continue, to exploit the case when it has already been dl'ed
808                      "wget -q -c -O pygccxml-1.0.0.zip %(pygccxml_source_url)s && " 
809                      "wget -q -c -O passfd-src.tar.gz %(passfd_source_url)s && "
810                      "wget -q -c -O ns3-src.tar.gz %(ns3_source_url)s && "  
811                      "unzip -n pybindgen-src.zip && " # Do not overwrite files, to exploit the case when it has already been built
812                      "unzip -n pygccxml-1.0.0.zip && "
813                      "mkdir -p ns3-src && "
814                      "mkdir -p passfd-src && "
815                      "tar xzf ns3-src.tar.gz --strip-components=1 -C ns3-src && "
816                      "tar xzf passfd-src.tar.gz --strip-components=1 -C passfd-src && "
817                      "rm -rf target && "    # mv doesn't like unclean targets
818                      "mkdir -p target && "
819                      "cd pygccxml-1.0.0 && "
820                      "rm -rf unittests docs && " # pygccxml has ~100M of unit tests - excessive - docs aren't needed either
821                      "python setup.py build && "
822                      "python setup.py install --install-lib ${BUILD}/target && "
823                      "python setup.py clean && "
824                      "cd ../pybindgen-0.15.0 && "
825                      "export PYTHONPATH=$PYTHONPATH:${BUILD}/target && "
826                      "./waf configure --prefix=${BUILD}/target -d release && "
827                      "./waf && "
828                      "./waf install && "
829                      "./waf clean && "
830                      "mv -f ${BUILD}/target/lib/python*/site-packages/pybindgen ${BUILD}/target/. && "
831                      "rm -rf ${BUILD}/target/lib && "
832                      "cd ../passfd-src && "
833                      "python setup.py build && "
834                      "python setup.py install --install-lib ${BUILD}/target && "
835                      "python setup.py clean && "
836                      "cd ../ns3-src && "
837                      "./waf configure --prefix=${BUILD}/target -d release --disable-examples --high-precision-as-double && "
838                      "./waf &&"
839                      "./waf install && "
840                      "./waf clean"
841              " )"
842                      % dict(
843                         pybindgen_source_url = server.shell_escape(pybindgen_source_url),
844                         pygccxml_source_url = server.shell_escape(pygccxml_source_url),
845                         ns3_source_url = server.shell_escape(ns3_source_url),
846                         passfd_source_url = server.shell_escape(passfd_source_url),
847                      ))
848         
849         # Just move ${BUILD}/target
850         self.install = (
851             " ( "
852             "  cd .. && "
853             "  python -c 'import pygccxml, pybindgen, passfd' && "
854             "  test -f lib/_ns3.so && "
855             "  test -f lib/libns3.so "
856             " ) || ( "
857                 # Not working, reinstall
858                     "( for i in ${BUILD}/target/* ; do rm -rf ${SOURCES}/${i##*/} ; done ) && " # mv doesn't like unclean targets
859                     "mv -f ${BUILD}/target/* ${SOURCES}"
860             " )"
861         )
862         
863         # Set extra environment paths
864         self.env['NEPI_NS3BINDINGS'] = "${SOURCES}/lib"
865         self.env['NEPI_NS3LIBRARY'] = "${SOURCES}/lib/libns3.so"
866     
867     @property
868     def tarball(self):
869         if self._tarball is None:
870             shared_tar = self._shared_nepi_tar and self._shared_nepi_tar()
871             if shared_tar is not None:
872                 self._tarball = shared_tar
873             else:
874                 # Build an ad-hoc tarball
875                 # Prebuilt
876                 import nepi
877                 import tempfile
878                 
879                 shared_tar = tempfile.NamedTemporaryFile(prefix='nepi-src-', suffix='.tar.gz')
880                 
881                 proc = subprocess.Popen(
882                     ["tar", "czf", shared_tar.name, 
883                         '-C', os.path.join(os.path.dirname(os.path.dirname(nepi.__file__)),'.'), 
884                         'nepi'],
885                     stdout = open("/dev/null","w"),
886                     stdin = open("/dev/null","r"))
887
888                 if proc.wait():
889                     raise RuntimeError, "Failed to create nepi tarball"
890                 
891                 self._tarball = self._shared_nepi_tar = shared_tar
892                 
893         return self._tarball
894
895