blob: d650701cb6a494fc353c86611c43d13b10b3d9da [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
mblighf1c52842007-10-16 15:21:38 +000017import hosts, errors
mblighdcd57a82007-07-11 23:06:47 +000018
mblighbea56822007-08-31 08:53:40 +000019# A dictionary of pid and a list of tmpdirs for that pid
20__tmp_dirs = {}
mblighdcd57a82007-07-11 23:06:47 +000021
22
23def sh_escape(command):
mblighdc735a22007-08-02 16:54:37 +000024 """
25 Escape special characters from a command so that it can be passed
mblighc8949b82007-07-23 16:33:58 +000026 as a double quoted (" ") string in a (ba)sh command.
mblighdc735a22007-08-02 16:54:37 +000027
mblighdcd57a82007-07-11 23:06:47 +000028 Args:
29 command: the command string to escape.
mblighdc735a22007-08-02 16:54:37 +000030
mblighdcd57a82007-07-11 23:06:47 +000031 Returns:
32 The escaped command string. The required englobing double
33 quotes are NOT added and so should be added at some point by
34 the caller.
mblighdc735a22007-08-02 16:54:37 +000035
mblighdcd57a82007-07-11 23:06:47 +000036 See also: http://www.tldp.org/LDP/abs/html/escapingsection.html
37 """
38 command= command.replace("\\", "\\\\")
39 command= command.replace("$", r'\$')
40 command= command.replace('"', r'\"')
41 command= command.replace('`', r'\`')
42 return command
43
44
45def scp_remote_escape(filename):
mblighdc735a22007-08-02 16:54:37 +000046 """
47 Escape special characters from a filename so that it can be passed
mblighdcd57a82007-07-11 23:06:47 +000048 to scp (within double quotes) as a remote file.
mblighdc735a22007-08-02 16:54:37 +000049
mblighdcd57a82007-07-11 23:06:47 +000050 Bis-quoting has to be used with scp for remote files, "bis-quoting"
51 as in quoting x 2
52 scp does not support a newline in the filename
mblighdc735a22007-08-02 16:54:37 +000053
mblighdcd57a82007-07-11 23:06:47 +000054 Args:
55 filename: the filename string to escape.
mblighdc735a22007-08-02 16:54:37 +000056
mblighdcd57a82007-07-11 23:06:47 +000057 Returns:
58 The escaped filename string. The required englobing double
59 quotes are NOT added and so should be added at some point by
60 the caller.
61 """
62 escape_chars= r' !"$&' "'" r'()*,:;<=>?[\]^`{|}'
mblighdc735a22007-08-02 16:54:37 +000063
mblighdcd57a82007-07-11 23:06:47 +000064 new_name= []
65 for char in filename:
66 if char in escape_chars:
67 new_name.append("\\%s" % (char,))
68 else:
69 new_name.append(char)
mblighdc735a22007-08-02 16:54:37 +000070
mblighdcd57a82007-07-11 23:06:47 +000071 return sh_escape("".join(new_name))
72
73
mbligh6e18dab2007-10-24 21:27:18 +000074def get(location, local_copy = False):
mblighdcd57a82007-07-11 23:06:47 +000075 """Get a file or directory to a local temporary directory.
mblighdc735a22007-08-02 16:54:37 +000076
mblighdcd57a82007-07-11 23:06:47 +000077 Args:
78 location: the source of the material to get. This source may
79 be one of:
80 * a local file or directory
81 * a URL (http or ftp)
82 * a python file-like object
mblighdc735a22007-08-02 16:54:37 +000083
mblighdcd57a82007-07-11 23:06:47 +000084 Returns:
85 The location of the file or directory where the requested
86 content was saved. This will be contained in a temporary
mblighc8949b82007-07-23 16:33:58 +000087 directory on the local host. If the material to get was a
88 directory, the location will contain a trailing '/'
mblighdcd57a82007-07-11 23:06:47 +000089 """
90 tmpdir = get_tmp_dir()
mblighdc735a22007-08-02 16:54:37 +000091
mblighdcd57a82007-07-11 23:06:47 +000092 # location is a file-like object
93 if hasattr(location, "read"):
94 tmpfile = os.path.join(tmpdir, "file")
95 tmpfileobj = file(tmpfile, 'w')
96 shutil.copyfileobj(location, tmpfileobj)
97 tmpfileobj.close()
98 return tmpfile
mblighdc735a22007-08-02 16:54:37 +000099
mblighdcd57a82007-07-11 23:06:47 +0000100 if isinstance(location, types.StringTypes):
101 # location is a URL
102 if location.startswith('http') or location.startswith('ftp'):
103 tmpfile = os.path.join(tmpdir, os.path.basename(location))
104 urllib.urlretrieve(location, tmpfile)
105 return tmpfile
106 # location is a local path
107 elif os.path.exists(os.path.abspath(location)):
mbligh6e18dab2007-10-24 21:27:18 +0000108 if not local_copy:
mbligh59f70aa2007-10-25 14:44:38 +0000109 if os.path.isdir(location):
110 return location.rstrip('/') + '/'
111 else:
112 return location
mblighdcd57a82007-07-11 23:06:47 +0000113 tmpfile = os.path.join(tmpdir, os.path.basename(location))
114 if os.path.isdir(location):
115 tmpfile += '/'
116 shutil.copytree(location, tmpfile, symlinks=True)
117 return tmpfile
118 shutil.copyfile(location, tmpfile)
119 return tmpfile
120 # location is just a string, dump it to a file
121 else:
122 tmpfd, tmpfile = tempfile.mkstemp(dir=tmpdir)
123 tmpfileobj = os.fdopen(tmpfd, 'w')
124 tmpfileobj.write(location)
125 tmpfileobj.close()
126 return tmpfile
127
128
mbligh0e4613b2007-10-29 16:55:07 +0000129def _process_output(pipe, fbuffer, teefile=None, use_os_read=True):
130 if use_os_read:
131 data = os.read(pipe.fileno(), 1024)
132 else:
133 data = pipe.read()
134 fbuffer.write(data)
135 if teefile:
136 teefile.write(data)
137 teefile.flush()
138
139
140def _wait_for_command(subproc, start_time, timeout, stdout_file, stderr_file,
141 stdout_tee, stderr_tee):
142 if timeout:
143 stop_time = start_time + timeout
144 time_left = stop_time - time.time()
145 else:
146 time_left = None # so that select never times out
147 while not timeout or time_left > 0:
148 # select will return when stdout is ready (including when it is
149 # EOF, that is the process has terminated).
150 ready, _, _ = select.select([subproc.stdout, subproc.stderr],
151 [], [], time_left)
152 # os.read() has to be used instead of
153 # subproc.stdout.read() which will otherwise block
154 if subproc.stdout in ready:
155 _process_output(subproc.stdout, stdout_file,
156 stdout_tee)
157 if subproc.stderr in ready:
158 _process_output(subproc.stderr, stderr_file,
159 stderr_tee)
160
161 pid, exit_status_indication = os.waitpid(subproc.pid,
162 os.WNOHANG)
163 if pid:
164 break
165 if timeout:
166 time_left = stop_time - time.time()
167
168 # the process has not terminated within timeout,
169 # kill it via an escalating series of signals.
170 if not pid:
171 signal_queue = [signal.SIGTERM, signal.SIGKILL]
172 for sig in signal_queue:
173 try:
174 os.kill(subproc.pid, sig)
175 # handle race condition in which
176 # process died before we could kill it.
177 except OSError:
178 pass
179
180 for i in range(5):
181 pid, exit_status_indication = (
182 os.waitpid(subproc.pid, os.WNOHANG))
183 if pid:
184 return exit_status_indication
185 else:
186 time.sleep(1)
187 return exit_status_indication
188
189
190def run(command, timeout=None, ignore_status=False,
191 stdout_tee=None, stderr_tee=None):
mblighdc735a22007-08-02 16:54:37 +0000192 """
193 Run a command on the host.
194
mblighdcd57a82007-07-11 23:06:47 +0000195 Args:
196 command: the command line string
mbligh0e4613b2007-10-29 16:55:07 +0000197 timeout: time limit in seconds before attempting to
mblighdcd57a82007-07-11 23:06:47 +0000198 kill the running process. The run() function
199 will take a few seconds longer than 'timeout'
200 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000201 ignore_status: do not raise an exception, no matter what
202 the exit code of the command is.
mbligh0e4613b2007-10-29 16:55:07 +0000203 stdout_tee: optional file-like object to which stdout data
204 will be written as it is generated (data will still
205 be stored in result.stdout)
206 stderr_tee: likewise for stderr
207
mblighdcd57a82007-07-11 23:06:47 +0000208 Returns:
209 a hosts.CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000210
mblighdcd57a82007-07-11 23:06:47 +0000211 Raises:
mbligh0e4613b2007-10-29 16:55:07 +0000212 AutoservRunError: the exit code of the command
mblighdcd57a82007-07-11 23:06:47 +0000213 execution was not 0
mblighdc735a22007-08-02 16:54:37 +0000214
mblighdcd57a82007-07-11 23:06:47 +0000215 TODO(poirier): Should a timeout raise an exception? Should
216 exceptions be raised at all?
217 """
mbligh0e4613b2007-10-29 16:55:07 +0000218 result = hosts.CmdResult()
219 result.command = command
220 sp = subprocess.Popen(command, stdout=subprocess.PIPE,
221 stderr=subprocess.PIPE, close_fds=True,
222 shell=True, executable="/bin/bash")
223 stdout_file = StringIO.StringIO()
224 stderr_file = StringIO.StringIO()
mbligh0dd2ae02007-08-01 17:31:10 +0000225
226 try:
227 # We are holding ends to stdin, stdout pipes
228 # hence we need to be sure to close those fds no mater what
mbligh0e4613b2007-10-29 16:55:07 +0000229 start_time = time.time()
230 exit_status_indication = _wait_for_command(
231 sp, start_time, timeout,
232 stdout_file, stderr_file,
233 stdout_tee, stderr_tee)
mbligh0dd2ae02007-08-01 17:31:10 +0000234
235 result.duration = time.time() - start_time
236 result.aborted = exit_status_indication & 127
237 if result.aborted:
mbligh0e4613b2007-10-29 16:55:07 +0000238 result.exit_status = None
mbligh0dd2ae02007-08-01 17:31:10 +0000239 else:
mbligh0e4613b2007-10-29 16:55:07 +0000240 result.exit_status = exit_status_indication / 256
241 # don't use os.read now, so we get all the rest of the output
242 _process_output(sp.stdout, stdout_file, stdout_tee,
243 use_os_read=False)
244 _process_output(sp.stderr, stderr_file, stderr_tee,
245 use_os_read=False)
mbligh0dd2ae02007-08-01 17:31:10 +0000246 finally:
247 # close our ends of the pipes to the sp no matter what
248 sp.stdout.close()
249 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000250
mbligh0e4613b2007-10-29 16:55:07 +0000251 result.stdout = stdout_file.getvalue()
252 result.stderr = stderr_file.getvalue()
253
mblighcf965b02007-07-25 16:49:45 +0000254 if not ignore_status and result.exit_status > 0:
mbligh0e4613b2007-10-29 16:55:07 +0000255 raise errors.AutoservRunError("command execution error",
mblighdcd57a82007-07-11 23:06:47 +0000256 result)
mbligh0dd2ae02007-08-01 17:31:10 +0000257
mblighdcd57a82007-07-11 23:06:47 +0000258 return result
259
260
mbligh5f876ad2007-10-12 23:59:53 +0000261def system(command, timeout=None, ignore_status=False):
262 return run(command, timeout, ignore_status).exit_status
263
264
265def system_output(command, timeout=None, ignore_status=False):
266 return run(command, timeout, ignore_status).stdout
267
268
mblighdcd57a82007-07-11 23:06:47 +0000269def get_tmp_dir():
270 """Return the pathname of a directory on the host suitable
271 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000272
mblighdcd57a82007-07-11 23:06:47 +0000273 The directory and its content will be deleted automatically
274 at the end of the program execution if they are still present.
275 """
276 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000277
mblighdcd57a82007-07-11 23:06:47 +0000278 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000279 pid = os.getpid()
280 if not pid in __tmp_dirs:
281 __tmp_dirs[pid] = []
282 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000283 return dir_name
284
285
286@atexit.register
287def __clean_tmp_dirs():
288 """Erase temporary directories that were created by the get_tmp_dir()
289 function and that are still present.
290 """
291 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000292
mblighbea56822007-08-31 08:53:40 +0000293 pid = os.getpid()
294 if pid not in __tmp_dirs:
295 return
296 for dir in __tmp_dirs[pid]:
297 try:
298 shutil.rmtree(dir)
299 except OSError, e:
300 if e.errno == 2:
301 pass
302 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000303
304
305def unarchive(host, source_material):
306 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000307
mblighc8949b82007-07-23 16:33:58 +0000308 If the "source_material" is compresses (according to the file
309 extension) it will be uncompressed. Supported compression formats
310 are gzip and bzip2. Afterwards, if the source_material is a tar
311 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000312
mblighc8949b82007-07-23 16:33:58 +0000313 Args:
314 host: the host object on which the archive is located
315 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000316
mblighc8949b82007-07-23 16:33:58 +0000317 Returns:
318 The file or directory name of the unarchived source material.
319 If the material is a tar archive, it will be extracted in the
320 directory where it is and the path returned will be the first
321 entry in the archive, assuming it is the topmost directory.
322 If the material is not an archive, nothing will be done so this
323 function is "harmless" when it is "useless".
324 """
325 # uncompress
326 if (source_material.endswith(".gz") or
327 source_material.endswith(".gzip")):
328 host.run('gunzip "%s"' % (sh_escape(source_material)))
329 source_material= ".".join(source_material.split(".")[:-1])
330 elif source_material.endswith("bz2"):
331 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
332 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000333
mblighc8949b82007-07-23 16:33:58 +0000334 # untar
335 if source_material.endswith(".tar"):
336 retval= host.run('tar -C "%s" -xvf "%s"' % (
337 sh_escape(os.path.dirname(source_material)),
338 sh_escape(source_material),))
339 source_material= os.path.join(os.path.dirname(source_material),
340 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000341
mblighc8949b82007-07-23 16:33:58 +0000342 return source_material
mblighf1c52842007-10-16 15:21:38 +0000343
344
345def write_keyval(dirname, dictionary):
346 keyval = open(os.path.join(dirname, 'keyval'), 'w')
347 for key in dictionary.keys():
348 value = '%s' % dictionary[key] # convert numbers to strings
349 if re.search(r'\W', key):
350 raise 'Invalid key: ' + key
351 keyval.write('%s=%s\n' % (key, str(value)))
352 keyval.close()
353
mbligh05269362007-10-16 16:58:11 +0000354
355def update_version(srcdir, preserve_srcdir, new_version, install, *args, **dargs):
356 """
357 Make sure srcdir is version new_version
358
359 If not, delete it and install() the new version.
360
361 In the preserve_srcdir case, we just check it's up to date,
362 and if not, we rerun install, without removing srcdir
363 """
364 versionfile = srcdir + '/.version'
365 install_needed = True
366
367 if os.path.exists(srcdir):
368 if os.path.exists(versionfile):
369 old_version = pickle.load(open(versionfile, 'r'))
370 if (old_version == new_version):
371 install_needed = False
372
373 if install_needed:
374 if not preserve_srcdir:
375 system('rm -rf ' + srcdir)
376 install(*args, **dargs)
377 if os.path.exists(srcdir):
378 pickle.dump(new_version, open(versionfile, 'w'))
mbligh9708f732007-10-18 03:18:54 +0000379
380
381def get_server_dir():
382 path = os.path.dirname(sys.modules['utils'].__file__)
383 return os.path.abspath(path)