blob: 0e007daf51b685a68a72f614e83e957bf8a9eabe [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
mblighc9f342d2007-11-28 22:29:23 +000016import time, types, urllib, re, sys, textwrap
mbligh03f4fc72007-11-29 20:56:14 +000017import hosts
mblighf31b0c02007-11-29 18:19:22 +000018from common.error import *
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
mblighc9f342d2007-11-28 22:29:23 +0000130def __nuke_subprocess(subproc):
131 # the process has not terminated within timeout,
132 # kill it via an escalating series of signals.
133 signal_queue = [signal.SIGTERM, signal.SIGKILL]
134 for sig in signal_queue:
135 try:
136 os.kill(subproc.pid, sig)
137 # The process may have died before we could kill it.
138 except OSError:
139 pass
140
141 for i in range(5):
142 rc = subproc.poll()
143 if rc != None:
144 return
145 time.sleep(1)
146
147
mbligh0e4613b2007-10-29 16:55:07 +0000148def _process_output(pipe, fbuffer, teefile=None, use_os_read=True):
149 if use_os_read:
150 data = os.read(pipe.fileno(), 1024)
151 else:
152 data = pipe.read()
153 fbuffer.write(data)
154 if teefile:
155 teefile.write(data)
156 teefile.flush()
157
158
159def _wait_for_command(subproc, start_time, timeout, stdout_file, stderr_file,
160 stdout_tee, stderr_tee):
161 if timeout:
162 stop_time = start_time + timeout
163 time_left = stop_time - time.time()
164 else:
165 time_left = None # so that select never times out
166 while not timeout or time_left > 0:
167 # select will return when stdout is ready (including when it is
168 # EOF, that is the process has terminated).
169 ready, _, _ = select.select([subproc.stdout, subproc.stderr],
170 [], [], time_left)
171 # os.read() has to be used instead of
172 # subproc.stdout.read() which will otherwise block
173 if subproc.stdout in ready:
174 _process_output(subproc.stdout, stdout_file,
175 stdout_tee)
176 if subproc.stderr in ready:
177 _process_output(subproc.stderr, stderr_file,
178 stderr_tee)
179
180 pid, exit_status_indication = os.waitpid(subproc.pid,
181 os.WNOHANG)
182 if pid:
mblighc9f342d2007-11-28 22:29:23 +0000183 return exit_status_indication
mbligh0e4613b2007-10-29 16:55:07 +0000184 if timeout:
185 time_left = stop_time - time.time()
186
187 # the process has not terminated within timeout,
188 # kill it via an escalating series of signals.
189 if not pid:
mblighc9f342d2007-11-28 22:29:23 +0000190 __nuke_subprocess(subproc)
191 raise CmdError('Command not complete within %s seconds' % timeout)
mbligh0e4613b2007-10-29 16:55:07 +0000192
193
194def run(command, timeout=None, ignore_status=False,
195 stdout_tee=None, stderr_tee=None):
mblighdc735a22007-08-02 16:54:37 +0000196 """
197 Run a command on the host.
198
mblighdcd57a82007-07-11 23:06:47 +0000199 Args:
200 command: the command line string
mbligh0e4613b2007-10-29 16:55:07 +0000201 timeout: time limit in seconds before attempting to
mblighdcd57a82007-07-11 23:06:47 +0000202 kill the running process. The run() function
203 will take a few seconds longer than 'timeout'
204 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000205 ignore_status: do not raise an exception, no matter what
206 the exit code of the command is.
mbligh0e4613b2007-10-29 16:55:07 +0000207 stdout_tee: optional file-like object to which stdout data
208 will be written as it is generated (data will still
209 be stored in result.stdout)
210 stderr_tee: likewise for stderr
211
mblighdcd57a82007-07-11 23:06:47 +0000212 Returns:
mblighc9f342d2007-11-28 22:29:23 +0000213 a CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000214
mblighdcd57a82007-07-11 23:06:47 +0000215 Raises:
mbligh0e4613b2007-10-29 16:55:07 +0000216 AutoservRunError: the exit code of the command
mblighdcd57a82007-07-11 23:06:47 +0000217 execution was not 0
mblighdcd57a82007-07-11 23:06:47 +0000218 """
mblighc9f342d2007-11-28 22:29:23 +0000219 result = CmdResult(command)
mbligh0e4613b2007-10-29 16:55:07 +0000220 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()
mblighc9f342d2007-11-28 22:29:23 +0000230 result.exit_status = _wait_for_command(sp, start_time, timeout,
231 stdout_file, stderr_file, stdout_tee, stderr_tee)
mbligh0dd2ae02007-08-01 17:31:10 +0000232
233 result.duration = time.time() - start_time
mbligh0e4613b2007-10-29 16:55:07 +0000234 # don't use os.read now, so we get all the rest of the output
235 _process_output(sp.stdout, stdout_file, stdout_tee,
236 use_os_read=False)
237 _process_output(sp.stderr, stderr_file, stderr_tee,
238 use_os_read=False)
mbligh0dd2ae02007-08-01 17:31:10 +0000239 finally:
240 # close our ends of the pipes to the sp no matter what
241 sp.stdout.close()
242 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000243
mbligh0e4613b2007-10-29 16:55:07 +0000244 result.stdout = stdout_file.getvalue()
245 result.stderr = stderr_file.getvalue()
246
mblighcf965b02007-07-25 16:49:45 +0000247 if not ignore_status and result.exit_status > 0:
mbligh03f4fc72007-11-29 20:56:14 +0000248 raise AutoservRunError("command execution error", result)
mbligh0dd2ae02007-08-01 17:31:10 +0000249
mblighdcd57a82007-07-11 23:06:47 +0000250 return result
251
252
mbligh5f876ad2007-10-12 23:59:53 +0000253def system(command, timeout=None, ignore_status=False):
254 return run(command, timeout, ignore_status).exit_status
255
256
257def system_output(command, timeout=None, ignore_status=False):
258 return run(command, timeout, ignore_status).stdout
259
260
mblighdcd57a82007-07-11 23:06:47 +0000261def get_tmp_dir():
262 """Return the pathname of a directory on the host suitable
263 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000264
mblighdcd57a82007-07-11 23:06:47 +0000265 The directory and its content will be deleted automatically
266 at the end of the program execution if they are still present.
267 """
268 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000269
mblighdcd57a82007-07-11 23:06:47 +0000270 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000271 pid = os.getpid()
272 if not pid in __tmp_dirs:
273 __tmp_dirs[pid] = []
274 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000275 return dir_name
276
277
278@atexit.register
279def __clean_tmp_dirs():
280 """Erase temporary directories that were created by the get_tmp_dir()
281 function and that are still present.
282 """
283 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000284
mblighbea56822007-08-31 08:53:40 +0000285 pid = os.getpid()
286 if pid not in __tmp_dirs:
287 return
288 for dir in __tmp_dirs[pid]:
289 try:
290 shutil.rmtree(dir)
291 except OSError, e:
292 if e.errno == 2:
293 pass
294 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000295
296
297def unarchive(host, source_material):
298 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000299
mblighc8949b82007-07-23 16:33:58 +0000300 If the "source_material" is compresses (according to the file
301 extension) it will be uncompressed. Supported compression formats
302 are gzip and bzip2. Afterwards, if the source_material is a tar
303 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000304
mblighc8949b82007-07-23 16:33:58 +0000305 Args:
306 host: the host object on which the archive is located
307 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000308
mblighc8949b82007-07-23 16:33:58 +0000309 Returns:
310 The file or directory name of the unarchived source material.
311 If the material is a tar archive, it will be extracted in the
312 directory where it is and the path returned will be the first
313 entry in the archive, assuming it is the topmost directory.
314 If the material is not an archive, nothing will be done so this
315 function is "harmless" when it is "useless".
316 """
317 # uncompress
318 if (source_material.endswith(".gz") or
319 source_material.endswith(".gzip")):
320 host.run('gunzip "%s"' % (sh_escape(source_material)))
321 source_material= ".".join(source_material.split(".")[:-1])
322 elif source_material.endswith("bz2"):
323 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
324 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000325
mblighc8949b82007-07-23 16:33:58 +0000326 # untar
327 if source_material.endswith(".tar"):
328 retval= host.run('tar -C "%s" -xvf "%s"' % (
329 sh_escape(os.path.dirname(source_material)),
330 sh_escape(source_material),))
331 source_material= os.path.join(os.path.dirname(source_material),
332 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000333
mblighc8949b82007-07-23 16:33:58 +0000334 return source_material
mblighf1c52842007-10-16 15:21:38 +0000335
336
337def write_keyval(dirname, dictionary):
338 keyval = open(os.path.join(dirname, 'keyval'), 'w')
339 for key in dictionary.keys():
340 value = '%s' % dictionary[key] # convert numbers to strings
341 if re.search(r'\W', key):
342 raise 'Invalid key: ' + key
343 keyval.write('%s=%s\n' % (key, str(value)))
344 keyval.close()
345
mbligh05269362007-10-16 16:58:11 +0000346
347def update_version(srcdir, preserve_srcdir, new_version, install, *args, **dargs):
348 """
349 Make sure srcdir is version new_version
350
351 If not, delete it and install() the new version.
352
353 In the preserve_srcdir case, we just check it's up to date,
354 and if not, we rerun install, without removing srcdir
355 """
356 versionfile = srcdir + '/.version'
357 install_needed = True
358
359 if os.path.exists(srcdir):
360 if os.path.exists(versionfile):
361 old_version = pickle.load(open(versionfile, 'r'))
362 if (old_version == new_version):
363 install_needed = False
364
365 if install_needed:
366 if not preserve_srcdir:
367 system('rm -rf ' + srcdir)
368 install(*args, **dargs)
369 if os.path.exists(srcdir):
370 pickle.dump(new_version, open(versionfile, 'w'))
mbligh9708f732007-10-18 03:18:54 +0000371
372
373def get_server_dir():
374 path = os.path.dirname(sys.modules['utils'].__file__)
375 return os.path.abspath(path)
mbligh40f122a2007-11-03 23:08:46 +0000376
377
378class AutoservOptionParser:
379 """Custom command-line options parser for autoserv.
380
381 We can't use the general getopt methods here, as there will be unknown
382 extra arguments that we pass down into the control file instead.
383 Thus we process the arguments by hand, for which we are duly repentant.
384 Making a single function here just makes it harder to read. Suck it up.
385 """
386
387 def __init__(self, args):
388 self.args = args
389
390
391 def parse_opts(self, flag):
392 if self.args.count(flag):
393 idx = self.args.index(flag)
394 self.args[idx : idx+1] = []
395 return True
396 else:
397 return False
398
399
400 def parse_opts_param(self, flag, default = None, split = False):
401 if self.args.count(flag):
402 idx = self.args.index(flag)
403 ret = self.args[idx+1]
404 self.args[idx : idx+2] = []
405 if split:
406 return ret.split(split)
407 else:
408 return ret
409 else:
410 return default
mblighc9f342d2007-11-28 22:29:23 +0000411
412
413class CmdResult(object):
414 """
415 Command execution result.
416
417 command: String containing the command line itself
418 exit_status: Integer exit code of the process
419 stdout: String containing stdout of the process
420 stderr: String containing stderr of the process
421 duration: Elapsed wall clock time running the process
422 """
423
424 def __init__(self, command = None):
425 self.command = command
426 self.exit_status = None
427 self.stdout = ""
428 self.stderr = ""
429 self.duration = 0
430
431
432 def __repr__(self):
433 wrapper = textwrap.TextWrapper(width = 78,
434 initial_indent="\n ",
435 subsequent_indent=" ")
436
437 stdout = self.stdout.rstrip()
438 if stdout:
439 stdout = "\nstdout:\n%s" % stdout
440
441 stderr = self.stderr.rstrip()
442 if stderr:
443 stderr = "\nstderr:\n%s" % stderr
444
445 return ("* Command: %s\n"
446 "Exit status: %s\n"
447 "Duration: %s\n"
448 "%s"
449 "%s"
450 % (wrapper.fill(self.command), self.exit_status,
451 self.duration, stdout, stderr))