2 # -*- coding: utf-8 -*-
4 from constants import TESTBED_ID
10 import nepi.util.server as server
19 from nepi.util.constants import STATUS_NOT_STARTED, STATUS_RUNNING, \
22 class Dependency(object):
24 A Dependency is in every respect like an application.
26 It depends on some packages, it may require building binaries, it must deploy
29 But it has no command. Dependencies aren't ever started, or stopped, and have
35 def __init__(self, api=None):
47 self.buildDepends = None
49 self.rpmFusion = False
57 self.add_to_path = True
59 # Those are filled when the app is configured
62 # Those are filled when an actual node is connected
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
74 # Spanning tree deployment
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
88 self.__class__.__name__,
89 ' '.join(filter(bool,(self.depends, self.sources)))
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"
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"
104 def remote_trace_path(self, whichtrace):
105 if whichtrace in self.TRACES:
106 tracefile = os.path.join(self.home_path, whichtrace)
112 def sync_trace(self, local_dir, whichtrace):
113 tracefile = self.remote_trace_path(whichtrace)
117 local_path = os.path.join(local_dir, tracefile)
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"))
126 raise RuntimeError, "Failed to synchronize trace"
131 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
135 except RuntimeError, e:
136 raise RuntimeError, "Failed to synchronize trace: %s %s" \
137 % (e.args[0], e.args[1],)
147 def async_setup(self):
148 if not self._setuper:
153 self._setuper._exc.append(sys.exc_info())
154 self._setuper = threading.Thread(
156 self._setuper._exc = []
157 self._setuper.start()
159 def async_setup_wait(self):
164 if self._setuper._exc:
165 exctyp,exval,exctrace = self._setuper._exc[0]
166 raise exctyp,exval,exctrace
168 raise RuntimeError, "Failed to setup application"
172 def _make_home(self):
173 # Make sure all the paths are created where
174 # they have to be created for deployment
177 self._popen_ssh_command(
178 "mkdir -p %(home)s && ( rm -f %(home)s/{pid,build-pid,nepi-build.sh} >/dev/null 2>&1 || /bin/true )" \
179 % { 'home' : server.shell_escape(self.home_path) }
181 except RuntimeError, e:
182 raise RuntimeError, "Failed to set up application %s: %s %s" % (self.home_path, e.args[0], e.args[1],)
185 # Write program input
188 cStringIO.StringIO(self.stdin),
189 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
190 os.path.join(self.home_path, 'stdin') ),
192 except RuntimeError, e:
193 raise RuntimeError, "Failed to set up application %s: %s %s" \
194 % (self.home_path, e.args[0], e.args[1],)
196 def _replace_paths(self, command):
198 Replace all special path tags with shell-escaped actual paths.
200 # need to append ${HOME} if paths aren't absolute, to MAKE them absolute.
201 root = '' if self.home_path.startswith('/') else "${HOME}/"
203 .replace("${SOURCES}", root+server.shell_escape(self.home_path))
204 .replace("${BUILD}", root+server.shell_escape(os.path.join(self.home_path,'build'))) )
206 def _launch_build(self):
207 if self._master is not None:
208 self._do_install_keys()
209 buildscript = self._do_build_slave()
211 buildscript = self._do_build_master()
213 if buildscript is not None:
214 # upload build script
218 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
219 os.path.join(self.home_path, 'nepi-build.sh') )
221 except RuntimeError, e:
222 raise RuntimeError, "Failed to set up application %s: %s %s" \
223 % (self.home_path, e.args[0], e.args[1],)
226 self._do_launch_build()
228 def _finish_build(self):
229 self._do_wait_build()
232 def _do_build_slave(self):
233 if not self.sources and not self.build:
236 # Create build script
240 sources = self.sources.split(' ')
242 "%s@%s:%s" % (self._master.node.slicename, self._master.node.hostname,
243 os.path.join(self._master.home_path, os.path.basename(source)),)
244 for source in sources
249 "%s@%s:%s" % (self._master.node.slicename, self._master.node.hostname,
250 os.path.join(self._master.home_path, 'build.tar.gz'),)
253 launch_agent = "{ ( echo -e '#!/bin/sh\\ncat' > .ssh-askpass ) && chmod u+x .ssh-askpass"\
254 " && export SSH_ASKPASS=$(pwd)/.ssh-askpass "\
255 " && ssh-agent > .ssh-agent.sh ; } && . ./.ssh-agent.sh && ( echo $NEPI_MASTER_PASSPHRASE | ssh-add %(prk)s ) && rm -rf %(prk)s %(puk)s" % \
257 'prk' : server.shell_escape(self._master_prk_name),
258 'puk' : server.shell_escape(self._master_puk_name),
261 kill_agent = "kill $SSH_AGENT_PID"
263 waitmaster = "{ . ./.ssh-agent.sh ; while [[ $(ssh -q -o UserKnownHostsFile=%(hostkey)s %(master)s cat %(token_path)s) != %(token)s ]] ; do sleep 5 ; done ; }" % {
264 'hostkey' : 'master_known_hosts',
265 'master' : "%s@%s" % (self._master.node.slicename, self._master.node.hostname),
266 'token_path' : os.path.join(self._master.home_path, 'build.token'),
267 'token' : server.shell_escape(self._master._master_token),
270 syncfiles = "scp -p -o UserKnownHostsFile=%(hostkey)s %(files)s ." % {
271 'hostkey' : 'master_known_hosts',
272 'files' : ' '.join(files),
275 syncfiles += " && tar xzf build.tar.gz"
276 syncfiles += " && ( echo %s > build.token )" % (server.shell_escape(self._master_token),)
277 syncfiles = "{ . ./.ssh-agent.sh ; %s ; }" % (syncfiles,)
279 cleanup = "{ . ./.ssh-agent.sh ; kill $SSH_AGENT_PID ; rm -rf %(prk)s %(puk)s master_known_hosts .ssh-askpass ; }" % {
280 'prk' : server.shell_escape(self._master_prk_name),
281 'puk' : server.shell_escape(self._master_puk_name),
284 slavescript = "( ( %(launch_agent)s && %(waitmaster)s && %(syncfiles)s && %(kill_agent)s && %(cleanup)s ) || %(cleanup)s )" % {
285 'waitmaster' : waitmaster,
286 'syncfiles' : syncfiles,
288 'kill_agent' : kill_agent,
289 'launch_agent' : launch_agent,
290 'home' : server.shell_escape(self.home_path),
293 return cStringIO.StringIO(slavescript)
295 def _do_launch_build(self):
296 script = "bash ./nepi-build.sh"
297 if self._master_passphrase:
298 script = "NEPI_MASTER_PASSPHRASE=%s %s" % (
299 server.shell_escape(self._master_passphrase),
302 (out,err),proc = rspawn.remote_spawn(
304 pidfile = 'build-pid',
305 home = self.home_path,
308 stderr = rspawn.STDOUT,
310 host = self.node.hostname,
312 user = self.node.slicename,
314 ident_key = self.node.ident_path,
315 server_key = self.node.server_key
319 raise RuntimeError, "Failed to set up build slave %s: %s %s" % (self.home_path, out,err,)
325 pidtuple = rspawn.remote_check_pid(
326 os.path.join(self.home_path,'build-pid'),
327 host = self.node.hostname,
329 user = self.node.slicename,
331 ident_key = self.node.ident_path,
332 server_key = self.node.server_key
337 self._build_pid, self._build_ppid = pidtuple
341 delay = min(30,delay*1.2)
343 raise RuntimeError, "Failed to set up build slave %s: cannot get pid" % (self.home_path,)
345 def _do_wait_build(self):
346 pid = self._build_pid
347 ppid = self._build_ppid
352 status = rspawn.remote_status(
354 host = self.node.hostname,
356 user = self.node.slicename,
358 ident_key = self.node.ident_path,
359 server_key = self.node.server_key
362 if status is not rspawn.RUNNING:
363 self._build_pid = self._build_ppid = None
366 time.sleep(delay*(0.5+random.random()))
367 delay = min(30,delay*1.2)
370 (out, err), proc = self._popen_ssh_command(
371 "cat %(token_path)s" % {
372 'token_path' : os.path.join(self.home_path, 'build.token'),
376 if not proc.wait() and out:
377 slave_token = out.strip()
379 if slave_token != self._master_token:
380 # Get buildlog for the error message
382 (buildlog, err), proc = self._popen_ssh_command(
383 "cat %(buildlog)s" % {
384 'buildlog' : os.path.join(self.home_path, 'buildlog'),
385 'buildscript' : os.path.join(self.home_path, 'nepi-build.sh'),
391 raise RuntimeError, "Failed to set up application %s: "\
392 "build failed, got wrong token from pid %s/%s "\
393 "(expected %r, got %r), see buildlog: %s" % (
394 self.home_path, pid, ppid, self._master_token, slave_token, buildlog)
396 def _do_kill_build(self):
397 pid = self._build_pid
398 ppid = self._build_ppid
403 host = self.node.hostname,
405 user = self.node.slicename,
407 ident_key = self.node.ident_path
411 def _do_build_master(self):
412 if not self.sources and not self.build and not self.buildDepends:
416 sources = self.sources.split(' ')
422 "%s@%s:%s" % (self.node.slicename, self.node.hostname,
423 os.path.join(self.home_path,'.'),)
425 except RuntimeError, e:
426 raise RuntimeError, "Failed upload source file %r: %s %s" \
427 % (sources, e.args[0], e.args[1],)
429 buildscript = cStringIO.StringIO()
431 if self.buildDepends:
432 # Install build dependencies
434 "sudo -S yum -y install %(packages)s\n" % {
435 'packages' : self.buildDepends
443 "mkdir -p build && ( cd build && ( %(command)s ) )\n" % {
444 'command' : self._replace_paths(self.build),
445 'home' : server.shell_escape(self.home_path),
450 buildscript.write("tar czf build.tar.gz build\n")
453 buildscript.write("echo %(master_token)s > build.token" % {
454 'master_token' : server.shell_escape(self._master_token)
461 def _do_install(self):
463 # Install application
465 self._popen_ssh_command(
466 "cd %(home)s && cd build && ( %(command)s ) > ${HOME}/%(home)s/installlog 2>&1 || ( tail ${HOME}/%(home)s/installlog >&2 && false )" % \
468 'command' : self._replace_paths(self.install),
469 'home' : server.shell_escape(self.home_path),
472 except RuntimeError, e:
473 raise RuntimeError, "Failed install build sources: %s %s" % (e.args[0], e.args[1],)
475 def set_master(self, master):
476 self._master = master
478 def install_keys(self, prk, puk, passphrase):
480 self._master_passphrase = passphrase
481 self._master_prk = prk
482 self._master_puk = puk
483 self._master_prk_name = os.path.basename(prk.name)
484 self._master_puk_name = os.path.basename(puk.name)
486 def _do_install_keys(self):
487 prk = self._master_prk
488 puk = self._master_puk
492 [ prk.name, puk.name ],
493 '%s@%s:%s' % (self.node.slicename, self.node.hostname, self.home_path )
495 except RuntimeError, e:
496 raise RuntimeError, "Failed to set up application deployment keys: %s %s" \
497 % (e.args[0], e.args[1],)
501 cStringIO.StringIO('%s,%s %s\n' % (
502 self._master.node.hostname, socket.gethostbyname(self._master.node.hostname),
503 self._master.node.server_key)),
504 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
505 os.path.join(self.home_path,"master_known_hosts") )
507 except RuntimeError, e:
508 raise RuntimeError, "Failed to set up application deployment keys: %s %s" \
509 % (e.args[0], e.args[1],)
512 self._master_prk = None
513 self._master_puk = None
516 # make sure there's no leftover build processes
517 self._do_kill_build()
520 def _popen_scp(self, src, dst, retry = True):
521 (out,err),proc = server.popen_scp(
526 ident_key = self.node.ident_path,
527 server_key = self.node.server_key
530 if server.eintr_retry(proc.wait)():
531 raise RuntimeError, (out, err)
532 return (out, err), proc
536 def _popen_ssh_command(self, command, retry = True, noerrors=False):
537 (out,err),proc = server.popen_ssh_command(
539 host = self.node.hostname,
541 user = self.node.slicename,
543 ident_key = self.node.ident_path,
544 server_key = self.node.server_key
547 if server.eintr_retry(proc.wait)():
549 raise RuntimeError, (out, err)
550 return (out, err), proc
552 class Application(Dependency):
554 An application also has dependencies, but also a command to be ran and monitored.
556 It adds the output of that command as traces.
559 TRACES = ('stdout','stderr','buildlog')
561 def __init__(self, api=None):
562 super(Application,self).__init__(api)
572 # Those are filled when the app is started
573 # Having both pid and ppid makes it harder
574 # for pid rollover to induce tracking mistakes
575 self._started = False
579 # Do not add to the python path of nodes
580 self.add_to_path = False
583 return "%s<command:%s%s>" % (
584 self.__class__.__name__,
585 "sudo " if self.sudo else "",
590 # Create shell script with the command
591 # This way, complex commands and scripts can be ran seamlessly
593 command = cStringIO.StringIO()
594 command.write('export PYTHONPATH=$PYTHONPATH:%s\n' % (
595 ':'.join(["${HOME}/"+server.shell_escape(s) for s in self.node.pythonpath])
597 command.write('export PATH=$PATH:%s\n' % (
598 ':'.join(["${HOME}/"+server.shell_escape(s) for s in self.node.pythonpath])
601 for envkey, envvals in self.node.env.iteritems():
602 for envval in envvals:
603 command.write('export %s=%s\n' % (envkey, envval))
604 command.write(self.command)
610 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
611 os.path.join(self.home_path, "app.sh"))
613 except RuntimeError, e:
614 raise RuntimeError, "Failed to set up application: %s %s" \
615 % (e.args[0], e.args[1],)
617 # Start process in a "daemonized" way, using nohup and heavy
618 # stdin/out redirection to avoid connection issues
619 (out,err),proc = rspawn.remote_spawn(
620 self._replace_paths("bash ./app.sh"),
623 home = self.home_path,
624 stdin = 'stdin' if self.stdin is not None else '/dev/null',
625 stdout = 'stdout' if self.stdout else '/dev/null',
626 stderr = 'stderr' if self.stderr else '/dev/null',
629 host = self.node.hostname,
631 user = self.node.slicename,
633 ident_key = self.node.ident_path,
634 server_key = self.node.server_key
638 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
644 # NOTE: wait a bit for the pidfile to be created
645 if self._started and not self._pid or not self._ppid:
646 pidtuple = rspawn.remote_check_pid(
647 os.path.join(self.home_path,'pid'),
648 host = self.node.hostname,
650 user = self.node.slicename,
652 ident_key = self.node.ident_path,
653 server_key = self.node.server_key
657 self._pid, self._ppid = pidtuple
661 if not self._started:
662 return STATUS_NOT_STARTED
663 elif not self._pid or not self._ppid:
664 return STATUS_NOT_STARTED
666 status = rspawn.remote_status(
667 self._pid, self._ppid,
668 host = self.node.hostname,
670 user = self.node.slicename,
672 ident_key = self.node.ident_path,
673 server_key = self.node.server_key
676 if status is rspawn.NOT_STARTED:
677 return STATUS_NOT_STARTED
678 elif status is rspawn.RUNNING:
679 return STATUS_RUNNING
680 elif status is rspawn.FINISHED:
681 return STATUS_FINISHED
684 return STATUS_NOT_STARTED
687 status = self.status()
688 if status == STATUS_RUNNING:
689 # kill by ppid+pid - SIGTERM first, then try SIGKILL
691 self._pid, self._ppid,
692 host = self.node.hostname,
694 user = self.node.slicename,
696 ident_key = self.node.ident_path,
697 server_key = self.node.server_key
701 class NepiDependency(Dependency):
703 This dependency adds nepi itself to the python path,
704 so that you may run testbeds within PL nodes.
707 # Class attribute holding a *weak* reference to the shared NEPI tar file
708 # so that they may share it. Don't operate on the file itself, it would
709 # be a mess, just use its path.
710 _shared_nepi_tar = None
712 def __init__(self, api = None):
713 super(NepiDependency, self).__init__(api)
717 self.depends = 'python python-ipaddr python-setuptools'
719 # our sources are in our ad-hoc tarball
720 self.sources = self.tarball.name
722 tarname = os.path.basename(self.tarball.name)
724 # it's already built - just move the tarball into place
725 self.build = "mv -f ${SOURCES}/%s ." % (tarname,)
727 # unpack it into sources, and we're done
728 self.install = "tar xzf ${BUILD}/%s -C .." % (tarname,)
732 if self._tarball is None:
733 shared_tar = self._shared_nepi_tar and self._shared_nepi_tar()
734 if shared_tar is not None:
735 self._tarball = shared_tar
737 # Build an ad-hoc tarball
742 shared_tar = tempfile.NamedTemporaryFile(prefix='nepi-src-', suffix='.tar.gz')
744 proc = subprocess.Popen(
745 ["tar", "czf", shared_tar.name,
746 '-C', os.path.join(os.path.dirname(os.path.dirname(nepi.__file__)),'.'),
748 stdout = open("/dev/null","w"),
749 stdin = open("/dev/null","r"))
752 raise RuntimeError, "Failed to create nepi tarball"
754 self._tarball = self._shared_nepi_tar = shared_tar
758 class NS3Dependency(Dependency):
760 This dependency adds NS3 libraries to the library paths,
761 so that you may run the NS3 testbed within PL nodes.
763 You'll also need the NepiDependency.
766 def __init__(self, api = None):
767 super(NS3Dependency, self).__init__(api)
769 self.buildDepends = 'make waf gcc gcc-c++ gccxml unzip'
771 # We have to download the sources, untar, build...
772 pybindgen_source_url = "http://pybindgen.googlecode.com/files/pybindgen-0.15.0.zip"
773 pygccxml_source_url = "http://leaseweb.dl.sourceforge.net/project/pygccxml/pygccxml/pygccxml-1.0/pygccxml-1.0.0.zip"
774 ns3_source_url = "http://yans.pl.sophia.inria.fr/code/hgwebdir.cgi/nepi-ns-3.9/archive/tip.tar.gz"
775 passfd_source_url = "http://yans.pl.sophia.inria.fr/code/hgwebdir.cgi/python-passfd/archive/tip.tar.gz"
779 " python -c 'import pygccxml, pybindgen, passfd' && "
780 " test -f lib/_ns3.so && "
781 " test -f lib/libns3.so "
783 # Not working, rebuild
784 "wget -q -c -O pybindgen-src.zip %(pybindgen_source_url)s && " # continue, to exploit the case when it has already been dl'ed
785 "wget -q -c -O pygccxml-1.0.0.zip %(pygccxml_source_url)s && "
786 "wget -q -c -O passfd-src.tar.gz %(passfd_source_url)s && "
787 "wget -q -c -O ns3-src.tar.gz %(ns3_source_url)s && "
788 "unzip -n pybindgen-src.zip && " # Do not overwrite files, to exploit the case when it has already been built
789 "unzip -n pygccxml-1.0.0.zip && "
790 "mkdir -p ns3-src && "
791 "mkdir -p passfd-src && "
792 "tar xzf ns3-src.tar.gz --strip-components=1 -C ns3-src && "
793 "tar xzf passfd-src.tar.gz --strip-components=1 -C passfd-src && "
794 "rm -rf target && " # mv doesn't like unclean targets
795 "mkdir -p target && "
796 "cd pygccxml-1.0.0 && "
797 "rm -rf unittests docs && " # pygccxml has ~100M of unit tests - excessive - docs aren't needed either
798 "python setup.py build && "
799 "python setup.py install --install-lib ${BUILD}/target && "
800 "python setup.py clean && "
801 "cd ../pybindgen-0.15.0 && "
802 "export PYTHONPATH=$PYTHONPATH:${BUILD}/target && "
803 "./waf configure --prefix=${BUILD}/target -d release && "
807 "mv -f ${BUILD}/target/lib/python*/site-packages/pybindgen ${BUILD}/target/. && "
808 "rm -rf ${BUILD}/target/lib && "
809 "cd ../passfd-src && "
810 "python setup.py build && "
811 "python setup.py install --install-lib ${BUILD}/target && "
812 "python setup.py clean && "
814 "./waf configure --prefix=${BUILD}/target -d release --disable-examples --high-precision-as-double && "
820 pybindgen_source_url = server.shell_escape(pybindgen_source_url),
821 pygccxml_source_url = server.shell_escape(pygccxml_source_url),
822 ns3_source_url = server.shell_escape(ns3_source_url),
823 passfd_source_url = server.shell_escape(passfd_source_url),
826 # Just move ${BUILD}/target
830 " python -c 'import pygccxml, pybindgen, passfd' && "
831 " test -f lib/_ns3.so && "
832 " test -f lib/libns3.so "
834 # Not working, reinstall
835 "( for i in ${BUILD}/target/* ; do rm -rf ${SOURCES}/${i##*/} ; done ) && " # mv doesn't like unclean targets
836 "mv -f ${BUILD}/target/* ${SOURCES}"
840 # Set extra environment paths
841 self.env['NEPI_NS3BINDINGS'] = "${SOURCES}/lib"
842 self.env['NEPI_NS3LIBRARY'] = "${SOURCES}/lib/libns3.so"
846 if self._tarball is None:
847 shared_tar = self._shared_nepi_tar and self._shared_nepi_tar()
848 if shared_tar is not None:
849 self._tarball = shared_tar
851 # Build an ad-hoc tarball
856 shared_tar = tempfile.NamedTemporaryFile(prefix='nepi-src-', suffix='.tar.gz')
858 proc = subprocess.Popen(
859 ["tar", "czf", shared_tar.name,
860 '-C', os.path.join(os.path.dirname(os.path.dirname(nepi.__file__)),'.'),
862 stdout = open("/dev/null","w"),
863 stdin = open("/dev/null","r"))
866 raise RuntimeError, "Failed to create nepi tarball"
868 self._tarball = self._shared_nepi_tar = shared_tar