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