Build script with application commands and run the script - supports more complex...
[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 nepi.util.server as server
10 import cStringIO
11 import subprocess
12 import rspawn
13
14 from nepi.util.constants import STATUS_NOT_STARTED, STATUS_RUNNING, \
15         STATUS_FINISHED
16
17 class Application(object):
18     def __init__(self, api=None):
19         if not api:
20             api = plcapi.PLCAPI()
21         self._api = api
22         
23         # Attributes
24         self.command = None
25         self.sudo = False
26         
27         self.build = None
28         self.install = None
29         self.depends = None
30         self.buildDepends = None
31         self.sources = None
32         
33         self.stdin = None
34         self.stdout = None
35         self.stderr = None
36         self.buildlog = None
37         
38         # Those are filled when the app is configured
39         self.home_path = None
40         self.ident_path = None
41         self.slicename = None
42         
43         # Those are filled when an actual node is connected
44         self.node = None
45         
46         # Those are filled when the app is started
47         #   Having both pid and ppid makes it harder
48         #   for pid rollover to induce tracking mistakes
49         self._started = False
50         self._pid = None
51         self._ppid = None
52     
53     def __str__(self):
54         return "%s<command:%s%s>" % (
55             self.__class__.__name__,
56             "sudo " if self.sudo else "",
57             self.command,
58         )
59     
60     def validate(self):
61         if self.home_path is None:
62             raise AssertionError, "Misconfigured application: missing home path"
63         if self.ident_path is None or not os.access(self.ident_path, os.R_OK):
64             raise AssertionError, "Misconfigured application: missing slice SSH key"
65         if self.node is None:
66             raise AssertionError, "Misconfigured application: unconnected node"
67         if self.node.hostname is None:
68             raise AssertionError, "Misconfigured application: misconfigured node"
69         if self.slicename is None:
70             raise AssertionError, "Misconfigured application: unspecified slice"
71
72     def start(self):
73         # Create shell script with the command
74         # This way, complex commands and scripts can be ran seamlessly
75         # sync files
76         (out,err),proc = server.popen_scp(
77             cStringIO.StringIO(self.command),
78             '%s@%s:%s' % (self.slicename, self.node.hostname, 
79                 os.path.join(self.home_path, "app.sh")),
80             port = None,
81             agent = None,
82             ident_key = self.ident_path,
83             server_key = self.node.server_key
84             )
85         
86         if proc.wait():
87             raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
88         
89         # Start process in a "daemonized" way, using nohup and heavy
90         # stdin/out redirection to avoid connection issues
91         (out,err),proc = rspawn.remote_spawn(
92             self._replace_paths("bash ./app.sh"),
93             
94             pidfile = './pid',
95             home = self.home_path,
96             stdin = 'stdin' if self.stdin is not None else '/dev/null',
97             stdout = 'stdout' if self.stdout else '/dev/null',
98             stderr = 'stderr' if self.stderr else '/dev/null',
99             sudo = self.sudo,
100             
101             host = self.node.hostname,
102             port = None,
103             user = self.slicename,
104             agent = None,
105             ident_key = self.ident_path,
106             server_key = self.node.server_key
107             )
108         
109         if proc.wait():
110             raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
111
112         self._started = True
113
114     def checkpid(self):            
115         # Get PID/PPID
116         # NOTE: wait a bit for the pidfile to be created
117         if self._started and not self._pid or not self._ppid:
118             pidtuple = rspawn.remote_check_pid(
119                 os.path.join(self.home_path,'pid'),
120                 host = self.node.hostname,
121                 port = None,
122                 user = self.slicename,
123                 agent = None,
124                 ident_key = self.ident_path,
125                 server_key = self.node.server_key
126                 )
127             
128             if pidtuple:
129                 self._pid, self._ppid = pidtuple
130     
131     def status(self):
132         self.checkpid()
133         if not self._started:
134             return STATUS_NOT_STARTED
135         elif not self._pid or not self._ppid:
136             return STATUS_NOT_STARTED
137         else:
138             status = rspawn.remote_status(
139                 self._pid, self._ppid,
140                 host = self.node.hostname,
141                 port = None,
142                 user = self.slicename,
143                 agent = None,
144                 ident_key = self.ident_path
145                 )
146             
147             if status is rspawn.NOT_STARTED:
148                 return STATUS_NOT_STARTED
149             elif status is rspawn.RUNNING:
150                 return STATUS_RUNNING
151             elif status is rspawn.FINISHED:
152                 return STATUS_FINISHED
153             else:
154                 # WTF?
155                 return STATUS_NOT_STARTED
156     
157     def kill(self):
158         status = self.status()
159         if status == STATUS_RUNNING:
160             # kill by ppid+pid - SIGTERM first, then try SIGKILL
161             rspawn.remote_kill(
162                 self._pid, self._ppid,
163                 host = self.node.hostname,
164                 port = None,
165                 user = self.slicename,
166                 agent = None,
167                 ident_key = self.ident_path,
168                 server_key = self.node.server_key
169                 )
170     
171     def remote_trace_path(self, whichtrace):
172         if whichtrace in ('stdout','stderr'):
173             tracefile = os.path.join(self.home_path, whichtrace)
174         else:
175             tracefile = None
176         
177         return tracefile
178     
179     def sync_trace(self, local_dir, whichtrace):
180         tracefile = self.remote_trace_path(whichtrace)
181         if not tracefile:
182             return None
183         
184         local_path = os.path.join(local_dir, tracefile)
185         
186         # create parent local folders
187         proc = subprocess.Popen(
188             ["mkdir", "-p", os.path.dirname(local_path)],
189             stdout = open("/dev/null","w"),
190             stdin = open("/dev/null","r"))
191
192         if proc.wait():
193             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
194         
195         # sync files
196         (out,err),proc = server.popen_scp(
197             '%s@%s:%s' % (self.slicename, self.node.hostname, 
198                 tracefile),
199             local_path,
200             port = None,
201             agent = None,
202             ident_key = self.ident_path,
203             server_key = self.node.server_key
204             )
205         
206         if proc.wait():
207             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
208         
209         return local_path
210     
211
212     def setup(self):
213         self._make_home()
214         self._build()
215         
216     def _make_home(self):
217         # Make sure all the paths are created where 
218         # they have to be created for deployment
219         (out,err),proc = server.popen_ssh_command(
220             "mkdir -p %s" % (server.shell_escape(self.home_path),),
221             host = self.node.hostname,
222             port = None,
223             user = self.slicename,
224             agent = None,
225             ident_key = self.ident_path,
226             server_key = self.node.server_key
227             )
228         
229         if proc.wait():
230             raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
231         
232         
233         if self.stdin:
234             # Write program input
235             (out,err),proc = server.popen_scp(
236                 cStringIO.StringIO(self.stdin),
237                 '%s@%s:%s' % (self.slicename, self.node.hostname, 
238                     os.path.join(self.home_path, 'stdin') ),
239                 port = None,
240                 agent = None,
241                 ident_key = self.ident_path,
242                 server_key = self.node.server_key
243                 )
244             
245             if proc.wait():
246                 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
247
248     def _replace_paths(self, command):
249         """
250         Replace all special path tags with shell-escaped actual paths.
251         """
252         # need to append ${HOME} if paths aren't absolute, to MAKE them absolute.
253         root = '' if self.home_path.startswith('/') else "${HOME}/"
254         return ( command
255             .replace("${SOURCES}", root+server.shell_escape(self.home_path))
256             .replace("${BUILD}", root+server.shell_escape(os.path.join(self.home_path,'build'))) )
257
258     def _build(self):
259         if self.sources:
260             sources = self.sources.split(' ')
261             
262             # Copy all sources
263             for source in sources:
264                 (out,err),proc = server.popen_scp(
265                     source,
266                     "%s@%s:%s" % (self.slicename, self.node.hostname, 
267                         os.path.join(self.home_path,'.'),),
268                     ident_key = self.ident_path,
269                     server_key = self.node.server_key
270                     )
271             
272                 if proc.wait():
273                     raise RuntimeError, "Failed upload source file %r: %s %s" % (source, out,err,)
274             
275         if self.buildDepends:
276             # Install build dependencies
277             (out,err),proc = server.popen_ssh_command(
278                 "sudo -S yum -y install %(packages)s" % {
279                     'packages' : self.buildDepends
280                 },
281                 host = self.node.hostname,
282                 port = None,
283                 user = self.slicename,
284                 agent = None,
285                 ident_key = self.ident_path,
286                 server_key = self.node.server_key
287                 )
288         
289             if proc.wait():
290                 raise RuntimeError, "Failed instal build dependencies: %s %s" % (out,err,)
291         
292             
293         if self.build:
294             # Build sources
295             (out,err),proc = server.popen_ssh_command(
296                 "cd %(home)s && mkdir -p build && cd build && %(command)s" % {
297                     'command' : self._replace_paths(self.build),
298                     'home' : server.shell_escape(self.home_path),
299                 },
300                 host = self.node.hostname,
301                 port = None,
302                 user = self.slicename,
303                 agent = None,
304                 ident_key = self.ident_path,
305                 server_key = self.node.server_key
306                 )
307         
308             if proc.wait():
309                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
310
311             # Make archive
312             (out,err),proc = server.popen_ssh_command(
313                 "cd %(home)s && tar czf build.tar.gz build" % {
314                     'command' : self._replace_paths(self.build),
315                     'home' : server.shell_escape(self.home_path),
316                 },
317                 host = self.node.hostname,
318                 port = None,
319                 user = self.slicename,
320                 agent = None,
321                 ident_key = self.ident_path,
322                 server_key = self.node.server_key
323                 )
324         
325             if proc.wait():
326                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
327
328         if self.install:
329             # Install application
330             (out,err),proc = server.popen_ssh_command(
331                 "cd %(home)s && cd build && %(command)s" % {
332                     'command' : self._replace_paths(self.install),
333                     'home' : server.shell_escape(self.home_path),
334                 },
335                 host = self.node.hostname,
336                 port = None,
337                 user = self.slicename,
338                 agent = None,
339                 ident_key = self.ident_path,
340                 server_key = self.node.server_key
341                 )
342         
343             if proc.wait():
344                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
345