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],)
142 print >>sys.stderr, "Setting up", self
148 def async_setup(self):
149 if not self._setuper:
154 self._setuper._exc.append(sys.exc_info())
155 self._setuper = threading.Thread(
157 self._setuper._exc = []
158 self._setuper.start()
160 def async_setup_wait(self):
162 print >>sys.stderr, "Waiting for", self, "to be setup"
166 if self._setuper._exc:
167 exctyp,exval,exctrace = self._setuper._exc[0]
168 raise exctyp,exval,exctrace
170 raise RuntimeError, "Failed to setup application"
174 def _make_home(self):
175 # Make sure all the paths are created where
176 # they have to be created for deployment
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) }
183 except RuntimeError, e:
184 raise RuntimeError, "Failed to set up application %s: %s %s" % (self.home_path, e.args[0], e.args[1],)
187 # Write program input
190 cStringIO.StringIO(self.stdin),
191 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
192 os.path.join(self.home_path, 'stdin') ),
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],)
198 def _replace_paths(self, command):
200 Replace all special path tags with shell-escaped actual paths.
202 # need to append ${HOME} if paths aren't absolute, to MAKE them absolute.
203 root = '' if self.home_path.startswith('/') else "${HOME}/"
205 .replace("${SOURCES}", root+server.shell_escape(self.home_path))
206 .replace("${BUILD}", root+server.shell_escape(os.path.join(self.home_path,'build'))) )
208 def _launch_build(self):
209 if self._master is not None:
210 self._do_install_keys()
211 buildscript = self._do_build_slave()
213 buildscript = self._do_build_master()
215 if buildscript is not None:
216 # upload build script
220 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
221 os.path.join(self.home_path, 'nepi-build.sh') )
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],)
228 self._do_launch_build()
230 def _finish_build(self):
231 self._do_wait_build()
234 def _do_build_slave(self):
235 if not self.sources and not self.build:
238 # Create build script
242 sources = self.sources.split(' ')
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
251 "%s@%s:%s" % (self._master.node.slicename, self._master.node.hostname,
252 os.path.join(self._master.home_path, 'build.tar.gz'),)
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" % \
259 'prk' : server.shell_escape(self._master_prk_name),
260 'puk' : server.shell_escape(self._master_puk_name),
263 kill_agent = "kill $SSH_AGENT_PID"
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),
272 syncfiles = "scp -p -o UserKnownHostsFile=%(hostkey)s %(files)s ." % {
273 'hostkey' : 'master_known_hosts',
274 'files' : ' '.join(files),
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,)
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),
286 slavescript = "( ( %(launch_agent)s && %(waitmaster)s && %(syncfiles)s && %(kill_agent)s && %(cleanup)s ) || %(cleanup)s )" % {
287 'waitmaster' : waitmaster,
288 'syncfiles' : syncfiles,
290 'kill_agent' : kill_agent,
291 'launch_agent' : launch_agent,
292 'home' : server.shell_escape(self.home_path),
295 return cStringIO.StringIO(slavescript)
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),
304 (out,err),proc = rspawn.remote_spawn(
306 pidfile = 'build-pid',
307 home = self.home_path,
310 stderr = rspawn.STDOUT,
312 host = self.node.hostname,
314 user = self.node.slicename,
316 ident_key = self.node.ident_path,
317 server_key = self.node.server_key
321 raise RuntimeError, "Failed to set up build slave %s: %s %s" % (self.home_path, out,err,)
327 pidtuple = rspawn.remote_check_pid(
328 os.path.join(self.home_path,'build-pid'),
329 host = self.node.hostname,
331 user = self.node.slicename,
333 ident_key = self.node.ident_path,
334 server_key = self.node.server_key
339 self._build_pid, self._build_ppid = pidtuple
343 delay = min(30,delay*1.2)
345 raise RuntimeError, "Failed to set up build slave %s: cannot get pid" % (self.home_path,)
347 print >>sys.stderr, "Deploying", self
349 def _do_wait_build(self):
350 pid = self._build_pid
351 ppid = self._build_ppid
357 status = rspawn.remote_status(
359 host = self.node.hostname,
361 user = self.node.slicename,
363 ident_key = self.node.ident_path,
364 server_key = self.node.server_key
367 if status is not rspawn.RUNNING:
368 self._build_pid = self._build_ppid = None
372 print >>sys.stderr, "Waiting for", self, "to finish building",
373 if self._master is not None:
374 print >>sys.stderr, "(build master)"
376 print >>sys.stderr, "(build slave)"
379 time.sleep(delay*(0.5+random.random()))
380 delay = min(30,delay*1.2)
383 (out, err), proc = self._popen_ssh_command(
384 "cat %(token_path)s" % {
385 'token_path' : os.path.join(self.home_path, 'build.token'),
389 if not proc.wait() and out:
390 slave_token = out.strip()
392 if slave_token != self._master_token:
393 # Get buildlog for the error message
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'),
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)
409 print >>sys.stderr, "Built", self
411 def _do_kill_build(self):
412 pid = self._build_pid
413 ppid = self._build_ppid
416 print >>sys.stderr, "Killing build of", self
419 host = self.node.hostname,
421 user = self.node.slicename,
423 ident_key = self.node.ident_path
427 def _do_build_master(self):
428 if not self.sources and not self.build and not self.buildDepends:
432 sources = self.sources.split(' ')
438 "%s@%s:%s" % (self.node.slicename, self.node.hostname,
439 os.path.join(self.home_path,'.'),)
441 except RuntimeError, e:
442 raise RuntimeError, "Failed upload source file %r: %s %s" \
443 % (sources, e.args[0], e.args[1],)
445 buildscript = cStringIO.StringIO()
447 if self.buildDepends:
448 # Install build dependencies
450 "sudo -S yum -y install %(packages)s\n" % {
451 'packages' : self.buildDepends
459 "mkdir -p build && ( cd build && ( %(command)s ) )\n" % {
460 'command' : self._replace_paths(self.build),
461 'home' : server.shell_escape(self.home_path),
466 buildscript.write("tar czf build.tar.gz build\n")
469 buildscript.write("echo %(master_token)s > build.token" % {
470 'master_token' : server.shell_escape(self._master_token)
477 def _do_install(self):
479 print >>sys.stderr, "Installing", self
481 # Install application
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 )" % \
486 'command' : self._replace_paths(self.install),
487 'home' : server.shell_escape(self.home_path),
490 except RuntimeError, e:
491 raise RuntimeError, "Failed install build sources: %s %s" % (e.args[0], e.args[1],)
493 def set_master(self, master):
494 self._master = master
496 def install_keys(self, prk, puk, passphrase):
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)
504 def _do_install_keys(self):
505 prk = self._master_prk
506 puk = self._master_puk
510 [ prk.name, puk.name ],
511 '%s@%s:%s' % (self.node.slicename, self.node.hostname, self.home_path )
513 except RuntimeError, e:
514 raise RuntimeError, "Failed to set up application deployment keys: %s %s" \
515 % (e.args[0], e.args[1],)
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") )
525 except RuntimeError, e:
526 raise RuntimeError, "Failed to set up application deployment keys: %s %s" \
527 % (e.args[0], e.args[1],)
530 self._master_prk = None
531 self._master_puk = None
534 # make sure there's no leftover build processes
535 self._do_kill_build()
538 def _popen_scp(self, src, dst, retry = True):
539 (out,err),proc = server.popen_scp(
544 ident_key = self.node.ident_path,
545 server_key = self.node.server_key
548 if server.eintr_retry(proc.wait)():
549 raise RuntimeError, (out, err)
550 return (out, err), proc
554 def _popen_ssh_command(self, command, retry = True, noerrors=False):
555 (out,err),proc = server.popen_ssh_command(
557 host = self.node.hostname,
559 user = self.node.slicename,
561 ident_key = self.node.ident_path,
562 server_key = self.node.server_key
565 if server.eintr_retry(proc.wait)():
567 raise RuntimeError, (out, err)
568 return (out, err), proc
570 class Application(Dependency):
572 An application also has dependencies, but also a command to be ran and monitored.
574 It adds the output of that command as traces.
577 TRACES = ('stdout','stderr','buildlog')
579 def __init__(self, api=None):
580 super(Application,self).__init__(api)
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
597 # Do not add to the python path of nodes
598 self.add_to_path = False
601 return "%s<command:%s%s>" % (
602 self.__class__.__name__,
603 "sudo " if self.sudo else "",
608 print >>sys.stderr, "Starting", self
610 # Create shell script with the command
611 # This way, complex commands and scripts can be ran seamlessly
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])
617 command.write('export PATH=$PATH:%s\n' % (
618 ':'.join(["${HOME}/"+server.shell_escape(s) for s in self.node.pythonpath])
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)
630 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
631 os.path.join(self.home_path, "app.sh"))
633 except RuntimeError, e:
634 raise RuntimeError, "Failed to set up application: %s %s" \
635 % (e.args[0], e.args[1],)
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"),
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',
649 host = self.node.hostname,
651 user = self.node.slicename,
653 ident_key = self.node.ident_path,
654 server_key = self.node.server_key
658 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
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,
670 user = self.node.slicename,
672 ident_key = self.node.ident_path,
673 server_key = self.node.server_key
677 self._pid, self._ppid = pidtuple
681 if not self._started:
682 return STATUS_NOT_STARTED
683 elif not self._pid or not self._ppid:
684 return STATUS_NOT_STARTED
686 status = rspawn.remote_status(
687 self._pid, self._ppid,
688 host = self.node.hostname,
690 user = self.node.slicename,
692 ident_key = self.node.ident_path,
693 server_key = self.node.server_key
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
704 return STATUS_NOT_STARTED
707 status = self.status()
708 if status == STATUS_RUNNING:
709 # kill by ppid+pid - SIGTERM first, then try SIGKILL
711 self._pid, self._ppid,
712 host = self.node.hostname,
714 user = self.node.slicename,
716 ident_key = self.node.ident_path,
717 server_key = self.node.server_key
719 print >>sys.stderr, "Killed", self
722 class NepiDependency(Dependency):
724 This dependency adds nepi itself to the python path,
725 so that you may run testbeds within PL nodes.
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
733 def __init__(self, api = None):
734 super(NepiDependency, self).__init__(api)
738 self.depends = 'python python-ipaddr python-setuptools'
740 # our sources are in our ad-hoc tarball
741 self.sources = self.tarball.name
743 tarname = os.path.basename(self.tarball.name)
745 # it's already built - just move the tarball into place
746 self.build = "mv -f ${SOURCES}/%s ." % (tarname,)
748 # unpack it into sources, and we're done
749 self.install = "tar xzf ${BUILD}/%s -C .." % (tarname,)
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
758 # Build an ad-hoc tarball
763 shared_tar = tempfile.NamedTemporaryFile(prefix='nepi-src-', suffix='.tar.gz')
765 proc = subprocess.Popen(
766 ["tar", "czf", shared_tar.name,
767 '-C', os.path.join(os.path.dirname(os.path.dirname(nepi.__file__)),'.'),
769 stdout = open("/dev/null","w"),
770 stdin = open("/dev/null","r"))
773 raise RuntimeError, "Failed to create nepi tarball"
775 self._tarball = self._shared_nepi_tar = shared_tar
779 class NS3Dependency(Dependency):
781 This dependency adds NS3 libraries to the library paths,
782 so that you may run the NS3 testbed within PL nodes.
784 You'll also need the NepiDependency.
787 def __init__(self, api = None):
788 super(NS3Dependency, self).__init__(api)
790 self.buildDepends = 'make waf gcc gcc-c++ gccxml unzip'
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"
800 " python -c 'import pygccxml, pybindgen, passfd' && "
801 " test -f lib/_ns3.so && "
802 " test -f lib/libns3.so "
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 && "
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 && "
835 "./waf configure --prefix=${BUILD}/target -d release --disable-examples --high-precision-as-double && "
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),
847 # Just move ${BUILD}/target
851 " python -c 'import pygccxml, pybindgen, passfd' && "
852 " test -f lib/_ns3.so && "
853 " test -f lib/libns3.so "
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}"
861 # Set extra environment paths
862 self.env['NEPI_NS3BINDINGS'] = "${SOURCES}/lib"
863 self.env['NEPI_NS3LIBRARY'] = "${SOURCES}/lib/libns3.so"
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
872 # Build an ad-hoc tarball
877 shared_tar = tempfile.NamedTemporaryFile(prefix='nepi-src-', suffix='.tar.gz')
879 proc = subprocess.Popen(
880 ["tar", "czf", shared_tar.name,
881 '-C', os.path.join(os.path.dirname(os.path.dirname(nepi.__file__)),'.'),
883 stdout = open("/dev/null","w"),
884 stdin = open("/dev/null","r"))
887 raise RuntimeError, "Failed to create nepi tarball"
889 self._tarball = self._shared_nepi_tar = shared_tar