blob: a8076864bcc2eaf9d7f649b2ce87b7e746ae47f9 [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
32
33__tmp_dirs= []
34
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.
mblighdc735a22007-08-02 16:54:37 +0000147
mblighdcd57a82007-07-11 23:06:47 +0000148 Returns:
149 a hosts.CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000150
mblighdcd57a82007-07-11 23:06:47 +0000151 Raises:
152 AutoservRunError: the exit code of the command
153 execution was not 0
mblighdc735a22007-08-02 16:54:37 +0000154
mblighdcd57a82007-07-11 23:06:47 +0000155 TODO(poirier): Add a "tee" option to send the command's
156 stdout and stderr to python's stdout and stderr? At
157 the moment, there is no way to see the command's
158 output as it is running.
159 TODO(poirier): Should a timeout raise an exception? Should
160 exceptions be raised at all?
161 """
162 result= hosts.CmdResult()
163 result.command= command
164 sp= subprocess.Popen(command, stdout=subprocess.PIPE,
165 stderr=subprocess.PIPE, close_fds=True, shell=True,
166 executable="/bin/bash")
mbligh0dd2ae02007-08-01 17:31:10 +0000167
168 try:
169 # We are holding ends to stdin, stdout pipes
170 # hence we need to be sure to close those fds no mater what
171 start_time= time.time()
172 if timeout:
173 stop_time= start_time + timeout
mblighdcd57a82007-07-11 23:06:47 +0000174 time_left= stop_time - time.time()
mbligh0dd2ae02007-08-01 17:31:10 +0000175 while time_left > 0:
176 # select will return when stdout is ready
177 # (including when it is EOF, that is the
178 # process has terminated).
179 (retval, tmp, tmp) = select.select(
180 [sp.stdout], [], [], time_left)
181 if len(retval):
182 # os.read() has to be used instead of
183 # sp.stdout.read() which will
184 # otherwise block
185 result.stdout += os.read(
186 sp.stdout.fileno(), 1024)
187
188 (pid, exit_status_indication) = os.waitpid(
189 sp.pid, os.WNOHANG)
190 if pid:
191 stop_time= time.time()
192 time_left= stop_time - time.time()
193
194 # the process has not terminated within timeout,
195 # kill it via an escalating series of signals.
196 if not pid:
197 signal_queue = [signal.SIGTERM, signal.SIGKILL]
198 for sig in signal_queue:
199 try:
200 os.kill(sp.pid, sig)
201 # handle race condition in which
202 # process died before we could kill it.
203 except OSError:
204 pass
205
206 for i in range(5):
207 (pid, exit_status_indication
208 ) = os.waitpid(sp.pid,
209 os.WNOHANG)
210 if pid:
211 break
212 else:
213 time.sleep(1)
mblighdcd57a82007-07-11 23:06:47 +0000214 if pid:
215 break
mbligh0dd2ae02007-08-01 17:31:10 +0000216 else:
217 exit_status_indication = os.waitpid(sp.pid, 0)[1]
218
219 result.duration = time.time() - start_time
220 result.aborted = exit_status_indication & 127
221 if result.aborted:
222 result.exit_status= None
223 else:
224 result.exit_status= exit_status_indication / 256
225 result.stdout += sp.stdout.read()
226 result.stderr = sp.stderr.read()
227
228 finally:
229 # close our ends of the pipes to the sp no matter what
230 sp.stdout.close()
231 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000232
233 if not ignore_status and result.exit_status > 0:
mblighdcd57a82007-07-11 23:06:47 +0000234 raise errors.AutoservRunError("command execution error",
235 result)
mbligh0dd2ae02007-08-01 17:31:10 +0000236
mblighdcd57a82007-07-11 23:06:47 +0000237 return result
238
239
240def get_tmp_dir():
241 """Return the pathname of a directory on the host suitable
242 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000243
mblighdcd57a82007-07-11 23:06:47 +0000244 The directory and its content will be deleted automatically
245 at the end of the program execution if they are still present.
246 """
247 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000248
mblighdcd57a82007-07-11 23:06:47 +0000249 dir_name= tempfile.mkdtemp(prefix="autoserv-")
250 __tmp_dirs.append(dir_name)
251 return dir_name
252
253
254@atexit.register
255def __clean_tmp_dirs():
256 """Erase temporary directories that were created by the get_tmp_dir()
257 function and that are still present.
258 """
259 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000260
mblighdcd57a82007-07-11 23:06:47 +0000261 for dir in __tmp_dirs:
262 shutil.rmtree(dir)
263 __tmp_dirs= []
mblighc8949b82007-07-23 16:33:58 +0000264
265
266def unarchive(host, source_material):
267 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000268
mblighc8949b82007-07-23 16:33:58 +0000269 If the "source_material" is compresses (according to the file
270 extension) it will be uncompressed. Supported compression formats
271 are gzip and bzip2. Afterwards, if the source_material is a tar
272 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000273
mblighc8949b82007-07-23 16:33:58 +0000274 Args:
275 host: the host object on which the archive is located
276 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000277
mblighc8949b82007-07-23 16:33:58 +0000278 Returns:
279 The file or directory name of the unarchived source material.
280 If the material is a tar archive, it will be extracted in the
281 directory where it is and the path returned will be the first
282 entry in the archive, assuming it is the topmost directory.
283 If the material is not an archive, nothing will be done so this
284 function is "harmless" when it is "useless".
285 """
286 # uncompress
287 if (source_material.endswith(".gz") or
288 source_material.endswith(".gzip")):
289 host.run('gunzip "%s"' % (sh_escape(source_material)))
290 source_material= ".".join(source_material.split(".")[:-1])
291 elif source_material.endswith("bz2"):
292 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
293 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000294
mblighc8949b82007-07-23 16:33:58 +0000295 # untar
296 if source_material.endswith(".tar"):
297 retval= host.run('tar -C "%s" -xvf "%s"' % (
298 sh_escape(os.path.dirname(source_material)),
299 sh_escape(source_material),))
300 source_material= os.path.join(os.path.dirname(source_material),
301 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000302
mblighc8949b82007-07-23 16:33:58 +0000303 return source_material