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
50 self.add_to_path = True
52 # Those are filled when the app is configured
55 # Those are filled when an actual node is connected
58 # Those are filled when the app is started
59 # Having both pid and ppid makes it harder
60 # for pid rollover to induce tracking mistakes
69 self.__class__.__name__,
70 ' '.join(list(self.depends or [])
71 + list(self.sources or []))
75 if self.home_path is None:
76 raise AssertionError, "Misconfigured application: missing home path"
77 if self.node.ident_path is None or not os.access(self.node.ident_path, os.R_OK):
78 raise AssertionError, "Misconfigured application: missing slice SSH key"
80 raise AssertionError, "Misconfigured application: unconnected node"
81 if self.node.hostname is None:
82 raise AssertionError, "Misconfigured application: misconfigured node"
83 if self.node.slicename is None:
84 raise AssertionError, "Misconfigured application: unspecified slice"
86 def remote_trace_path(self, whichtrace):
87 if whichtrace in self.TRACES:
88 tracefile = os.path.join(self.home_path, whichtrace)
94 def sync_trace(self, local_dir, whichtrace):
95 tracefile = self.remote_trace_path(whichtrace)
99 local_path = os.path.join(local_dir, tracefile)
101 # create parent local folders
102 proc = subprocess.Popen(
103 ["mkdir", "-p", os.path.dirname(local_path)],
104 stdout = open("/dev/null","w"),
105 stdin = open("/dev/null","r"))
108 raise RuntimeError, "Failed to synchronize trace"
111 (out,err),proc = server.popen_scp(
112 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
117 ident_key = self.node.ident_path,
118 server_key = self.node.server_key
122 raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
132 def async_setup(self):
133 if not self._setuper:
134 self._setuper = threading.Thread(
136 self._setuper.start()
138 def async_setup_wait(self):
143 raise RuntimeError, "Failed to setup application"
147 def _make_home(self):
148 # Make sure all the paths are created where
149 # they have to be created for deployment
150 (out,err),proc = server.popen_ssh_command(
151 "mkdir -p %s" % (server.shell_escape(self.home_path),),
152 host = self.node.hostname,
154 user = self.node.slicename,
156 ident_key = self.node.ident_path,
157 server_key = self.node.server_key
161 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
165 # Write program input
166 (out,err),proc = server.popen_scp(
167 cStringIO.StringIO(self.stdin),
168 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
169 os.path.join(self.home_path, 'stdin') ),
172 ident_key = self.node.ident_path,
173 server_key = self.node.server_key
177 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
179 def _replace_paths(self, command):
181 Replace all special path tags with shell-escaped actual paths.
183 # need to append ${HOME} if paths aren't absolute, to MAKE them absolute.
184 root = '' if self.home_path.startswith('/') else "${HOME}/"
186 .replace("${SOURCES}", root+server.shell_escape(self.home_path))
187 .replace("${BUILD}", root+server.shell_escape(os.path.join(self.home_path,'build'))) )
191 sources = self.sources.split(' ')
194 (out,err),proc = server.popen_scp(
196 "%s@%s:%s" % (self.node.slicename, self.node.hostname,
197 os.path.join(self.home_path,'.'),),
198 ident_key = self.node.ident_path,
199 server_key = self.node.server_key
203 raise RuntimeError, "Failed upload source file %r: %s %s" % (source, out,err,)
205 if self.buildDepends:
206 # Install build dependencies
207 (out,err),proc = server.popen_ssh_command(
208 "sudo -S yum -y install %(packages)s" % {
209 'packages' : self.buildDepends
211 host = self.node.hostname,
213 user = self.node.slicename,
215 ident_key = self.node.ident_path,
216 server_key = self.node.server_key
220 raise RuntimeError, "Failed instal build dependencies: %s %s" % (out,err,)
225 (out,err),proc = server.popen_ssh_command(
226 "cd %(home)s && mkdir -p build && cd build && %(command)s" % {
227 'command' : self._replace_paths(self.build),
228 'home' : server.shell_escape(self.home_path),
230 host = self.node.hostname,
232 user = self.node.slicename,
234 ident_key = self.node.ident_path,
235 server_key = self.node.server_key
239 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
242 (out,err),proc = server.popen_ssh_command(
243 "cd %(home)s && tar czf build.tar.gz build" % {
244 'command' : self._replace_paths(self.build),
245 'home' : server.shell_escape(self.home_path),
247 host = self.node.hostname,
249 user = self.node.slicename,
251 ident_key = self.node.ident_path,
252 server_key = self.node.server_key
256 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
259 # Install application
260 (out,err),proc = server.popen_ssh_command(
261 "cd %(home)s && cd build && %(command)s" % {
262 'command' : self._replace_paths(self.install),
263 'home' : server.shell_escape(self.home_path),
265 host = self.node.hostname,
267 user = self.node.slicename,
269 ident_key = self.node.ident_path,
270 server_key = self.node.server_key
274 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
276 class Application(Dependency):
278 An application also has dependencies, but also a command to be ran and monitored.
280 It adds the output of that command as traces.
283 TRACES = ('stdout','stderr','buildlog')
285 def __init__(self, api=None):
286 super(Application,self).__init__(api)
296 # Those are filled when the app is started
297 # Having both pid and ppid makes it harder
298 # for pid rollover to induce tracking mistakes
299 self._started = False
303 # Do not add to the python path of nodes
304 self.add_to_path = False
307 return "%s<command:%s%s>" % (
308 self.__class__.__name__,
309 "sudo " if self.sudo else "",
314 # Create shell script with the command
315 # This way, complex commands and scripts can be ran seamlessly
317 command = cStringIO.StringIO()
318 command.write('export PYTHONPATH=$PYTHONPATH:%s\n' % (
319 ':'.join(["${HOME}/"+server.shell_escape(s) for s in self.node.pythonpath])
321 command.write('export PATH=$PATH:%s\n' % (
322 ':'.join(["${HOME}/"+server.shell_escape(s) for s in self.node.pythonpath])
324 command.write(self.command)
327 (out,err),proc = server.popen_scp(
329 '%s@%s:%s' % (self.node.slicename, self.node.hostname,
330 os.path.join(self.home_path, "app.sh")),
333 ident_key = self.node.ident_path,
334 server_key = self.node.server_key
338 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
340 # Start process in a "daemonized" way, using nohup and heavy
341 # stdin/out redirection to avoid connection issues
342 (out,err),proc = rspawn.remote_spawn(
343 self._replace_paths("bash ./app.sh"),
346 home = self.home_path,
347 stdin = 'stdin' if self.stdin is not None else '/dev/null',
348 stdout = 'stdout' if self.stdout else '/dev/null',
349 stderr = 'stderr' if self.stderr else '/dev/null',
352 host = self.node.hostname,
354 user = self.node.slicename,
356 ident_key = self.node.ident_path,
357 server_key = self.node.server_key
361 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
367 # NOTE: wait a bit for the pidfile to be created
368 if self._started and not self._pid or not self._ppid:
369 pidtuple = rspawn.remote_check_pid(
370 os.path.join(self.home_path,'pid'),
371 host = self.node.hostname,
373 user = self.node.slicename,
375 ident_key = self.node.ident_path,
376 server_key = self.node.server_key
380 self._pid, self._ppid = pidtuple
384 if not self._started:
385 return STATUS_NOT_STARTED
386 elif not self._pid or not self._ppid:
387 return STATUS_NOT_STARTED
389 status = rspawn.remote_status(
390 self._pid, self._ppid,
391 host = self.node.hostname,
393 user = self.node.slicename,
395 ident_key = self.node.ident_path
398 if status is rspawn.NOT_STARTED:
399 return STATUS_NOT_STARTED
400 elif status is rspawn.RUNNING:
401 return STATUS_RUNNING
402 elif status is rspawn.FINISHED:
403 return STATUS_FINISHED
406 return STATUS_NOT_STARTED
409 status = self.status()
410 if status == STATUS_RUNNING:
411 # kill by ppid+pid - SIGTERM first, then try SIGKILL
413 self._pid, self._ppid,
414 host = self.node.hostname,
416 user = self.node.slicename,
418 ident_key = self.node.ident_path,
419 server_key = self.node.server_key
422 class NepiDependency(Dependency):
424 A Dependency is in every respect like an application.
426 It depends on some packages, it may require building binaries, it must deploy
429 But it has no command. Dependencies aren't ever started, or stopped, and have
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-ipaddrn 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