Various fixes:
[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 import logging
19
20 from nepi.util.constants import ApplicationStatus as AS
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         # Logging
86         self._logger = logging.getLogger('nepi.testbeds.planetlab')
87         
88     
89     def __str__(self):
90         return "%s<%s>" % (
91             self.__class__.__name__,
92             ' '.join(filter(bool,(self.depends, self.sources)))
93         )
94     
95     def validate(self):
96         if self.home_path is None:
97             raise AssertionError, "Misconfigured application: missing home path"
98         if self.node.ident_path is None or not os.access(self.node.ident_path, os.R_OK):
99             raise AssertionError, "Misconfigured application: missing slice SSH key"
100         if self.node is None:
101             raise AssertionError, "Misconfigured application: unconnected node"
102         if self.node.hostname is None:
103             raise AssertionError, "Misconfigured application: misconfigured node"
104         if self.node.slicename is None:
105             raise AssertionError, "Misconfigured application: unspecified slice"
106     
107     def remote_trace_path(self, whichtrace):
108         if whichtrace in self.TRACES:
109             tracefile = os.path.join(self.home_path, whichtrace)
110         else:
111             tracefile = None
112         
113         return tracefile
114     
115     def sync_trace(self, local_dir, whichtrace):
116         tracefile = self.remote_trace_path(whichtrace)
117         if not tracefile:
118             return None
119         
120         local_path = os.path.join(local_dir, tracefile)
121         
122         # create parent local folders
123         proc = subprocess.Popen(
124             ["mkdir", "-p", os.path.dirname(local_path)],
125             stdout = open("/dev/null","w"),
126             stdin = open("/dev/null","r"))
127
128         if proc.wait():
129             raise RuntimeError, "Failed to synchronize trace"
130         
131         # sync files
132         try:
133             self._popen_scp(
134                 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
135                     tracefile),
136                 local_path
137                 )
138         except RuntimeError, e:
139             raise RuntimeError, "Failed to synchronize trace: %s %s" \
140                     % (e.args[0], e.args[1],)
141         
142         return local_path
143
144     def setup(self):
145         self._logger.info("Setting up %s", self)
146         self._make_home()
147         self._launch_build()
148         self._finish_build()
149         self._setup = True
150     
151     def async_setup(self):
152         if not self._setuper:
153             def setuper():
154                 try:
155                     self.setup()
156                 except:
157                     self._setuper._exc.append(sys.exc_info())
158             self._setuper = threading.Thread(
159                 target = setuper)
160             self._setuper._exc = []
161             self._setuper.start()
162     
163     def async_setup_wait(self):
164         if not self._setup:
165             self._logger.info("Waiting for %s to be setup", self)
166             if self._setuper:
167                 self._setuper.join()
168                 if not self._setup:
169                     if self._setuper._exc:
170                         exctyp,exval,exctrace = self._setuper._exc[0]
171                         raise exctyp,exval,exctrace
172                     else:
173                         raise RuntimeError, "Failed to setup application"
174             else:
175                 self.setup()
176         
177     def _make_home(self):
178         # Make sure all the paths are created where 
179         # they have to be created for deployment
180         # sync files
181         try:
182             self._popen_ssh_command(
183                 "mkdir -p %(home)s && ( rm -f %(home)s/{pid,build-pid,nepi-build.sh} >/dev/null 2>&1 || /bin/true )" \
184                     % { 'home' : server.shell_escape(self.home_path) }
185                 )
186         except RuntimeError, e:
187             raise RuntimeError, "Failed to set up application %s: %s %s" % (self.home_path, e.args[0], e.args[1],)
188         
189         if self.stdin:
190             # Write program input
191             try:
192                 self._popen_scp(
193                     cStringIO.StringIO(self.stdin),
194                     '%s@%s:%s' % (self.node.slicename, self.node.hostname, 
195                         os.path.join(self.home_path, 'stdin') ),
196                     )
197             except RuntimeError, e:
198                 raise RuntimeError, "Failed to set up application %s: %s %s" \
199                         % (self.home_path, e.args[0], e.args[1],)
200
201     def _replace_paths(self, command):
202         """
203         Replace all special path tags with shell-escaped actual paths.
204         """
205         # need to append ${HOME} if paths aren't absolute, to MAKE them absolute.
206         root = '' if self.home_path.startswith('/') else "${HOME}/"
207         return ( command
208             .replace("${SOURCES}", root+server.shell_escape(self.home_path))
209             .replace("${BUILD}", root+server.shell_escape(os.path.join(self.home_path,'build'))) )
210
211     def _launch_build(self):
212         if self._master is not None:
213             self._do_install_keys()
214             buildscript = self._do_build_slave()
215         else:
216             buildscript = self._do_build_master()
217             
218         if buildscript is not None:
219             self._logger.info("Building %s", self)
220             
221             # upload build script
222             try:
223                 self._popen_scp(
224                     buildscript,
225                     '%s@%s:%s' % (self.node.slicename, self.node.hostname, 
226                         os.path.join(self.home_path, 'nepi-build.sh') )
227                     )
228             except RuntimeError, e:
229                 raise RuntimeError, "Failed to set up application %s: %s %s" \
230                         % (self.home_path, e.args[0], e.args[1],)
231             
232             # launch build
233             self._do_launch_build()
234     
235     def _finish_build(self):
236         self._do_wait_build()
237         self._do_install()
238
239     def _do_build_slave(self):
240         if not self.sources and not self.build:
241             return None
242             
243         # Create build script
244         files = set()
245         
246         if self.sources:
247             sources = self.sources.split(' ')
248             files.update(
249                 "%s@%s:%s" % (self._master.node.slicename, self._master.node.hostname, 
250                     os.path.join(self._master.home_path, os.path.basename(source)),)
251                 for source in sources
252             )
253         
254         if self.build:
255             files.add(
256                 "%s@%s:%s" % (self._master.node.slicename, self._master.node.hostname, 
257                     os.path.join(self._master.home_path, 'build.tar.gz'),)
258             )
259         
260         launch_agent = "{ ( echo -e '#!/bin/sh\\ncat' > .ssh-askpass ) && chmod u+x .ssh-askpass"\
261                         " && export SSH_ASKPASS=$(pwd)/.ssh-askpass "\
262                         " && ssh-agent > .ssh-agent.sh ; } && . ./.ssh-agent.sh && ( echo $NEPI_MASTER_PASSPHRASE | ssh-add %(prk)s ) && rm -rf %(prk)s %(puk)s" %  \
263         {
264             'prk' : server.shell_escape(self._master_prk_name),
265             'puk' : server.shell_escape(self._master_puk_name),
266         }
267         
268         kill_agent = "kill $SSH_AGENT_PID"
269         
270         waitmaster = "{ . ./.ssh-agent.sh ; while [[ $(ssh -q -o UserKnownHostsFile=%(hostkey)s %(master)s cat %(token_path)s) != %(token)s ]] ; do sleep 5 ; done ; }" % {
271             'hostkey' : 'master_known_hosts',
272             'master' : "%s@%s" % (self._master.node.slicename, self._master.node.hostname),
273             'token_path' : os.path.join(self._master.home_path, 'build.token'),
274             'token' : server.shell_escape(self._master._master_token),
275         }
276         
277         syncfiles = "scp -p -o UserKnownHostsFile=%(hostkey)s %(files)s ." % {
278             'hostkey' : 'master_known_hosts',
279             'files' : ' '.join(files),
280         }
281         if self.build:
282             syncfiles += " && tar xzf build.tar.gz"
283         syncfiles += " && ( echo %s > build.token )" % (server.shell_escape(self._master_token),)
284         syncfiles = "{ . ./.ssh-agent.sh ; %s ; }" % (syncfiles,)
285         
286         cleanup = "{ . ./.ssh-agent.sh ; kill $SSH_AGENT_PID ; rm -rf %(prk)s %(puk)s master_known_hosts .ssh-askpass ; }" % {
287             'prk' : server.shell_escape(self._master_prk_name),
288             'puk' : server.shell_escape(self._master_puk_name),
289         }
290         
291         slavescript = "( ( %(launch_agent)s && %(waitmaster)s && %(syncfiles)s && %(kill_agent)s && %(cleanup)s ) || %(cleanup)s )" % {
292             'waitmaster' : waitmaster,
293             'syncfiles' : syncfiles,
294             'cleanup' : cleanup,
295             'kill_agent' : kill_agent,
296             'launch_agent' : launch_agent,
297             'home' : server.shell_escape(self.home_path),
298         }
299         
300         return cStringIO.StringIO(slavescript)
301          
302     def _do_launch_build(self):
303         script = "bash ./nepi-build.sh"
304         if self._master_passphrase:
305             script = "NEPI_MASTER_PASSPHRASE=%s %s" % (
306                 server.shell_escape(self._master_passphrase),
307                 script
308             )
309         (out,err),proc = rspawn.remote_spawn(
310             script,
311             pidfile = 'build-pid',
312             home = self.home_path,
313             stdin = '/dev/null',
314             stdout = 'buildlog',
315             stderr = rspawn.STDOUT,
316             
317             host = self.node.hostname,
318             port = None,
319             user = self.node.slicename,
320             agent = None,
321             ident_key = self.node.ident_path,
322             server_key = self.node.server_key
323             )
324         
325         if proc.wait():
326             raise RuntimeError, "Failed to set up build slave %s: %s %s" % (self.home_path, out,err,)
327         
328         
329         pid = ppid = None
330         delay = 1.0
331         for i in xrange(5):
332             pidtuple = rspawn.remote_check_pid(
333                 os.path.join(self.home_path,'build-pid'),
334                 host = self.node.hostname,
335                 port = None,
336                 user = self.node.slicename,
337                 agent = None,
338                 ident_key = self.node.ident_path,
339                 server_key = self.node.server_key
340                 )
341             
342             if pidtuple:
343                 pid, ppid = pidtuple
344                 self._build_pid, self._build_ppid = pidtuple
345                 break
346             else:
347                 time.sleep(delay)
348                 delay = min(30,delay*1.2)
349         else:
350             raise RuntimeError, "Failed to set up build slave %s: cannot get pid" % (self.home_path,)
351
352         self._logger.info("Deploying %s", self)
353         
354     def _do_wait_build(self):
355         pid = self._build_pid
356         ppid = self._build_ppid
357         
358         if pid and ppid:
359             delay = 1.0
360             first = True
361             while True:
362                 status = rspawn.remote_status(
363                     pid, ppid,
364                     host = self.node.hostname,
365                     port = None,
366                     user = self.node.slicename,
367                     agent = None,
368                     ident_key = self.node.ident_path,
369                     server_key = self.node.server_key
370                     )
371                 
372                 if status is not rspawn.RUNNING:
373                     self._build_pid = self._build_ppid = None
374                     break
375                 else:
376                     if first:
377                         self._logger.info("Waiting for %s to finish building %s", self,
378                             "(build slave)" if self._master is not None else "(build master)")
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             self._logger.info("Built %s", 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             self._logger.info("Killing build of %s", 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             self._logger.info("Installing %s", 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/{install,build}log >&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         self._logger.info("Starting %s", 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 AS.STATUS_NOT_STARTED
685         elif not self._pid or not self._ppid:
686             return AS.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 AS.STATUS_NOT_STARTED
700             elif status is rspawn.RUNNING:
701                 return AS.STATUS_RUNNING
702             elif status is rspawn.FINISHED:
703                 return AS.STATUS_FINISHED
704             else:
705                 # WTF?
706                 return AS.STATUS_NOT_STARTED
707     
708     def kill(self):
709         status = self.status()
710         if status == AS.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             self._logger.info("Killed %s", 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/ns-3.9-nepi/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                      # Archive SHA1 sums to check
808                      "echo '7158877faff2254e6c094bf18e6b4283cac19137  pygccxml-1.0.0.zip' > archive_sums.txt && "
809                      "echo 'ddc7c5d288e1bacb1307114878956762c5146fac  pybindgen-src.zip' >> archive_sums.txt && "
810                      " ( " # check existing files
811                      " sha1sum -c archive_sums.txt && "
812                      " test -f passfd-src.tar.gz && "
813                      " test -f ns3-src.tar.gz "
814                      " ) || ( " # nope? re-download
815                      " rm -f pybindgen-src.zip pygccxml-1.0.0.zip passfd-src.tar.gz ns3-src.tar.gz && "
816                      " wget -q -c -O pybindgen-src.zip %(pybindgen_source_url)s && " # continue, to exploit the case when it has already been dl'ed
817                      " wget -q -c -O pygccxml-1.0.0.zip %(pygccxml_source_url)s && " 
818                      " wget -q -c -O passfd-src.tar.gz %(passfd_source_url)s && "
819                      " wget -q -c -O ns3-src.tar.gz %(ns3_source_url)s && "  
820                      " sha1sum -c archive_sums.txt " # Check SHA1 sums when applicable
821                      " ) && "
822                      "unzip -n pybindgen-src.zip && " # Do not overwrite files, to exploit the case when it has already been built
823                      "unzip -n pygccxml-1.0.0.zip && "
824                      "mkdir -p ns3-src && "
825                      "mkdir -p passfd-src && "
826                      "tar xzf ns3-src.tar.gz --strip-components=1 -C ns3-src && "
827                      "tar xzf passfd-src.tar.gz --strip-components=1 -C passfd-src && "
828                      "rm -rf target && "    # mv doesn't like unclean targets
829                      "mkdir -p target && "
830                      "cd pygccxml-1.0.0 && "
831                      "rm -rf unittests docs && " # pygccxml has ~100M of unit tests - excessive - docs aren't needed either
832                      "python setup.py build && "
833                      "python setup.py install --install-lib ${BUILD}/target && "
834                      "python setup.py clean && "
835                      "cd ../pybindgen-0.15.0 && "
836                      "export PYTHONPATH=$PYTHONPATH:${BUILD}/target && "
837                      "./waf configure --prefix=${BUILD}/target -d release && "
838                      "./waf && "
839                      "./waf install && "
840                      "./waf clean && "
841                      "mv -f ${BUILD}/target/lib/python*/site-packages/pybindgen ${BUILD}/target/. && "
842                      "rm -rf ${BUILD}/target/lib && "
843                      "cd ../passfd-src && "
844                      "python setup.py build && "
845                      "python setup.py install --install-lib ${BUILD}/target && "
846                      "python setup.py clean && "
847                      "cd ../ns3-src && "
848                      "./waf configure --prefix=${BUILD}/target -d release --disable-examples --high-precision-as-double && "
849                      "./waf &&"
850                      "./waf install && "
851                      "./waf clean"
852              " )"
853                      % dict(
854                         pybindgen_source_url = server.shell_escape(pybindgen_source_url),
855                         pygccxml_source_url = server.shell_escape(pygccxml_source_url),
856                         ns3_source_url = server.shell_escape(ns3_source_url),
857                         passfd_source_url = server.shell_escape(passfd_source_url),
858                      ))
859         
860         # Just move ${BUILD}/target
861         self.install = (
862             " ( "
863             "  cd .. && "
864             "  python -c 'import pygccxml, pybindgen, passfd' && "
865             "  test -f lib/_ns3.so && "
866             "  test -f lib/libns3.so "
867             " ) || ( "
868                 # Not working, reinstall
869                     "test -d ${BUILD}/target && "
870                     "[[ \"x\" != \"x$(find ${BUILD}/target -mindepth 1 -print -quit)\" ]] &&"
871                     "( for i in ${BUILD}/target/* ; do rm -rf ${SOURCES}/${i##*/} ; done ) && " # mv doesn't like unclean targets
872                     "mv -f ${BUILD}/target/* ${SOURCES}"
873             " )"
874         )
875         
876         # Set extra environment paths
877         self.env['NEPI_NS3BINDINGS'] = "${SOURCES}/lib"
878         self.env['NEPI_NS3LIBRARY'] = "${SOURCES}/lib/libns3.so"
879     
880     @property
881     def tarball(self):
882         if self._tarball is None:
883             shared_tar = self._shared_nepi_tar and self._shared_nepi_tar()
884             if shared_tar is not None:
885                 self._tarball = shared_tar
886             else:
887                 # Build an ad-hoc tarball
888                 # Prebuilt
889                 import nepi
890                 import tempfile
891                 
892                 shared_tar = tempfile.NamedTemporaryFile(prefix='nepi-src-', suffix='.tar.gz')
893                 
894                 proc = subprocess.Popen(
895                     ["tar", "czf", shared_tar.name, 
896                         '-C', os.path.join(os.path.dirname(os.path.dirname(nepi.__file__)),'.'), 
897                         'nepi'],
898                     stdout = open("/dev/null","w"),
899                     stdin = open("/dev/null","r"))
900
901                 if proc.wait():
902                     raise RuntimeError, "Failed to create nepi tarball"
903                 
904                 self._tarball = self._shared_nepi_tar = shared_tar
905                 
906         return self._tarball
907
908