blob: acc57d2b6975be7bccc0ebeda7760aaafb03de53 [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
mblighdc735a22007-08-02 16:54:37 +00005"""
6Miscellaneous small functions.
mblighdcd57a82007-07-11 23:06:47 +00007"""
8
mblighdc735a22007-08-02 16:54:37 +00009__author__ = """
10mbligh@google.com (Martin J. Bligh),
mblighdcd57a82007-07-11 23:06:47 +000011poirier@google.com (Benjamin Poirier),
mblighdc735a22007-08-02 16:54:37 +000012stutsman@google.com (Ryan Stutsman)
13"""
mblighdcd57a82007-07-11 23:06:47 +000014
15
16import atexit
17import os
18import os.path
19import select
20import shutil
21import signal
22import StringIO
23import subprocess
24import tempfile
25import time
26import types
27import urllib
28
29import hosts
30import errors
31
mblighbea56822007-08-31 08:53:40 +000032# A dictionary of pid and a list of tmpdirs for that pid
33__tmp_dirs = {}
mblighdcd57a82007-07-11 23:06:47 +000034
35
36def sh_escape(command):
mblighdc735a22007-08-02 16:54:37 +000037 """
38 Escape special characters from a command so that it can be passed
mblighc8949b82007-07-23 16:33:58 +000039 as a double quoted (" ") string in a (ba)sh command.
mblighdc735a22007-08-02 16:54:37 +000040
mblighdcd57a82007-07-11 23:06:47 +000041 Args:
42 command: the command string to escape.
mblighdc735a22007-08-02 16:54:37 +000043
mblighdcd57a82007-07-11 23:06:47 +000044 Returns:
45 The escaped command string. The required englobing double
46 quotes are NOT added and so should be added at some point by
47 the caller.
mblighdc735a22007-08-02 16:54:37 +000048
mblighdcd57a82007-07-11 23:06:47 +000049 See also: http://www.tldp.org/LDP/abs/html/escapingsection.html
50 """
51 command= command.replace("\\", "\\\\")
52 command= command.replace("$", r'\$')
53 command= command.replace('"', r'\"')
54 command= command.replace('`', r'\`')
55 return command
56
57
58def scp_remote_escape(filename):
mblighdc735a22007-08-02 16:54:37 +000059 """
60 Escape special characters from a filename so that it can be passed
mblighdcd57a82007-07-11 23:06:47 +000061 to scp (within double quotes) as a remote file.
mblighdc735a22007-08-02 16:54:37 +000062
mblighdcd57a82007-07-11 23:06:47 +000063 Bis-quoting has to be used with scp for remote files, "bis-quoting"
64 as in quoting x 2
65 scp does not support a newline in the filename
mblighdc735a22007-08-02 16:54:37 +000066
mblighdcd57a82007-07-11 23:06:47 +000067 Args:
68 filename: the filename string to escape.
mblighdc735a22007-08-02 16:54:37 +000069
mblighdcd57a82007-07-11 23:06:47 +000070 Returns:
71 The escaped filename string. The required englobing double
72 quotes are NOT added and so should be added at some point by
73 the caller.
74 """
75 escape_chars= r' !"$&' "'" r'()*,:;<=>?[\]^`{|}'
mblighdc735a22007-08-02 16:54:37 +000076
mblighdcd57a82007-07-11 23:06:47 +000077 new_name= []
78 for char in filename:
79 if char in escape_chars:
80 new_name.append("\\%s" % (char,))
81 else:
82 new_name.append(char)
mblighdc735a22007-08-02 16:54:37 +000083
mblighdcd57a82007-07-11 23:06:47 +000084 return sh_escape("".join(new_name))
85
86
87def get(location):
88 """Get a file or directory to a local temporary directory.
mblighdc735a22007-08-02 16:54:37 +000089
mblighdcd57a82007-07-11 23:06:47 +000090 Args:
91 location: the source of the material to get. This source may
92 be one of:
93 * a local file or directory
94 * a URL (http or ftp)
95 * a python file-like object
mblighdc735a22007-08-02 16:54:37 +000096
mblighdcd57a82007-07-11 23:06:47 +000097 Returns:
98 The location of the file or directory where the requested
99 content was saved. This will be contained in a temporary
mblighc8949b82007-07-23 16:33:58 +0000100 directory on the local host. If the material to get was a
101 directory, the location will contain a trailing '/'
mblighdcd57a82007-07-11 23:06:47 +0000102 """
103 tmpdir = get_tmp_dir()
mblighdc735a22007-08-02 16:54:37 +0000104
mblighdcd57a82007-07-11 23:06:47 +0000105 # location is a file-like object
106 if hasattr(location, "read"):
107 tmpfile = os.path.join(tmpdir, "file")
108 tmpfileobj = file(tmpfile, 'w')
109 shutil.copyfileobj(location, tmpfileobj)
110 tmpfileobj.close()
111 return tmpfile
mblighdc735a22007-08-02 16:54:37 +0000112
mblighdcd57a82007-07-11 23:06:47 +0000113 if isinstance(location, types.StringTypes):
114 # location is a URL
115 if location.startswith('http') or location.startswith('ftp'):
116 tmpfile = os.path.join(tmpdir, os.path.basename(location))
117 urllib.urlretrieve(location, tmpfile)
118 return tmpfile
119 # location is a local path
120 elif os.path.exists(os.path.abspath(location)):
121 tmpfile = os.path.join(tmpdir, os.path.basename(location))
122 if os.path.isdir(location):
123 tmpfile += '/'
124 shutil.copytree(location, tmpfile, symlinks=True)
125 return tmpfile
126 shutil.copyfile(location, tmpfile)
127 return tmpfile
128 # location is just a string, dump it to a file
129 else:
130 tmpfd, tmpfile = tempfile.mkstemp(dir=tmpdir)
131 tmpfileobj = os.fdopen(tmpfd, 'w')
132 tmpfileobj.write(location)
133 tmpfileobj.close()
134 return tmpfile
135
136
mblighcf965b02007-07-25 16:49:45 +0000137def run(command, timeout=None, ignore_status=False):
mblighdc735a22007-08-02 16:54:37 +0000138 """
139 Run a command on the host.
140
mblighdcd57a82007-07-11 23:06:47 +0000141 Args:
142 command: the command line string
143 timeout: time limit in seconds before attempting to
144 kill the running process. The run() function
145 will take a few seconds longer than 'timeout'
146 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000147 ignore_status: do not raise an exception, no matter what
148 the exit code of the command is.
149
mblighdcd57a82007-07-11 23:06:47 +0000150 Returns:
151 a hosts.CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000152
mblighdcd57a82007-07-11 23:06:47 +0000153 Raises:
154 AutoservRunError: the exit code of the command
155 execution was not 0
mblighdc735a22007-08-02 16:54:37 +0000156
mblighdcd57a82007-07-11 23:06:47 +0000157 TODO(poirier): Add a "tee" option to send the command's
158 stdout and stderr to python's stdout and stderr? At
159 the moment, there is no way to see the command's
160 output as it is running.
161 TODO(poirier): Should a timeout raise an exception? Should
162 exceptions be raised at all?
163 """
164 result= hosts.CmdResult()
165 result.command= command
166 sp= subprocess.Popen(command, stdout=subprocess.PIPE,
167 stderr=subprocess.PIPE, close_fds=True, shell=True,
168 executable="/bin/bash")
mbligh0dd2ae02007-08-01 17:31:10 +0000169
170 try:
171 # We are holding ends to stdin, stdout pipes
172 # hence we need to be sure to close those fds no mater what
173 start_time= time.time()
174 if timeout:
175 stop_time= start_time + timeout
mblighdcd57a82007-07-11 23:06:47 +0000176 time_left= stop_time - time.time()
mbligh0dd2ae02007-08-01 17:31:10 +0000177 while time_left > 0:
178 # select will return when stdout is ready
179 # (including when it is EOF, that is the
180 # process has terminated).
181 (retval, tmp, tmp) = select.select(
182 [sp.stdout], [], [], time_left)
183 if len(retval):
184 # os.read() has to be used instead of
185 # sp.stdout.read() which will
186 # otherwise block
187 result.stdout += os.read(
188 sp.stdout.fileno(), 1024)
189
190 (pid, exit_status_indication) = os.waitpid(
191 sp.pid, os.WNOHANG)
192 if pid:
193 stop_time= time.time()
194 time_left= stop_time - time.time()
195
196 # the process has not terminated within timeout,
197 # kill it via an escalating series of signals.
198 if not pid:
199 signal_queue = [signal.SIGTERM, signal.SIGKILL]
200 for sig in signal_queue:
201 try:
202 os.kill(sp.pid, sig)
203 # handle race condition in which
204 # process died before we could kill it.
205 except OSError:
206 pass
207
208 for i in range(5):
209 (pid, exit_status_indication
210 ) = os.waitpid(sp.pid,
211 os.WNOHANG)
212 if pid:
213 break
214 else:
215 time.sleep(1)
mblighdcd57a82007-07-11 23:06:47 +0000216 if pid:
217 break
mbligh0dd2ae02007-08-01 17:31:10 +0000218 else:
219 exit_status_indication = os.waitpid(sp.pid, 0)[1]
220
221 result.duration = time.time() - start_time
222 result.aborted = exit_status_indication & 127
223 if result.aborted:
224 result.exit_status= None
225 else:
226 result.exit_status= exit_status_indication / 256
227 result.stdout += sp.stdout.read()
228 result.stderr = sp.stderr.read()
229
230 finally:
231 # close our ends of the pipes to the sp no matter what
232 sp.stdout.close()
233 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000234
235 if not ignore_status and result.exit_status > 0:
mblighdcd57a82007-07-11 23:06:47 +0000236 raise errors.AutoservRunError("command execution error",
237 result)
mbligh0dd2ae02007-08-01 17:31:10 +0000238
mblighdcd57a82007-07-11 23:06:47 +0000239 return result
240
241
242def get_tmp_dir():
243 """Return the pathname of a directory on the host suitable
244 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000245
mblighdcd57a82007-07-11 23:06:47 +0000246 The directory and its content will be deleted automatically
247 at the end of the program execution if they are still present.
248 """
249 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000250
mblighdcd57a82007-07-11 23:06:47 +0000251 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000252 pid = os.getpid()
253 if not pid in __tmp_dirs:
254 __tmp_dirs[pid] = []
255 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000256 return dir_name
257
258
259@atexit.register
260def __clean_tmp_dirs():
261 """Erase temporary directories that were created by the get_tmp_dir()
262 function and that are still present.
263 """
264 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000265
mblighbea56822007-08-31 08:53:40 +0000266 pid = os.getpid()
267 if pid not in __tmp_dirs:
268 return
269 for dir in __tmp_dirs[pid]:
270 try:
271 shutil.rmtree(dir)
272 except OSError, e:
273 if e.errno == 2:
274 pass
275 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000276
277
278def unarchive(host, source_material):
279 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000280
mblighc8949b82007-07-23 16:33:58 +0000281 If the "source_material" is compresses (according to the file
282 extension) it will be uncompressed. Supported compression formats
283 are gzip and bzip2. Afterwards, if the source_material is a tar
284 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000285
mblighc8949b82007-07-23 16:33:58 +0000286 Args:
287 host: the host object on which the archive is located
288 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000289
mblighc8949b82007-07-23 16:33:58 +0000290 Returns:
291 The file or directory name of the unarchived source material.
292 If the material is a tar archive, it will be extracted in the
293 directory where it is and the path returned will be the first
294 entry in the archive, assuming it is the topmost directory.
295 If the material is not an archive, nothing will be done so this
296 function is "harmless" when it is "useless".
297 """
298 # uncompress
299 if (source_material.endswith(".gz") or
300 source_material.endswith(".gzip")):
301 host.run('gunzip "%s"' % (sh_escape(source_material)))
302 source_material= ".".join(source_material.split(".")[:-1])
303 elif source_material.endswith("bz2"):
304 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
305 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000306
mblighc8949b82007-07-23 16:33:58 +0000307 # untar
308 if source_material.endswith(".tar"):
309 retval= host.run('tar -C "%s" -xvf "%s"' % (
310 sh_escape(os.path.dirname(source_material)),
311 sh_escape(source_material),))
312 source_material= os.path.join(os.path.dirname(source_material),
313 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000314
mblighc8949b82007-07-23 16:33:58 +0000315 return source_material