blob: caf1b401429bd4600be90be131768f7b595a314a [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
mblighea397bb2008-02-02 19:17:51 +000020from common.utils import *
21
22
mblighbea56822007-08-31 08:53:40 +000023# A dictionary of pid and a list of tmpdirs for that pid
24__tmp_dirs = {}
mblighdcd57a82007-07-11 23:06:47 +000025
26
27def sh_escape(command):
mblighdc735a22007-08-02 16:54:37 +000028 """
29 Escape special characters from a command so that it can be passed
mblighc8949b82007-07-23 16:33:58 +000030 as a double quoted (" ") string in a (ba)sh command.
mblighdc735a22007-08-02 16:54:37 +000031
mblighdcd57a82007-07-11 23:06:47 +000032 Args:
33 command: the command string to escape.
mblighdc735a22007-08-02 16:54:37 +000034
mblighdcd57a82007-07-11 23:06:47 +000035 Returns:
36 The escaped command string. The required englobing double
37 quotes are NOT added and so should be added at some point by
38 the caller.
mblighdc735a22007-08-02 16:54:37 +000039
mblighdcd57a82007-07-11 23:06:47 +000040 See also: http://www.tldp.org/LDP/abs/html/escapingsection.html
41 """
42 command= command.replace("\\", "\\\\")
43 command= command.replace("$", r'\$')
44 command= command.replace('"', r'\"')
45 command= command.replace('`', r'\`')
46 return command
47
48
49def scp_remote_escape(filename):
mblighdc735a22007-08-02 16:54:37 +000050 """
51 Escape special characters from a filename so that it can be passed
mblighdcd57a82007-07-11 23:06:47 +000052 to scp (within double quotes) as a remote file.
mblighdc735a22007-08-02 16:54:37 +000053
mblighdcd57a82007-07-11 23:06:47 +000054 Bis-quoting has to be used with scp for remote files, "bis-quoting"
55 as in quoting x 2
56 scp does not support a newline in the filename
mblighdc735a22007-08-02 16:54:37 +000057
mblighdcd57a82007-07-11 23:06:47 +000058 Args:
59 filename: the filename string to escape.
mblighdc735a22007-08-02 16:54:37 +000060
mblighdcd57a82007-07-11 23:06:47 +000061 Returns:
62 The escaped filename string. The required englobing double
63 quotes are NOT added and so should be added at some point by
64 the caller.
65 """
66 escape_chars= r' !"$&' "'" r'()*,:;<=>?[\]^`{|}'
mblighdc735a22007-08-02 16:54:37 +000067
mblighdcd57a82007-07-11 23:06:47 +000068 new_name= []
69 for char in filename:
70 if char in escape_chars:
71 new_name.append("\\%s" % (char,))
72 else:
73 new_name.append(char)
mblighdc735a22007-08-02 16:54:37 +000074
mblighdcd57a82007-07-11 23:06:47 +000075 return sh_escape("".join(new_name))
76
77
mbligh6e18dab2007-10-24 21:27:18 +000078def get(location, local_copy = False):
mblighdcd57a82007-07-11 23:06:47 +000079 """Get a file or directory to a local temporary directory.
mblighdc735a22007-08-02 16:54:37 +000080
mblighdcd57a82007-07-11 23:06:47 +000081 Args:
82 location: the source of the material to get. This source may
83 be one of:
84 * a local file or directory
85 * a URL (http or ftp)
86 * a python file-like object
mblighdc735a22007-08-02 16:54:37 +000087
mblighdcd57a82007-07-11 23:06:47 +000088 Returns:
89 The location of the file or directory where the requested
90 content was saved. This will be contained in a temporary
mblighc8949b82007-07-23 16:33:58 +000091 directory on the local host. If the material to get was a
92 directory, the location will contain a trailing '/'
mblighdcd57a82007-07-11 23:06:47 +000093 """
94 tmpdir = get_tmp_dir()
mblighdc735a22007-08-02 16:54:37 +000095
mblighdcd57a82007-07-11 23:06:47 +000096 # location is a file-like object
97 if hasattr(location, "read"):
98 tmpfile = os.path.join(tmpdir, "file")
99 tmpfileobj = file(tmpfile, 'w')
100 shutil.copyfileobj(location, tmpfileobj)
101 tmpfileobj.close()
102 return tmpfile
mblighdc735a22007-08-02 16:54:37 +0000103
mblighdcd57a82007-07-11 23:06:47 +0000104 if isinstance(location, types.StringTypes):
105 # location is a URL
106 if location.startswith('http') or location.startswith('ftp'):
107 tmpfile = os.path.join(tmpdir, os.path.basename(location))
108 urllib.urlretrieve(location, tmpfile)
109 return tmpfile
110 # location is a local path
111 elif os.path.exists(os.path.abspath(location)):
mbligh6e18dab2007-10-24 21:27:18 +0000112 if not local_copy:
mbligh59f70aa2007-10-25 14:44:38 +0000113 if os.path.isdir(location):
114 return location.rstrip('/') + '/'
115 else:
116 return location
mblighdcd57a82007-07-11 23:06:47 +0000117 tmpfile = os.path.join(tmpdir, os.path.basename(location))
118 if os.path.isdir(location):
119 tmpfile += '/'
120 shutil.copytree(location, tmpfile, symlinks=True)
121 return tmpfile
122 shutil.copyfile(location, tmpfile)
123 return tmpfile
124 # location is just a string, dump it to a file
125 else:
126 tmpfd, tmpfile = tempfile.mkstemp(dir=tmpdir)
127 tmpfileobj = os.fdopen(tmpfd, 'w')
128 tmpfileobj.write(location)
129 tmpfileobj.close()
130 return tmpfile
131
mblighd8b39252008-03-20 21:15:03 +0000132def nuke_subprocess(subproc):
mblighc9f342d2007-11-28 22:29:23 +0000133 # the process has not terminated within timeout,
134 # kill it via an escalating series of signals.
135 signal_queue = [signal.SIGTERM, signal.SIGKILL]
136 for sig in signal_queue:
137 try:
138 os.kill(subproc.pid, sig)
139 # The process may have died before we could kill it.
140 except OSError:
141 pass
142
143 for i in range(5):
144 rc = subproc.poll()
145 if rc != None:
146 return
147 time.sleep(1)
148
149
mblighc3aee0f2008-01-17 16:26:39 +0000150def nuke_pid(pid):
151 # the process has not terminated within timeout,
152 # kill it via an escalating series of signals.
153 signal_queue = [signal.SIGTERM, signal.SIGKILL]
154 for sig in signal_queue:
155 try:
156 os.kill(pid, sig)
157
158 # The process may have died before we could kill it.
159 except OSError:
160 pass
161
162 try:
163 for i in range(5):
164 status = os.waitpid(pid, os.WNOHANG)[0]
165 if status == pid:
166 return
167 time.sleep(1)
168
169 if status != pid:
170 raise AutoservRunError('Could not kill pid %d'
171 % pid, None)
172
173 # the process died before we join it.
174 except OSError:
175 pass
176
177
mbligh0e4613b2007-10-29 16:55:07 +0000178def _process_output(pipe, fbuffer, teefile=None, use_os_read=True):
179 if use_os_read:
180 data = os.read(pipe.fileno(), 1024)
181 else:
182 data = pipe.read()
183 fbuffer.write(data)
184 if teefile:
185 teefile.write(data)
186 teefile.flush()
187
188
189def _wait_for_command(subproc, start_time, timeout, stdout_file, stderr_file,
190 stdout_tee, stderr_tee):
191 if timeout:
192 stop_time = start_time + timeout
193 time_left = stop_time - time.time()
194 else:
195 time_left = None # so that select never times out
196 while not timeout or time_left > 0:
197 # select will return when stdout is ready (including when it is
198 # EOF, that is the process has terminated).
199 ready, _, _ = select.select([subproc.stdout, subproc.stderr],
200 [], [], time_left)
201 # os.read() has to be used instead of
202 # subproc.stdout.read() which will otherwise block
203 if subproc.stdout in ready:
204 _process_output(subproc.stdout, stdout_file,
205 stdout_tee)
206 if subproc.stderr in ready:
207 _process_output(subproc.stderr, stderr_file,
208 stderr_tee)
209
mbligh35f43952008-03-05 17:02:08 +0000210 exit_status_indication = subproc.poll()
211
212 if exit_status_indication is not None:
mblighc9f342d2007-11-28 22:29:23 +0000213 return exit_status_indication
mbligh0e4613b2007-10-29 16:55:07 +0000214 if timeout:
215 time_left = stop_time - time.time()
216
217 # the process has not terminated within timeout,
218 # kill it via an escalating series of signals.
mbligh35f43952008-03-05 17:02:08 +0000219 if exit_status_indication is None:
mblighd8b39252008-03-20 21:15:03 +0000220 nuke_subprocess(subproc)
mblighff6b4022008-01-10 16:20:51 +0000221 raise AutoservRunError('Command not complete within %s seconds'
mbligh6a2a2df2008-01-16 17:41:55 +0000222 % timeout, None)
mbligh0e4613b2007-10-29 16:55:07 +0000223
224
225def run(command, timeout=None, ignore_status=False,
226 stdout_tee=None, stderr_tee=None):
mblighdc735a22007-08-02 16:54:37 +0000227 """
228 Run a command on the host.
229
mblighdcd57a82007-07-11 23:06:47 +0000230 Args:
231 command: the command line string
mbligh0e4613b2007-10-29 16:55:07 +0000232 timeout: time limit in seconds before attempting to
mblighdcd57a82007-07-11 23:06:47 +0000233 kill the running process. The run() function
234 will take a few seconds longer than 'timeout'
235 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000236 ignore_status: do not raise an exception, no matter what
237 the exit code of the command is.
mbligh0e4613b2007-10-29 16:55:07 +0000238 stdout_tee: optional file-like object to which stdout data
239 will be written as it is generated (data will still
240 be stored in result.stdout)
241 stderr_tee: likewise for stderr
242
mblighdcd57a82007-07-11 23:06:47 +0000243 Returns:
mblighc9f342d2007-11-28 22:29:23 +0000244 a CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000245
mblighdcd57a82007-07-11 23:06:47 +0000246 Raises:
mbligh0e4613b2007-10-29 16:55:07 +0000247 AutoservRunError: the exit code of the command
mblighdcd57a82007-07-11 23:06:47 +0000248 execution was not 0
mblighdcd57a82007-07-11 23:06:47 +0000249 """
mblighd8b39252008-03-20 21:15:03 +0000250 return join_bg_job(run_bg(command), timeout, ignore_status,
251 stdout_tee, stderr_tee)
252
253
254def run_bg(command):
255 """Run the command in a subprocess and return the subprocess."""
mblighc9f342d2007-11-28 22:29:23 +0000256 result = CmdResult(command)
mbligh0e4613b2007-10-29 16:55:07 +0000257 sp = subprocess.Popen(command, stdout=subprocess.PIPE,
mbligh3c647c32008-02-08 16:51:02 +0000258 stderr=subprocess.PIPE,
mbligh0e4613b2007-10-29 16:55:07 +0000259 shell=True, executable="/bin/bash")
mblighd8b39252008-03-20 21:15:03 +0000260 return sp, result
261
262
263def join_bg_job(bg_job, timeout=None, ignore_status=False,
264 stdout_tee=None, stderr_tee=None):
265 """Join the subprocess with the current thread. See run description."""
266 sp, result = bg_job
mbligh0e4613b2007-10-29 16:55:07 +0000267 stdout_file = StringIO.StringIO()
268 stderr_file = StringIO.StringIO()
mbligh0dd2ae02007-08-01 17:31:10 +0000269
270 try:
271 # We are holding ends to stdin, stdout pipes
272 # hence we need to be sure to close those fds no mater what
mbligh0e4613b2007-10-29 16:55:07 +0000273 start_time = time.time()
mbligh34faa282008-01-16 17:44:49 +0000274 ret = _wait_for_command(sp, start_time, timeout, stdout_file,
275 stderr_file, stdout_tee, stderr_tee)
mblighe13239a2008-03-10 15:06:11 +0000276 result.exit_status = ret
mbligh0dd2ae02007-08-01 17:31:10 +0000277
278 result.duration = time.time() - start_time
mbligh0e4613b2007-10-29 16:55:07 +0000279 # don't use os.read now, so we get all the rest of the output
280 _process_output(sp.stdout, stdout_file, stdout_tee,
281 use_os_read=False)
282 _process_output(sp.stderr, stderr_file, stderr_tee,
283 use_os_read=False)
mbligh0dd2ae02007-08-01 17:31:10 +0000284 finally:
285 # close our ends of the pipes to the sp no matter what
286 sp.stdout.close()
287 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000288
mbligh0e4613b2007-10-29 16:55:07 +0000289 result.stdout = stdout_file.getvalue()
290 result.stderr = stderr_file.getvalue()
291
mblighcf965b02007-07-25 16:49:45 +0000292 if not ignore_status and result.exit_status > 0:
mbligh03f4fc72007-11-29 20:56:14 +0000293 raise AutoservRunError("command execution error", result)
mbligh0dd2ae02007-08-01 17:31:10 +0000294
mblighdcd57a82007-07-11 23:06:47 +0000295 return result
296
297
mbligh5f876ad2007-10-12 23:59:53 +0000298def system(command, timeout=None, ignore_status=False):
mbligh10b3a082008-01-10 16:35:29 +0000299 return run(command, timeout, ignore_status,
300 stdout_tee=sys.stdout, stderr_tee=sys.stderr).exit_status
mbligh5f876ad2007-10-12 23:59:53 +0000301
302
303def system_output(command, timeout=None, ignore_status=False):
mblighd60b0322008-02-12 20:58:43 +0000304 out = run(command, timeout, ignore_status).stdout
305 if out[-1:] == '\n': out = out[:-1]
306 return out
mbligh5f876ad2007-10-12 23:59:53 +0000307
308
mblighdcd57a82007-07-11 23:06:47 +0000309def get_tmp_dir():
310 """Return the pathname of a directory on the host suitable
311 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000312
mblighdcd57a82007-07-11 23:06:47 +0000313 The directory and its content will be deleted automatically
314 at the end of the program execution if they are still present.
315 """
316 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000317
mblighdcd57a82007-07-11 23:06:47 +0000318 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000319 pid = os.getpid()
320 if not pid in __tmp_dirs:
321 __tmp_dirs[pid] = []
322 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000323 return dir_name
324
325
326@atexit.register
327def __clean_tmp_dirs():
328 """Erase temporary directories that were created by the get_tmp_dir()
329 function and that are still present.
330 """
331 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000332
mblighbea56822007-08-31 08:53:40 +0000333 pid = os.getpid()
334 if pid not in __tmp_dirs:
335 return
336 for dir in __tmp_dirs[pid]:
337 try:
338 shutil.rmtree(dir)
339 except OSError, e:
340 if e.errno == 2:
341 pass
342 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000343
344
345def unarchive(host, source_material):
346 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000347
mblighc8949b82007-07-23 16:33:58 +0000348 If the "source_material" is compresses (according to the file
349 extension) it will be uncompressed. Supported compression formats
350 are gzip and bzip2. Afterwards, if the source_material is a tar
351 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000352
mblighc8949b82007-07-23 16:33:58 +0000353 Args:
354 host: the host object on which the archive is located
355 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000356
mblighc8949b82007-07-23 16:33:58 +0000357 Returns:
358 The file or directory name of the unarchived source material.
359 If the material is a tar archive, it will be extracted in the
360 directory where it is and the path returned will be the first
361 entry in the archive, assuming it is the topmost directory.
362 If the material is not an archive, nothing will be done so this
363 function is "harmless" when it is "useless".
364 """
365 # uncompress
366 if (source_material.endswith(".gz") or
367 source_material.endswith(".gzip")):
368 host.run('gunzip "%s"' % (sh_escape(source_material)))
369 source_material= ".".join(source_material.split(".")[:-1])
370 elif source_material.endswith("bz2"):
371 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
372 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000373
mblighc8949b82007-07-23 16:33:58 +0000374 # untar
375 if source_material.endswith(".tar"):
376 retval= host.run('tar -C "%s" -xvf "%s"' % (
377 sh_escape(os.path.dirname(source_material)),
378 sh_escape(source_material),))
379 source_material= os.path.join(os.path.dirname(source_material),
380 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000381
mblighc8949b82007-07-23 16:33:58 +0000382 return source_material
mblighf1c52842007-10-16 15:21:38 +0000383
384
mbligh9708f732007-10-18 03:18:54 +0000385def get_server_dir():
386 path = os.path.dirname(sys.modules['utils'].__file__)
387 return os.path.abspath(path)
mbligh40f122a2007-11-03 23:08:46 +0000388
389
mbligh34a3fd72007-12-10 17:16:22 +0000390def find_pid(command):
391 for line in system_output('ps -eo pid,cmd').rstrip().split('\n'):
392 (pid, cmd) = line.split(None, 1)
393 if re.search(command, cmd):
394 return int(pid)
395 return None
396
397
398def nohup(command, stdout='/dev/null', stderr='/dev/null', background=True,
399 env = {}):
400 cmd = ' '.join(key+'='+val for key, val in env.iteritems())
401 cmd += ' nohup ' + command
402 cmd += ' > %s' % stdout
403 if stdout == stderr:
404 cmd += ' 2>&1'
405 else:
406 cmd += ' 2> %s' % stderr
407 if background:
408 cmd += ' &'
409 system(cmd)
410
411
mbligh40f122a2007-11-03 23:08:46 +0000412class AutoservOptionParser:
413 """Custom command-line options parser for autoserv.
414
415 We can't use the general getopt methods here, as there will be unknown
416 extra arguments that we pass down into the control file instead.
417 Thus we process the arguments by hand, for which we are duly repentant.
418 Making a single function here just makes it harder to read. Suck it up.
419 """
420
421 def __init__(self, args):
422 self.args = args
423
424
425 def parse_opts(self, flag):
426 if self.args.count(flag):
427 idx = self.args.index(flag)
428 self.args[idx : idx+1] = []
429 return True
430 else:
431 return False
432
433
434 def parse_opts_param(self, flag, default = None, split = False):
435 if self.args.count(flag):
436 idx = self.args.index(flag)
437 ret = self.args[idx+1]
438 self.args[idx : idx+2] = []
439 if split:
440 return ret.split(split)
441 else:
442 return ret
443 else:
444 return default
mblighc9f342d2007-11-28 22:29:23 +0000445
446
447class CmdResult(object):
448 """
449 Command execution result.
450
451 command: String containing the command line itself
452 exit_status: Integer exit code of the process
453 stdout: String containing stdout of the process
454 stderr: String containing stderr of the process
455 duration: Elapsed wall clock time running the process
456 """
457
458 def __init__(self, command = None):
459 self.command = command
460 self.exit_status = None
461 self.stdout = ""
462 self.stderr = ""
463 self.duration = 0
464
465
466 def __repr__(self):
467 wrapper = textwrap.TextWrapper(width = 78,
468 initial_indent="\n ",
469 subsequent_indent=" ")
470
471 stdout = self.stdout.rstrip()
472 if stdout:
473 stdout = "\nstdout:\n%s" % stdout
474
475 stderr = self.stderr.rstrip()
476 if stderr:
477 stderr = "\nstderr:\n%s" % stderr
478
479 return ("* Command: %s\n"
480 "Exit status: %s\n"
481 "Duration: %s\n"
482 "%s"
483 "%s"
484 % (wrapper.fill(self.command), self.exit_status,
485 self.duration, stdout, stderr))