Replace paths on main application command too
[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         # Start process in a "daemonized" way, using nohup and heavy
74         # stdin/out redirection to avoid connection issues
75         (out,err),proc = rspawn.remote_spawn(
76             self._replace_paths(self.command),
77             
78             pidfile = './pid',
79             home = self.home_path,
80             stdin = 'stdin' if self.stdin is not None else '/dev/null',
81             stdout = 'stdout' if self.stdout else '/dev/null',
82             stderr = 'stderr' if self.stderr else '/dev/null',
83             sudo = self.sudo,
84             
85             host = self.node.hostname,
86             port = None,
87             user = self.slicename,
88             agent = None,
89             ident_key = self.ident_path,
90             server_key = self.node.server_key
91             )
92         
93         if proc.wait():
94             raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
95
96         self._started = True
97
98     def checkpid(self):            
99         # Get PID/PPID
100         # NOTE: wait a bit for the pidfile to be created
101         if self._started and not self._pid or not self._ppid:
102             pidtuple = rspawn.remote_check_pid(
103                 os.path.join(self.home_path,'pid'),
104                 host = self.node.hostname,
105                 port = None,
106                 user = self.slicename,
107                 agent = None,
108                 ident_key = self.ident_path,
109                 server_key = self.node.server_key
110                 )
111             
112             if pidtuple:
113                 self._pid, self._ppid = pidtuple
114     
115     def status(self):
116         self.checkpid()
117         if not self._started:
118             return STATUS_NOT_STARTED
119         elif not self._pid or not self._ppid:
120             return STATUS_NOT_STARTED
121         else:
122             status = rspawn.remote_status(
123                 self._pid, self._ppid,
124                 host = self.node.hostname,
125                 port = None,
126                 user = self.slicename,
127                 agent = None,
128                 ident_key = self.ident_path
129                 )
130             
131             if status is rspawn.NOT_STARTED:
132                 return STATUS_NOT_STARTED
133             elif status is rspawn.RUNNING:
134                 return STATUS_RUNNING
135             elif status is rspawn.FINISHED:
136                 return STATUS_FINISHED
137             else:
138                 # WTF?
139                 return STATUS_NOT_STARTED
140     
141     def kill(self):
142         status = self.status()
143         if status == STATUS_RUNNING:
144             # kill by ppid+pid - SIGTERM first, then try SIGKILL
145             rspawn.remote_kill(
146                 self._pid, self._ppid,
147                 host = self.node.hostname,
148                 port = None,
149                 user = self.slicename,
150                 agent = None,
151                 ident_key = self.ident_path,
152                 server_key = self.node.server_key
153                 )
154     
155     def remote_trace_path(self, whichtrace):
156         if whichtrace in ('stdout','stderr'):
157             tracefile = os.path.join(self.home_path, whichtrace)
158         else:
159             tracefile = None
160         
161         return tracefile
162     
163     def sync_trace(self, local_dir, whichtrace):
164         tracefile = self.remote_trace_path(whichtrace)
165         if not tracefile:
166             return None
167         
168         local_path = os.path.join(local_dir, tracefile)
169         
170         # create parent local folders
171         proc = subprocess.Popen(
172             ["mkdir", "-p", os.path.dirname(local_path)],
173             stdout = open("/dev/null","w"),
174             stdin = open("/dev/null","r"))
175
176         if proc.wait():
177             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
178         
179         # sync files
180         (out,err),proc = server.popen_scp(
181             '%s@%s:%s' % (self.slicename, self.node.hostname, 
182                 tracefile),
183             local_path,
184             port = None,
185             agent = None,
186             ident_key = self.ident_path,
187             server_key = self.node.server_key
188             )
189         
190         if proc.wait():
191             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
192         
193         return local_path
194     
195
196     def setup(self):
197         self._make_home()
198         self._build()
199         
200     def _make_home(self):
201         # Make sure all the paths are created where 
202         # they have to be created for deployment
203         (out,err),proc = server.popen_ssh_command(
204             "mkdir -p %s" % (server.shell_escape(self.home_path),),
205             host = self.node.hostname,
206             port = None,
207             user = self.slicename,
208             agent = None,
209             ident_key = self.ident_path,
210             server_key = self.node.server_key
211             )
212         
213         if proc.wait():
214             raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
215         
216         
217         if self.stdin:
218             # Write program input
219             (out,err),proc = server.popen_scp(
220                 cStringIO.StringIO(self.stdin),
221                 '%s@%s:%s' % (self.slicename, self.node.hostname, 
222                     os.path.join(self.home_path, 'stdin') ),
223                 port = None,
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     def _replace_paths(self, command):
233         """
234         Replace all special path tags with shell-escaped actual paths.
235         """
236         # need to append ${HOME} if paths aren't absolute, to MAKE them absolute.
237         root = '' if self.home_path.startswith('/') else "${HOME}/"
238         return ( command
239             .replace("${SOURCES}", root+server.shell_escape(self.home_path))
240             .replace("${BUILD}", root+server.shell_escape(os.path.join(self.home_path,'build'))) )
241
242     def _build(self):
243         if self.sources:
244             sources = self.sources.split(' ')
245             
246             # Copy all sources
247             for source in sources:
248                 (out,err),proc = server.popen_scp(
249                     source,
250                     "%s@%s:%s" % (self.slicename, self.node.hostname, 
251                         os.path.join(self.home_path,'.'),),
252                     ident_key = self.ident_path,
253                     server_key = self.node.server_key
254                     )
255             
256                 if proc.wait():
257                     raise RuntimeError, "Failed upload source file %r: %s %s" % (source, out,err,)
258             
259         if self.buildDepends:
260             # Install build dependencies
261             (out,err),proc = server.popen_ssh_command(
262                 "sudo -S yum -y install %(packages)s" % {
263                     'packages' : self.buildDepends
264                 },
265                 host = self.node.hostname,
266                 port = None,
267                 user = self.slicename,
268                 agent = None,
269                 ident_key = self.ident_path,
270                 server_key = self.node.server_key
271                 )
272         
273             if proc.wait():
274                 raise RuntimeError, "Failed instal build dependencies: %s %s" % (out,err,)
275         
276             
277         if self.build:
278             # Build sources
279             (out,err),proc = server.popen_ssh_command(
280                 "cd %(home)s && mkdir -p build && cd build && %(command)s" % {
281                     'command' : self._replace_paths(self.build),
282                     'home' : server.shell_escape(self.home_path),
283                 },
284                 host = self.node.hostname,
285                 port = None,
286                 user = self.slicename,
287                 agent = None,
288                 ident_key = self.ident_path,
289                 server_key = self.node.server_key
290                 )
291         
292             if proc.wait():
293                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
294
295             # Make archive
296             (out,err),proc = server.popen_ssh_command(
297                 "cd %(home)s && tar czf build.tar.gz build" % {
298                     'command' : self._replace_paths(self.build),
299                     'home' : server.shell_escape(self.home_path),
300                 },
301                 host = self.node.hostname,
302                 port = None,
303                 user = self.slicename,
304                 agent = None,
305                 ident_key = self.ident_path,
306                 server_key = self.node.server_key
307                 )
308         
309             if proc.wait():
310                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
311
312         if self.install:
313             # Install application
314             (out,err),proc = server.popen_ssh_command(
315                 "cd %(home)s && cd build && %(command)s" % {
316                     'command' : self._replace_paths(self.install),
317                     'home' : server.shell_escape(self.home_path),
318                 },
319                 host = self.node.hostname,
320                 port = None,
321                 user = self.slicename,
322                 agent = None,
323                 ident_key = self.ident_path,
324                 server_key = self.node.server_key
325                 )
326         
327             if proc.wait():
328                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
329