blob: fadc1681d9a4edba537415255b6d04158966bafc [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
mblighf1c52842007-10-16 15:21:38 +000015import atexit, os, select, shutil, signal, StringIO, subprocess, tempfile
mbligh9708f732007-10-18 03:18:54 +000016import time, types, urllib, re, sys
mblighdcd57a82007-07-11 23:06:47 +000017
mblighf1c52842007-10-16 15:21:38 +000018import hosts, errors
mblighdcd57a82007-07-11 23:06:47 +000019
mblighbea56822007-08-31 08:53:40 +000020# A dictionary of pid and a list of tmpdirs for that pid
21__tmp_dirs = {}
mblighdcd57a82007-07-11 23:06:47 +000022
23
24def sh_escape(command):
mblighdc735a22007-08-02 16:54:37 +000025 """
26 Escape special characters from a command so that it can be passed
mblighc8949b82007-07-23 16:33:58 +000027 as a double quoted (" ") string in a (ba)sh command.
mblighdc735a22007-08-02 16:54:37 +000028
mblighdcd57a82007-07-11 23:06:47 +000029 Args:
30 command: the command string to escape.
mblighdc735a22007-08-02 16:54:37 +000031
mblighdcd57a82007-07-11 23:06:47 +000032 Returns:
33 The escaped command string. The required englobing double
34 quotes are NOT added and so should be added at some point by
35 the caller.
mblighdc735a22007-08-02 16:54:37 +000036
mblighdcd57a82007-07-11 23:06:47 +000037 See also: http://www.tldp.org/LDP/abs/html/escapingsection.html
38 """
39 command= command.replace("\\", "\\\\")
40 command= command.replace("$", r'\$')
41 command= command.replace('"', r'\"')
42 command= command.replace('`', r'\`')
43 return command
44
45
46def scp_remote_escape(filename):
mblighdc735a22007-08-02 16:54:37 +000047 """
48 Escape special characters from a filename so that it can be passed
mblighdcd57a82007-07-11 23:06:47 +000049 to scp (within double quotes) as a remote file.
mblighdc735a22007-08-02 16:54:37 +000050
mblighdcd57a82007-07-11 23:06:47 +000051 Bis-quoting has to be used with scp for remote files, "bis-quoting"
52 as in quoting x 2
53 scp does not support a newline in the filename
mblighdc735a22007-08-02 16:54:37 +000054
mblighdcd57a82007-07-11 23:06:47 +000055 Args:
56 filename: the filename string to escape.
mblighdc735a22007-08-02 16:54:37 +000057
mblighdcd57a82007-07-11 23:06:47 +000058 Returns:
59 The escaped filename string. The required englobing double
60 quotes are NOT added and so should be added at some point by
61 the caller.
62 """
63 escape_chars= r' !"$&' "'" r'()*,:;<=>?[\]^`{|}'
mblighdc735a22007-08-02 16:54:37 +000064
mblighdcd57a82007-07-11 23:06:47 +000065 new_name= []
66 for char in filename:
67 if char in escape_chars:
68 new_name.append("\\%s" % (char,))
69 else:
70 new_name.append(char)
mblighdc735a22007-08-02 16:54:37 +000071
mblighdcd57a82007-07-11 23:06:47 +000072 return sh_escape("".join(new_name))
73
74
mbligh6e18dab2007-10-24 21:27:18 +000075def get(location, local_copy = False):
mblighdcd57a82007-07-11 23:06:47 +000076 """Get a file or directory to a local temporary directory.
mblighdc735a22007-08-02 16:54:37 +000077
mblighdcd57a82007-07-11 23:06:47 +000078 Args:
79 location: the source of the material to get. This source may
80 be one of:
81 * a local file or directory
82 * a URL (http or ftp)
83 * a python file-like object
mblighdc735a22007-08-02 16:54:37 +000084
mblighdcd57a82007-07-11 23:06:47 +000085 Returns:
86 The location of the file or directory where the requested
87 content was saved. This will be contained in a temporary
mblighc8949b82007-07-23 16:33:58 +000088 directory on the local host. If the material to get was a
89 directory, the location will contain a trailing '/'
mblighdcd57a82007-07-11 23:06:47 +000090 """
91 tmpdir = get_tmp_dir()
mblighdc735a22007-08-02 16:54:37 +000092
mblighdcd57a82007-07-11 23:06:47 +000093 # location is a file-like object
94 if hasattr(location, "read"):
95 tmpfile = os.path.join(tmpdir, "file")
96 tmpfileobj = file(tmpfile, 'w')
97 shutil.copyfileobj(location, tmpfileobj)
98 tmpfileobj.close()
99 return tmpfile
mblighdc735a22007-08-02 16:54:37 +0000100
mblighdcd57a82007-07-11 23:06:47 +0000101 if isinstance(location, types.StringTypes):
102 # location is a URL
103 if location.startswith('http') or location.startswith('ftp'):
104 tmpfile = os.path.join(tmpdir, os.path.basename(location))
105 urllib.urlretrieve(location, tmpfile)
106 return tmpfile
107 # location is a local path
108 elif os.path.exists(os.path.abspath(location)):
mbligh6e18dab2007-10-24 21:27:18 +0000109 if not local_copy:
mbligh59f70aa2007-10-25 14:44:38 +0000110 if os.path.isdir(location):
111 return location.rstrip('/') + '/'
112 else:
113 return location
mblighdcd57a82007-07-11 23:06:47 +0000114 tmpfile = os.path.join(tmpdir, os.path.basename(location))
115 if os.path.isdir(location):
116 tmpfile += '/'
117 shutil.copytree(location, tmpfile, symlinks=True)
118 return tmpfile
119 shutil.copyfile(location, tmpfile)
120 return tmpfile
121 # location is just a string, dump it to a file
122 else:
123 tmpfd, tmpfile = tempfile.mkstemp(dir=tmpdir)
124 tmpfileobj = os.fdopen(tmpfd, 'w')
125 tmpfileobj.write(location)
126 tmpfileobj.close()
127 return tmpfile
128
129
mblighcf965b02007-07-25 16:49:45 +0000130def run(command, timeout=None, ignore_status=False):
mblighdc735a22007-08-02 16:54:37 +0000131 """
132 Run a command on the host.
133
mblighdcd57a82007-07-11 23:06:47 +0000134 Args:
135 command: the command line string
136 timeout: time limit in seconds before attempting to
137 kill the running process. The run() function
138 will take a few seconds longer than 'timeout'
139 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000140 ignore_status: do not raise an exception, no matter what
141 the exit code of the command is.
142
mblighdcd57a82007-07-11 23:06:47 +0000143 Returns:
144 a hosts.CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000145
mblighdcd57a82007-07-11 23:06:47 +0000146 Raises:
147 AutoservRunError: the exit code of the command
148 execution was not 0
mblighdc735a22007-08-02 16:54:37 +0000149
mblighdcd57a82007-07-11 23:06:47 +0000150 TODO(poirier): Add a "tee" option to send the command's
151 stdout and stderr to python's stdout and stderr? At
152 the moment, there is no way to see the command's
153 output as it is running.
154 TODO(poirier): Should a timeout raise an exception? Should
155 exceptions be raised at all?
156 """
157 result= hosts.CmdResult()
158 result.command= command
159 sp= subprocess.Popen(command, stdout=subprocess.PIPE,
160 stderr=subprocess.PIPE, close_fds=True, shell=True,
161 executable="/bin/bash")
mbligh0dd2ae02007-08-01 17:31:10 +0000162
163 try:
164 # We are holding ends to stdin, stdout pipes
165 # hence we need to be sure to close those fds no mater what
166 start_time= time.time()
167 if timeout:
168 stop_time= start_time + timeout
mblighdcd57a82007-07-11 23:06:47 +0000169 time_left= stop_time - time.time()
mbligh0dd2ae02007-08-01 17:31:10 +0000170 while time_left > 0:
171 # select will return when stdout is ready
172 # (including when it is EOF, that is the
173 # process has terminated).
174 (retval, tmp, tmp) = select.select(
175 [sp.stdout], [], [], time_left)
176 if len(retval):
177 # os.read() has to be used instead of
178 # sp.stdout.read() which will
179 # otherwise block
180 result.stdout += os.read(
181 sp.stdout.fileno(), 1024)
182
183 (pid, exit_status_indication) = os.waitpid(
184 sp.pid, os.WNOHANG)
185 if pid:
186 stop_time= time.time()
187 time_left= stop_time - time.time()
188
189 # the process has not terminated within timeout,
190 # kill it via an escalating series of signals.
191 if not pid:
192 signal_queue = [signal.SIGTERM, signal.SIGKILL]
193 for sig in signal_queue:
194 try:
195 os.kill(sp.pid, sig)
196 # handle race condition in which
197 # process died before we could kill it.
198 except OSError:
199 pass
200
201 for i in range(5):
202 (pid, exit_status_indication
203 ) = os.waitpid(sp.pid,
204 os.WNOHANG)
205 if pid:
206 break
207 else:
208 time.sleep(1)
mblighdcd57a82007-07-11 23:06:47 +0000209 if pid:
210 break
mbligh0dd2ae02007-08-01 17:31:10 +0000211 else:
212 exit_status_indication = os.waitpid(sp.pid, 0)[1]
213
214 result.duration = time.time() - start_time
215 result.aborted = exit_status_indication & 127
216 if result.aborted:
217 result.exit_status= None
218 else:
219 result.exit_status= exit_status_indication / 256
220 result.stdout += sp.stdout.read()
221 result.stderr = sp.stderr.read()
222
223 finally:
224 # close our ends of the pipes to the sp no matter what
225 sp.stdout.close()
226 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000227
228 if not ignore_status and result.exit_status > 0:
mblighdcd57a82007-07-11 23:06:47 +0000229 raise errors.AutoservRunError("command execution error",
230 result)
mbligh0dd2ae02007-08-01 17:31:10 +0000231
mblighdcd57a82007-07-11 23:06:47 +0000232 return result
233
234
mbligh5f876ad2007-10-12 23:59:53 +0000235def system(command, timeout=None, ignore_status=False):
236 return run(command, timeout, ignore_status).exit_status
237
238
239def system_output(command, timeout=None, ignore_status=False):
240 return run(command, timeout, ignore_status).stdout
241
242
mblighdcd57a82007-07-11 23:06:47 +0000243def get_tmp_dir():
244 """Return the pathname of a directory on the host suitable
245 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000246
mblighdcd57a82007-07-11 23:06:47 +0000247 The directory and its content will be deleted automatically
248 at the end of the program execution if they are still present.
249 """
250 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000251
mblighdcd57a82007-07-11 23:06:47 +0000252 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000253 pid = os.getpid()
254 if not pid in __tmp_dirs:
255 __tmp_dirs[pid] = []
256 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000257 return dir_name
258
259
260@atexit.register
261def __clean_tmp_dirs():
262 """Erase temporary directories that were created by the get_tmp_dir()
263 function and that are still present.
264 """
265 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000266
mblighbea56822007-08-31 08:53:40 +0000267 pid = os.getpid()
268 if pid not in __tmp_dirs:
269 return
270 for dir in __tmp_dirs[pid]:
271 try:
272 shutil.rmtree(dir)
273 except OSError, e:
274 if e.errno == 2:
275 pass
276 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000277
278
279def unarchive(host, source_material):
280 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000281
mblighc8949b82007-07-23 16:33:58 +0000282 If the "source_material" is compresses (according to the file
283 extension) it will be uncompressed. Supported compression formats
284 are gzip and bzip2. Afterwards, if the source_material is a tar
285 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000286
mblighc8949b82007-07-23 16:33:58 +0000287 Args:
288 host: the host object on which the archive is located
289 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000290
mblighc8949b82007-07-23 16:33:58 +0000291 Returns:
292 The file or directory name of the unarchived source material.
293 If the material is a tar archive, it will be extracted in the
294 directory where it is and the path returned will be the first
295 entry in the archive, assuming it is the topmost directory.
296 If the material is not an archive, nothing will be done so this
297 function is "harmless" when it is "useless".
298 """
299 # uncompress
300 if (source_material.endswith(".gz") or
301 source_material.endswith(".gzip")):
302 host.run('gunzip "%s"' % (sh_escape(source_material)))
303 source_material= ".".join(source_material.split(".")[:-1])
304 elif source_material.endswith("bz2"):
305 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
306 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000307
mblighc8949b82007-07-23 16:33:58 +0000308 # untar
309 if source_material.endswith(".tar"):
310 retval= host.run('tar -C "%s" -xvf "%s"' % (
311 sh_escape(os.path.dirname(source_material)),
312 sh_escape(source_material),))
313 source_material= os.path.join(os.path.dirname(source_material),
314 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000315
mblighc8949b82007-07-23 16:33:58 +0000316 return source_material
mblighf1c52842007-10-16 15:21:38 +0000317
318
319def write_keyval(dirname, dictionary):
320 keyval = open(os.path.join(dirname, 'keyval'), 'w')
321 for key in dictionary.keys():
322 value = '%s' % dictionary[key] # convert numbers to strings
323 if re.search(r'\W', key):
324 raise 'Invalid key: ' + key
325 keyval.write('%s=%s\n' % (key, str(value)))
326 keyval.close()
327
mbligh05269362007-10-16 16:58:11 +0000328
329def update_version(srcdir, preserve_srcdir, new_version, install, *args, **dargs):
330 """
331 Make sure srcdir is version new_version
332
333 If not, delete it and install() the new version.
334
335 In the preserve_srcdir case, we just check it's up to date,
336 and if not, we rerun install, without removing srcdir
337 """
338 versionfile = srcdir + '/.version'
339 install_needed = True
340
341 if os.path.exists(srcdir):
342 if os.path.exists(versionfile):
343 old_version = pickle.load(open(versionfile, 'r'))
344 if (old_version == new_version):
345 install_needed = False
346
347 if install_needed:
348 if not preserve_srcdir:
349 system('rm -rf ' + srcdir)
350 install(*args, **dargs)
351 if os.path.exists(srcdir):
352 pickle.dump(new_version, open(versionfile, 'w'))
mbligh9708f732007-10-18 03:18:54 +0000353
354
355def get_server_dir():
356 path = os.path.dirname(sys.modules['utils'].__file__)
357 return os.path.abspath(path)