Binary building support - and tests for it
[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.depends = None
29         self.buildDepends = None
30         self.sources = None
31         
32         self.stdin = None
33         self.stdout = None
34         self.stderr = None
35         self.buildlog = None
36         
37         # Those are filled when the app is configured
38         self.home_path = None
39         self.ident_path = None
40         self.slicename = None
41         
42         # Those are filled when an actual node is connected
43         self.node = None
44         
45         # Those are filled when the app is started
46         #   Having both pid and ppid makes it harder
47         #   for pid rollover to induce tracking mistakes
48         self._started = False
49         self._pid = None
50         self._ppid = None
51     
52     def __str__(self):
53         return "%s<command:%s%s>" % (
54             self.__class__.__name__,
55             "sudo " if self.sudo else "",
56             self.command,
57         )
58     
59     def validate(self):
60         if self.home_path is None:
61             raise AssertionError, "Misconfigured application: missing home path"
62         if self.ident_path is None or not os.access(self.ident_path, os.R_OK):
63             raise AssertionError, "Misconfigured application: missing slice SSH key"
64         if self.node is None:
65             raise AssertionError, "Misconfigured application: unconnected node"
66         if self.node.hostname is None:
67             raise AssertionError, "Misconfigured application: misconfigured node"
68         if self.slicename is None:
69             raise AssertionError, "Misconfigured application: unspecified slice"
70
71     def start(self):
72         # Start process in a "daemonized" way, using nohup and heavy
73         # stdin/out redirection to avoid connection issues
74         (out,err),proc = rspawn.remote_spawn(
75             self.command,
76             
77             pidfile = './pid',
78             home = self.home_path,
79             stdin = 'stdin' if self.stdin is not None else '/dev/null',
80             stdout = 'stdout' if self.stdout else '/dev/null',
81             stderr = 'stderr' if self.stderr else '/dev/null',
82             sudo = self.sudo,
83             
84             host = self.node.hostname,
85             port = None,
86             user = self.slicename,
87             agent = None,
88             ident_key = self.ident_path
89             )
90         
91         if proc.wait():
92             raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
93
94         self._started = True
95
96     def checkpid(self):            
97         # Get PID/PPID
98         # NOTE: wait a bit for the pidfile to be created
99         if self._started and not self._pid or not self._ppid:
100             pidtuple = rspawn.remote_check_pid(
101                 os.path.join(self.home_path,'pid'),
102                 host = self.node.hostname,
103                 port = None,
104                 user = self.slicename,
105                 agent = None,
106                 ident_key = self.ident_path
107                 )
108             
109             if pidtuple:
110                 self._pid, self._ppid = pidtuple
111     
112     def status(self):
113         self.checkpid()
114         if not self._started:
115             return STATUS_NOT_STARTED
116         elif not self._pid or not self._ppid:
117             return STATUS_NOT_STARTED
118         else:
119             status = rspawn.remote_status(
120                 self._pid, self._ppid,
121                 host = self.node.hostname,
122                 port = None,
123                 user = self.slicename,
124                 agent = None,
125                 ident_key = self.ident_path
126                 )
127             
128             if status is rspawn.NOT_STARTED:
129                 return STATUS_NOT_STARTED
130             elif status is rspawn.RUNNING:
131                 return STATUS_RUNNING
132             elif status is rspawn.FINISHED:
133                 return STATUS_FINISHED
134             else:
135                 # WTF?
136                 return STATUS_NOT_STARTED
137     
138     def kill(self):
139         status = self.status()
140         if status == STATUS_RUNNING:
141             # kill by ppid+pid - SIGTERM first, then try SIGKILL
142             rspawn.remote_kill(
143                 self._pid, self._ppid,
144                 host = self.node.hostname,
145                 port = None,
146                 user = self.slicename,
147                 agent = None,
148                 ident_key = self.ident_path
149                 )
150     
151     def remote_trace_path(self, whichtrace):
152         if whichtrace in ('stdout','stderr'):
153             tracefile = os.path.join(self.home_path, whichtrace)
154         else:
155             tracefile = None
156         
157         return tracefile
158     
159     def sync_trace(self, local_dir, whichtrace):
160         tracefile = self.remote_trace_path(whichtrace)
161         if not tracefile:
162             return None
163         
164         local_path = os.path.join(local_dir, tracefile)
165         
166         # create parent local folders
167         proc = subprocess.Popen(
168             ["mkdir", "-p", os.path.dirname(local_path)],
169             stdout = open("/dev/null","w"),
170             stdin = open("/dev/null","r"))
171
172         if proc.wait():
173             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
174         
175         # sync files
176         (out,err),proc = server.popen_scp(
177             '%s@%s:%s' % (self.slicename, self.node.hostname, 
178                 tracefile),
179             local_path,
180             port = None,
181             agent = None,
182             ident_key = self.ident_path
183             )
184         
185         if proc.wait():
186             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
187         
188         return local_path
189     
190
191     def setup(self):
192         self._make_home()
193         self._build()
194         
195     def _make_home(self):
196         # Make sure all the paths are created where 
197         # they have to be created for deployment
198         (out,err),proc = server.popen_ssh_command(
199             "mkdir -p %s" % (server.shell_escape(self.home_path),),
200             host = self.node.hostname,
201             port = None,
202             user = self.slicename,
203             agent = None,
204             ident_key = self.ident_path
205             )
206         
207         if proc.wait():
208             raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
209         
210         
211         if self.stdin:
212             # Write program input
213             (out,err),proc = server.popen_scp(
214                 cStringIO.StringIO(self.stdin),
215                 '%s@%s:%s' % (self.slicename, self.node.hostname, 
216                     os.path.join(self.home_path, 'stdin') ),
217                 port = None,
218                 agent = None,
219                 ident_key = self.ident_path
220                 )
221             
222             if proc.wait():
223                 raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
224
225     def _build(self):
226         if self.sources:
227             sources = self.sources.split(' ')
228             
229             # Copy all sources
230             for source in sources:
231                 (out,err),proc = server.popen_scp(
232                     source,
233                     "%s@%s:%s" % (self.slicename, self.node.hostname, 
234                         os.path.join(self.home_path,'.'),),
235                     ident_key = self.ident_path
236                     )
237             
238                 if proc.wait():
239                     raise RuntimeError, "Failed upload source file %r: %s %s" % (source, out,err,)
240             
241         if self.buildDepends:
242             # Install build dependencies
243             (out,err),proc = server.popen_ssh_command(
244                 "sudo -S yum -y install %(packages)s" % {
245                     'packages' : self.buildDepends
246                 },
247                 host = self.node.hostname,
248                 port = None,
249                 user = self.slicename,
250                 agent = None,
251                 ident_key = self.ident_path
252                 )
253         
254             if proc.wait():
255                 raise RuntimeError, "Failed instal build dependencies: %s %s" % (out,err,)
256         
257             
258         if self.build:
259             # Install build dependencies
260             (out,err),proc = server.popen_ssh_command(
261                 "cd %(home)s ; %(command)s" % {
262                     'command' : self.build,
263                     'home' : server.shell_escape(self.home_path),
264                 },
265                 host = self.node.hostname,
266                 port = None,
267                 user = self.slicename,
268                 agent = None,
269                 ident_key = self.ident_path
270                 )
271         
272             if proc.wait():
273                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
274
275