blob: ea860f56cead8a50a88d667e178276c5175fa2e7 [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
132def run(command, timeout=None):
133 """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")
161
162 start_time= time.time()
163 if timeout:
164 stop_time= start_time + timeout
165 time_left= stop_time - time.time()
166 while time_left > 0:
167 # select will return when stdout is ready
168 # (including when it is EOF, that is the
169 # process has terminated).
170 (retval, tmp, tmp) = select.select(
171 [sp.stdout], [], [], time_left)
172 if len(retval):
173 # os.read() has to be used instead of
174 # sp.stdout.read() which will
175 # otherwise block
176 result.stdout += os.read(
177 sp.stdout.fileno(), 1024)
178
179 (pid, exit_status_indication) = os.waitpid(
180 sp.pid, os.WNOHANG)
181 if pid:
182 stop_time= time.time()
183 time_left= stop_time - time.time()
184
185 # the process has not terminated within timeout,
186 # kill it via an escalating series of signals.
187 if not pid:
188 signal_queue = [signal.SIGTERM, signal.SIGKILL]
189 for sig in signal_queue:
190 try:
191 os.kill(sp.pid, sig)
192 # handle race condition in which
193 # process died before we could kill it.
194 except OSError:
195 pass
196
197 for i in range(5):
198 (pid, exit_status_indication
199 ) = os.waitpid(sp.pid,
200 os.WNOHANG)
201 if pid:
202 break
203 else:
204 time.sleep(1)
205 if pid:
206 break
207 else:
208 exit_status_indication = os.waitpid(sp.pid, 0)[1]
209
210 result.duration = time.time() - start_time
211 result.aborted = exit_status_indication & 127
212 if result.aborted:
213 result.exit_status= None
214 else:
215 result.exit_status= exit_status_indication / 256
216 result.stdout += sp.stdout.read()
217 result.stderr = sp.stderr.read()
218
219 if result.exit_status > 0:
220 raise errors.AutoservRunError("command execution error",
221 result)
222
223 return result
224
225
226def get_tmp_dir():
227 """Return the pathname of a directory on the host suitable
228 for temporary file storage.
229
230 The directory and its content will be deleted automatically
231 at the end of the program execution if they are still present.
232 """
233 global __tmp_dirs
234
235 dir_name= tempfile.mkdtemp(prefix="autoserv-")
236 __tmp_dirs.append(dir_name)
237 return dir_name
238
239
240@atexit.register
241def __clean_tmp_dirs():
242 """Erase temporary directories that were created by the get_tmp_dir()
243 function and that are still present.
244 """
245 global __tmp_dirs
246
247 for dir in __tmp_dirs:
248 shutil.rmtree(dir)
249 __tmp_dirs= []
mblighc8949b82007-07-23 16:33:58 +0000250
251
252def unarchive(host, source_material):
253 """Uncompress and untar an archive on a host.
254
255 If the "source_material" is compresses (according to the file
256 extension) it will be uncompressed. Supported compression formats
257 are gzip and bzip2. Afterwards, if the source_material is a tar
258 archive, it will be untarred.
259
260 Args:
261 host: the host object on which the archive is located
262 source_material: the path of the archive on the host
263
264 Returns:
265 The file or directory name of the unarchived source material.
266 If the material is a tar archive, it will be extracted in the
267 directory where it is and the path returned will be the first
268 entry in the archive, assuming it is the topmost directory.
269 If the material is not an archive, nothing will be done so this
270 function is "harmless" when it is "useless".
271 """
272 # uncompress
273 if (source_material.endswith(".gz") or
274 source_material.endswith(".gzip")):
275 host.run('gunzip "%s"' % (sh_escape(source_material)))
276 source_material= ".".join(source_material.split(".")[:-1])
277 elif source_material.endswith("bz2"):
278 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
279 source_material= ".".join(source_material.split(".")[:-1])
280
281 # untar
282 if source_material.endswith(".tar"):
283 retval= host.run('tar -C "%s" -xvf "%s"' % (
284 sh_escape(os.path.dirname(source_material)),
285 sh_escape(source_material),))
286 source_material= os.path.join(os.path.dirname(source_material),
287 retval.stdout.split()[0])
288
289 return source_material