blob: afedfce9b7c02e02f1f1d667efaaee8a3fd6acaf [file] [log] [blame]
mblighdcd57a82007-07-11 23:06:47 +00001#!/usr/bin/python
2#
3# Copyright 2007 Google Inc. Released under the GPL v2
4
5"""Miscellaneous small functions.
6"""
7
8__author__ = """mbligh@google.com (Martin J. Bligh),
9poirier@google.com (Benjamin Poirier),
10stutsman@google.com (Ryan Stutsman)"""
11
12
13import atexit
14import os
15import os.path
16import select
17import shutil
18import signal
19import StringIO
20import subprocess
21import tempfile
22import time
23import types
24import urllib
25
26import hosts
27import errors
28
29
30__tmp_dirs= []
31
32
33def sh_escape(command):
34 """Escape special characters from a command so that it can be passed
mblighc8949b82007-07-23 16:33:58 +000035 as a double quoted (" ") string in a (ba)sh command.
mblighdcd57a82007-07-11 23:06:47 +000036
37 Args:
38 command: the command string to escape.
39
40 Returns:
41 The escaped command string. The required englobing double
42 quotes are NOT added and so should be added at some point by
43 the caller.
44
45 See also: http://www.tldp.org/LDP/abs/html/escapingsection.html
46 """
47 command= command.replace("\\", "\\\\")
48 command= command.replace("$", r'\$')
49 command= command.replace('"', r'\"')
50 command= command.replace('`', r'\`')
51 return command
52
53
54def scp_remote_escape(filename):
55 """Escape special characters from a filename so that it can be passed
56 to scp (within double quotes) as a remote file.
57
58 Bis-quoting has to be used with scp for remote files, "bis-quoting"
59 as in quoting x 2
60 scp does not support a newline in the filename
61
62 Args:
63 filename: the filename string to escape.
64
65 Returns:
66 The escaped filename string. The required englobing double
67 quotes are NOT added and so should be added at some point by
68 the caller.
69 """
70 escape_chars= r' !"$&' "'" r'()*,:;<=>?[\]^`{|}'
71
72 new_name= []
73 for char in filename:
74 if char in escape_chars:
75 new_name.append("\\%s" % (char,))
76 else:
77 new_name.append(char)
78
79 return sh_escape("".join(new_name))
80
81
82def get(location):
83 """Get a file or directory to a local temporary directory.
84
85 Args:
86 location: the source of the material to get. This source may
87 be one of:
88 * a local file or directory
89 * a URL (http or ftp)
90 * a python file-like object
91
92 Returns:
93 The location of the file or directory where the requested
94 content was saved. This will be contained in a temporary
mblighc8949b82007-07-23 16:33:58 +000095 directory on the local host. If the material to get was a
96 directory, the location will contain a trailing '/'
mblighdcd57a82007-07-11 23:06:47 +000097 """
98 tmpdir = get_tmp_dir()
99
100 # location is a file-like object
101 if hasattr(location, "read"):
102 tmpfile = os.path.join(tmpdir, "file")
103 tmpfileobj = file(tmpfile, 'w')
104 shutil.copyfileobj(location, tmpfileobj)
105 tmpfileobj.close()
106 return tmpfile
107
108 if isinstance(location, types.StringTypes):
109 # location is a URL
110 if location.startswith('http') or location.startswith('ftp'):
111 tmpfile = os.path.join(tmpdir, os.path.basename(location))
112 urllib.urlretrieve(location, tmpfile)
113 return tmpfile
114 # location is a local path
115 elif os.path.exists(os.path.abspath(location)):
116 tmpfile = os.path.join(tmpdir, os.path.basename(location))
117 if os.path.isdir(location):
118 tmpfile += '/'
119 shutil.copytree(location, tmpfile, symlinks=True)
120 return tmpfile
121 shutil.copyfile(location, tmpfile)
122 return tmpfile
123 # location is just a string, dump it to a file
124 else:
125 tmpfd, tmpfile = tempfile.mkstemp(dir=tmpdir)
126 tmpfileobj = os.fdopen(tmpfd, 'w')
127 tmpfileobj.write(location)
128 tmpfileobj.close()
129 return tmpfile
130
131
mblighcf965b02007-07-25 16:49:45 +0000132def run(command, timeout=None, ignore_status=False):
mblighdcd57a82007-07-11 23:06:47 +0000133 """Run a command on the host.
134
135 Args:
136 command: the command line string
137 timeout: time limit in seconds before attempting to
138 kill the running process. The run() function
139 will take a few seconds longer than 'timeout'
140 to complete if it has to kill the process.
141
142 Returns:
143 a hosts.CmdResult object
144
145 Raises:
146 AutoservRunError: the exit code of the command
147 execution was not 0
148
149 TODO(poirier): Add a "tee" option to send the command's
150 stdout and stderr to python's stdout and stderr? At
151 the moment, there is no way to see the command's
152 output as it is running.
153 TODO(poirier): Should a timeout raise an exception? Should
154 exceptions be raised at all?
155 """
156 result= hosts.CmdResult()
157 result.command= command
158 sp= subprocess.Popen(command, stdout=subprocess.PIPE,
159 stderr=subprocess.PIPE, close_fds=True, shell=True,
160 executable="/bin/bash")
mbligh0dd2ae02007-08-01 17:31:10 +0000161
162 try:
163 # We are holding ends to stdin, stdout pipes
164 # hence we need to be sure to close those fds no mater what
165 start_time= time.time()
166 if timeout:
167 stop_time= start_time + timeout
mblighdcd57a82007-07-11 23:06:47 +0000168 time_left= stop_time - time.time()
mbligh0dd2ae02007-08-01 17:31:10 +0000169 while time_left > 0:
170 # select will return when stdout is ready
171 # (including when it is EOF, that is the
172 # process has terminated).
173 (retval, tmp, tmp) = select.select(
174 [sp.stdout], [], [], time_left)
175 if len(retval):
176 # os.read() has to be used instead of
177 # sp.stdout.read() which will
178 # otherwise block
179 result.stdout += os.read(
180 sp.stdout.fileno(), 1024)
181
182 (pid, exit_status_indication) = os.waitpid(
183 sp.pid, os.WNOHANG)
184 if pid:
185 stop_time= time.time()
186 time_left= stop_time - time.time()
187
188 # the process has not terminated within timeout,
189 # kill it via an escalating series of signals.
190 if not pid:
191 signal_queue = [signal.SIGTERM, signal.SIGKILL]
192 for sig in signal_queue:
193 try:
194 os.kill(sp.pid, sig)
195 # handle race condition in which
196 # process died before we could kill it.
197 except OSError:
198 pass
199
200 for i in range(5):
201 (pid, exit_status_indication
202 ) = os.waitpid(sp.pid,
203 os.WNOHANG)
204 if pid:
205 break
206 else:
207 time.sleep(1)
mblighdcd57a82007-07-11 23:06:47 +0000208 if pid:
209 break
mbligh0dd2ae02007-08-01 17:31:10 +0000210 else:
211 exit_status_indication = os.waitpid(sp.pid, 0)[1]
212
213 result.duration = time.time() - start_time
214 result.aborted = exit_status_indication & 127
215 if result.aborted:
216 result.exit_status= None
217 else:
218 result.exit_status= exit_status_indication / 256
219 result.stdout += sp.stdout.read()
220 result.stderr = sp.stderr.read()
221
222 finally:
223 # close our ends of the pipes to the sp no matter what
224 sp.stdout.close()
225 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000226
227 if not ignore_status and result.exit_status > 0:
mblighdcd57a82007-07-11 23:06:47 +0000228 raise errors.AutoservRunError("command execution error",
229 result)
mbligh0dd2ae02007-08-01 17:31:10 +0000230
mblighdcd57a82007-07-11 23:06:47 +0000231 return result
232
233
234def get_tmp_dir():
235 """Return the pathname of a directory on the host suitable
236 for temporary file storage.
237
238 The directory and its content will be deleted automatically
239 at the end of the program execution if they are still present.
240 """
241 global __tmp_dirs
242
243 dir_name= tempfile.mkdtemp(prefix="autoserv-")
244 __tmp_dirs.append(dir_name)
245 return dir_name
246
247
248@atexit.register
249def __clean_tmp_dirs():
250 """Erase temporary directories that were created by the get_tmp_dir()
251 function and that are still present.
252 """
253 global __tmp_dirs
254
255 for dir in __tmp_dirs:
256 shutil.rmtree(dir)
257 __tmp_dirs= []
mblighc8949b82007-07-23 16:33:58 +0000258
259
260def unarchive(host, source_material):
261 """Uncompress and untar an archive on a host.
262
263 If the "source_material" is compresses (according to the file
264 extension) it will be uncompressed. Supported compression formats
265 are gzip and bzip2. Afterwards, if the source_material is a tar
266 archive, it will be untarred.
267
268 Args:
269 host: the host object on which the archive is located
270 source_material: the path of the archive on the host
271
272 Returns:
273 The file or directory name of the unarchived source material.
274 If the material is a tar archive, it will be extracted in the
275 directory where it is and the path returned will be the first
276 entry in the archive, assuming it is the topmost directory.
277 If the material is not an archive, nothing will be done so this
278 function is "harmless" when it is "useless".
279 """
280 # uncompress
281 if (source_material.endswith(".gz") or
282 source_material.endswith(".gzip")):
283 host.run('gunzip "%s"' % (sh_escape(source_material)))
284 source_material= ".".join(source_material.split(".")[:-1])
285 elif source_material.endswith("bz2"):
286 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
287 source_material= ".".join(source_material.split(".")[:-1])
288
289 # untar
290 if source_material.endswith(".tar"):
291 retval= host.run('tar -C "%s" -xvf "%s"' % (
292 sh_escape(os.path.dirname(source_material)),
293 sh_escape(source_material),))
294 source_material= os.path.join(os.path.dirname(source_material),
295 retval.stdout.split()[0])
296
297 return source_material