2 # -*- coding: utf-8 -*-
4 from constants import TESTBED_ID
9 import nepi.util.server as server
14 from nepi.util.constants import STATUS_NOT_STARTED, STATUS_RUNNING, \
17 class Dependency(object):
19 A Dependency is in every respect like an application.
21 It depends on some packages, it may require building binaries, it must deploy
24 But it has no command. Dependencies aren't ever started, or stopped, and have
30 def __init__(self, api=None):
42 self.buildDepends = None
51 self.add_to_path = True
53 # Those are filled when the app is configured
56 # Those are filled when an actual node is connected
59 # Those are filled when the app is started
60 # Having both pid and ppid makes it harder
61 # for pid rollover to induce tracking mistakes
70 self.__class__.__name__,
71 ' '.join(list(self.depends or [])
72 + list(self.sources or []))
76 if self.home_path is None:
77 raise AssertionError, "Misconfigured application: missing home path"
78 if self.node.ident_path is None or not os.access(self.node.ident_path, os.R_OK):
79 raise AssertionError, "Misconfigured application: missing slice SSH key"
81 raise AssertionError, "Misconfigured application: unconnected node"
82 if self.node.hostname is None:
83 raise AssertionError, "Misconfigured application: misconfigured node"
84 if self.node.slicename is None:
85 raise AssertionError, "Misconfigured application: unspecified slice"
87 def remote_trace_path(self, whichtrace):
88 if whichtrace in self.TRACES:
89 tracefile = os.path.join(self.home_path, whichtrace)
95 def sync_trace(self, local_dir, whichtrace):
96 tracefile = self.remote_trace_path(whichtrace)
100 local_path = os.path.join(local_dir, tracefile)
102 # create parent local folders
103 proc = subprocess.Popen(
104 ["mkdir", "-p", os.path.dirname(local_path)],
105 stdout = open("/dev/null","w"),
106 stdin = open("/dev/null","r"))
109 raise RuntimeError, "Failed to synchronize trace"
112 (out,err),proc = server.popen_scp(
113 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
118 ident_key = self.node.ident_path,
119 server_key = self.node.server_key
123 raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
133 def async_setup(self):
134 if not self._setuper:
135 self._setuper = threading.Thread(
137 self._setuper.start()
139 def async_setup_wait(self):
144 raise RuntimeError, "Failed to setup application"
148 def _make_home(self):
149 # Make sure all the paths are created where
150 # they have to be created for deployment
151 (out,err),proc = server.popen_ssh_command(
152 "mkdir -p %s" % (server.shell_escape(self.home_path),),
153 host = self.node.hostname,
155 user = self.node.slicename,
157 ident_key = self.node.ident_path,
158 server_key = self.node.server_key
162 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
166 # Write program input
167 (out,err),proc = server.popen_scp(
168 cStringIO.StringIO(self.stdin),
169 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
170 os.path.join(self.home_path, 'stdin') ),
173 ident_key = self.node.ident_path,
174 server_key = self.node.server_key
178 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
180 def _replace_paths(self, command):
182 Replace all special path tags with shell-escaped actual paths.
184 # need to append ${HOME} if paths aren't absolute, to MAKE them absolute.
185 root = '' if self.home_path.startswith('/') else "${HOME}/"
187 .replace("${SOURCES}", root+server.shell_escape(self.home_path))
188 .replace("${BUILD}", root+server.shell_escape(os.path.join(self.home_path,'build'))) )
192 sources = self.sources.split(' ')
195 (out,err),proc = server.popen_scp(
197 "%s@%s:%s" % (self.node.slicename, self.node.hostname,
198 os.path.join(self.home_path,'.'),),
199 ident_key = self.node.ident_path,
200 server_key = self.node.server_key
204 raise RuntimeError, "Failed upload source file %r: %s %s" % (source, out,err,)
206 if self.buildDepends:
207 # Install build dependencies
208 (out,err),proc = server.popen_ssh_command(
209 "sudo -S yum -y install %(packages)s" % {
210 'packages' : self.buildDepends
212 host = self.node.hostname,
214 user = self.node.slicename,
216 ident_key = self.node.ident_path,
217 server_key = self.node.server_key
221 raise RuntimeError, "Failed instal build dependencies: %s %s" % (out,err,)
226 (out,err),proc = server.popen_ssh_command(
227 "cd %(home)s && mkdir -p build && cd build && ( %(command)s ) > ${HOME}/%(home)s/buildlog 2>&1 || ( tail ${HOME}/%(home)s/buildlog >&2 && false )" % {
228 'command' : self._replace_paths(self.build),
229 'home' : server.shell_escape(self.home_path),
231 host = self.node.hostname,
233 user = self.node.slicename,
235 ident_key = self.node.ident_path,
236 server_key = self.node.server_key
240 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
243 (out,err),proc = server.popen_ssh_command(
244 "cd %(home)s && tar czf build.tar.gz build" % {
245 'command' : self._replace_paths(self.build),
246 'home' : server.shell_escape(self.home_path),
248 host = self.node.hostname,
250 user = self.node.slicename,
252 ident_key = self.node.ident_path,
253 server_key = self.node.server_key
257 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
260 # Install application
261 (out,err),proc = server.popen_ssh_command(
262 "cd %(home)s && cd build && ( %(command)s ) > ${HOME}/%(home)s/installlog 2>&1 || ( tail ${HOME}/%(home)s/installlog >&2 && false )" % {
263 'command' : self._replace_paths(self.install),
264 'home' : server.shell_escape(self.home_path),
266 host = self.node.hostname,
268 user = self.node.slicename,
270 ident_key = self.node.ident_path,
271 server_key = self.node.server_key
275 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
277 class Application(Dependency):
279 An application also has dependencies, but also a command to be ran and monitored.
281 It adds the output of that command as traces.
284 TRACES = ('stdout','stderr','buildlog')
286 def __init__(self, api=None):
287 super(Application,self).__init__(api)
297 # Those are filled when the app is started
298 # Having both pid and ppid makes it harder
299 # for pid rollover to induce tracking mistakes
300 self._started = False
304 # Do not add to the python path of nodes
305 self.add_to_path = False
308 return "%s<command:%s%s>" % (
309 self.__class__.__name__,
310 "sudo " if self.sudo else "",
315 # Create shell script with the command
316 # This way, complex commands and scripts can be ran seamlessly
318 command = cStringIO.StringIO()
319 command.write('export PYTHONPATH=$PYTHONPATH:%s\n' % (
320 ':'.join(["${HOME}/"+server.shell_escape(s) for s in self.node.pythonpath])
322 command.write('export PATH=$PATH:%s\n' % (
323 ':'.join(["${HOME}/"+server.shell_escape(s) for s in self.node.pythonpath])
326 for envkey, envvals in self.node.env.iteritems():
327 for envval in envvals:
328 command.write('export %s=%s\n' % (envkey, envval))
329 command.write(self.command)
332 (out,err),proc = server.popen_scp(
334 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
335 os.path.join(self.home_path, "app.sh")),
338 ident_key = self.node.ident_path,
339 server_key = self.node.server_key
343 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
345 # Start process in a "daemonized" way, using nohup and heavy
346 # stdin/out redirection to avoid connection issues
347 (out,err),proc = rspawn.remote_spawn(
348 self._replace_paths("bash ./app.sh"),
351 home = self.home_path,
352 stdin = 'stdin' if self.stdin is not None else '/dev/null',
353 stdout = 'stdout' if self.stdout else '/dev/null',
354 stderr = 'stderr' if self.stderr else '/dev/null',
357 host = self.node.hostname,
359 user = self.node.slicename,
361 ident_key = self.node.ident_path,
362 server_key = self.node.server_key
366 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
372 # NOTE: wait a bit for the pidfile to be created
373 if self._started and not self._pid or not self._ppid:
374 pidtuple = rspawn.remote_check_pid(
375 os.path.join(self.home_path,'pid'),
376 host = self.node.hostname,
378 user = self.node.slicename,
380 ident_key = self.node.ident_path,
381 server_key = self.node.server_key
385 self._pid, self._ppid = pidtuple
389 if not self._started:
390 return STATUS_NOT_STARTED
391 elif not self._pid or not self._ppid:
392 return STATUS_NOT_STARTED
394 status = rspawn.remote_status(
395 self._pid, self._ppid,
396 host = self.node.hostname,
398 user = self.node.slicename,
400 ident_key = self.node.ident_path
403 if status is rspawn.NOT_STARTED:
404 return STATUS_NOT_STARTED
405 elif status is rspawn.RUNNING:
406 return STATUS_RUNNING
407 elif status is rspawn.FINISHED:
408 return STATUS_FINISHED
411 return STATUS_NOT_STARTED
414 status = self.status()
415 if status == STATUS_RUNNING:
416 # kill by ppid+pid - SIGTERM first, then try SIGKILL
418 self._pid, self._ppid,
419 host = self.node.hostname,
421 user = self.node.slicename,
423 ident_key = self.node.ident_path,
424 server_key = self.node.server_key
427 class NepiDependency(Dependency):
429 This dependency adds nepi itself to the python path,
430 so that you may run testbeds within PL nodes.
433 # Class attribute holding a *weak* reference to the shared NEPI tar file
434 # so that they may share it. Don't operate on the file itself, it would
435 # be a mess, just use its path.
436 _shared_nepi_tar = None
438 def __init__(self, api = None):
439 super(NepiDependency, self).__init__(api)
443 self.depends = 'python python-ipaddr python-setuptools'
445 # our sources are in our ad-hoc tarball
446 self.sources = self.tarball.name
448 tarname = os.path.basename(self.tarball.name)
450 # it's already built - just move the tarball into place
451 self.build = "mv ${SOURCES}/%s ." % (tarname,)
453 # unpack it into sources, and we're done
454 self.install = "tar xzf ${BUILD}/%s -C .." % (tarname,)
458 if self._tarball is None:
459 shared_tar = self._shared_nepi_tar and self._shared_nepi_tar()
460 if shared_tar is not None:
461 self._tarball = shared_tar
463 # Build an ad-hoc tarball
468 shared_tar = tempfile.NamedTemporaryFile(prefix='nepi-src-', suffix='.tar.gz')
470 proc = subprocess.Popen(
471 ["tar", "czf", shared_tar.name,
472 '-C', os.path.join(os.path.dirname(os.path.dirname(nepi.__file__)),'.'),
474 stdout = open("/dev/null","w"),
475 stdin = open("/dev/null","r"))
478 raise RuntimeError, "Failed to create nepi tarball"
480 self._tarball = self._shared_nepi_tar = shared_tar
484 class NS3Dependency(Dependency):
486 This dependency adds NS3 libraries to the library paths,
487 so that you may run the NS3 testbed within PL nodes.
489 You'll also need the NepiDependency.
492 def __init__(self, api = None):
493 super(NS3Dependency, self).__init__(api)
495 self.buildDepends = 'build-essential waf gcc gcc-c++ gccxml unzip'
497 # We have to download the sources, untar, build...
498 pybindgen_source_url = "http://pybindgen.googlecode.com/files/pybindgen-0.15.0.zip"
499 pygccxml_source_url = "http://leaseweb.dl.sourceforge.net/project/pygccxml/pygccxml/pygccxml-1.0/pygccxml-1.0.0.zip"
500 ns3_source_url = "http://yans.pl.sophia.inria.fr/code/hgwebdir.cgi/ns-3-dev/archive/tip.tar.gz"
501 passfd_source_url = "http://yans.pl.sophia.inria.fr/code/hgwebdir.cgi/python-passfd/archive/tip.tar.gz"
505 " python -c 'import pygccxml, pybindgen, passfd' && "
506 " test -f lib/_ns3.so && "
507 " test -f lib/libns3.so "
509 # Not working, rebuild
510 "wget -q -c -O pybindgen-src.zip %(pybindgen_source_url)s && " # continue, to exploit the case when it has already been dl'ed
511 "wget -q -c -O pygccxml-1.0.0.zip %(pygccxml_source_url)s && "
512 "wget -q -c -O passfd-src.tar.gz %(passfd_source_url)s && "
513 "wget -q -c -O ns3-src.tar.gz %(ns3_source_url)s && "
514 "unzip -n pybindgen-src.zip && " # Do not overwrite files, to exploit the case when it has already been built
515 "unzip -n pygccxml-1.0.0.zip && "
516 "mkdir -p ns3-src && "
517 "mkdir -p passfd-src && "
518 "tar xzf ns3-src.tar.gz --strip-components=1 -C ns3-src && "
519 "tar xzf passfd-src.tar.gz --strip-components=1 -C passfd-src && "
520 "rm -rf target && " # mv doesn't like unclean targets
521 "mkdir -p target && "
522 "cd pygccxml-1.0.0 && "
523 "rm -rf unittests docs && " # pygccxml has ~100M of unit tests - excessive - docs aren't needed either
524 "python setup.py build && "
525 "python setup.py install --install-lib ${BUILD}/target && "
526 "python setup.py clean && "
527 "cd ../pybindgen-0.15.0 && "
528 "export PYTHONPATH=$PYTHONPATH:${BUILD}/target && "
529 "./waf configure --prefix=${BUILD}/target -d release && "
533 "mv -f ${BUILD}/target/lib/python*/site-packages/pybindgen ${BUILD}/target/. && "
534 "rm -rf ${BUILD}/target/lib && "
535 "cd ../passfd-src && "
536 "python setup.py build && "
537 "python setup.py install --install-lib ${BUILD}/target && "
538 "python setup.py clean && "
540 "./waf configure --prefix=${BUILD}/target -d release --disable-examples --high-precision-as-double && "
546 pybindgen_source_url = server.shell_escape(pybindgen_source_url),
547 pygccxml_source_url = server.shell_escape(pygccxml_source_url),
548 ns3_source_url = server.shell_escape(ns3_source_url),
549 passfd_source_url = server.shell_escape(passfd_source_url),
552 # Just move ${BUILD}/target
556 " python -c 'import pygccxml, pybindgen, passfd' && "
557 " test -f lib/_ns3.so && "
558 " test -f lib/libns3.so "
560 # Not working, reinstall
561 "( for i in ${BUILD}/target/* ; do rm -rf ${SOURCES}/${i##*/} ; done ) && " # mv doesn't like unclean targets
562 "mv -f ${BUILD}/target/* ${SOURCES}"
566 # Set extra environment paths
567 self.env['NEPI_NS3BINDINGS'] = "${SOURCES}/lib"
568 self.env['NEPI_NS3LIBRARY'] = "${SOURCES}/lib/libns3.so"
572 if self._tarball is None:
573 shared_tar = self._shared_nepi_tar and self._shared_nepi_tar()
574 if shared_tar is not None:
575 self._tarball = shared_tar
577 # Build an ad-hoc tarball
582 shared_tar = tempfile.NamedTemporaryFile(prefix='nepi-src-', suffix='.tar.gz')
584 proc = subprocess.Popen(
585 ["tar", "czf", shared_tar.name,
586 '-C', os.path.join(os.path.dirname(os.path.dirname(nepi.__file__)),'.'),
588 stdout = open("/dev/null","w"),
589 stdin = open("/dev/null","r"))
592 raise RuntimeError, "Failed to create nepi tarball"
594 self._tarball = self._shared_nepi_tar = shared_tar