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