blob: 45b4703d2349eecbfc6924d9abe5f10ce7b0587f [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
132
mblighc9f342d2007-11-28 22:29:23 +0000133def __nuke_subprocess(subproc):
134 # the process has not terminated within timeout,
135 # kill it via an escalating series of signals.
136 signal_queue = [signal.SIGTERM, signal.SIGKILL]
137 for sig in signal_queue:
138 try:
139 os.kill(subproc.pid, sig)
140 # The process may have died before we could kill it.
141 except OSError:
142 pass
143
144 for i in range(5):
145 rc = subproc.poll()
146 if rc != None:
147 return
148 time.sleep(1)
149
150
mblighc3aee0f2008-01-17 16:26:39 +0000151def nuke_pid(pid):
152 # the process has not terminated within timeout,
153 # kill it via an escalating series of signals.
154 signal_queue = [signal.SIGTERM, signal.SIGKILL]
155 for sig in signal_queue:
156 try:
157 os.kill(pid, sig)
158
159 # The process may have died before we could kill it.
160 except OSError:
161 pass
162
163 try:
164 for i in range(5):
165 status = os.waitpid(pid, os.WNOHANG)[0]
166 if status == pid:
167 return
168 time.sleep(1)
169
170 if status != pid:
171 raise AutoservRunError('Could not kill pid %d'
172 % pid, None)
173
174 # the process died before we join it.
175 except OSError:
176 pass
177
178
mbligh0e4613b2007-10-29 16:55:07 +0000179def _process_output(pipe, fbuffer, teefile=None, use_os_read=True):
180 if use_os_read:
181 data = os.read(pipe.fileno(), 1024)
182 else:
183 data = pipe.read()
184 fbuffer.write(data)
185 if teefile:
186 teefile.write(data)
187 teefile.flush()
188
189
190def _wait_for_command(subproc, start_time, timeout, stdout_file, stderr_file,
191 stdout_tee, stderr_tee):
192 if timeout:
193 stop_time = start_time + timeout
194 time_left = stop_time - time.time()
195 else:
196 time_left = None # so that select never times out
197 while not timeout or time_left > 0:
198 # select will return when stdout is ready (including when it is
199 # EOF, that is the process has terminated).
200 ready, _, _ = select.select([subproc.stdout, subproc.stderr],
201 [], [], time_left)
202 # os.read() has to be used instead of
203 # subproc.stdout.read() which will otherwise block
204 if subproc.stdout in ready:
205 _process_output(subproc.stdout, stdout_file,
206 stdout_tee)
207 if subproc.stderr in ready:
208 _process_output(subproc.stderr, stderr_file,
209 stderr_tee)
210
mbligh35f43952008-03-05 17:02:08 +0000211 exit_status_indication = subproc.poll()
212
213 if exit_status_indication is not None:
mblighc9f342d2007-11-28 22:29:23 +0000214 return exit_status_indication
mbligh0e4613b2007-10-29 16:55:07 +0000215 if timeout:
216 time_left = stop_time - time.time()
217
218 # the process has not terminated within timeout,
219 # kill it via an escalating series of signals.
mbligh35f43952008-03-05 17:02:08 +0000220 if exit_status_indication is None:
mblighc9f342d2007-11-28 22:29:23 +0000221 __nuke_subprocess(subproc)
mblighff6b4022008-01-10 16:20:51 +0000222 raise AutoservRunError('Command not complete within %s seconds'
mbligh6a2a2df2008-01-16 17:41:55 +0000223 % timeout, None)
mbligh0e4613b2007-10-29 16:55:07 +0000224
225
226def run(command, timeout=None, ignore_status=False,
227 stdout_tee=None, stderr_tee=None):
mblighdc735a22007-08-02 16:54:37 +0000228 """
229 Run a command on the host.
230
mblighdcd57a82007-07-11 23:06:47 +0000231 Args:
232 command: the command line string
mbligh0e4613b2007-10-29 16:55:07 +0000233 timeout: time limit in seconds before attempting to
mblighdcd57a82007-07-11 23:06:47 +0000234 kill the running process. The run() function
235 will take a few seconds longer than 'timeout'
236 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000237 ignore_status: do not raise an exception, no matter what
238 the exit code of the command is.
mbligh0e4613b2007-10-29 16:55:07 +0000239 stdout_tee: optional file-like object to which stdout data
240 will be written as it is generated (data will still
241 be stored in result.stdout)
242 stderr_tee: likewise for stderr
243
mblighdcd57a82007-07-11 23:06:47 +0000244 Returns:
mblighc9f342d2007-11-28 22:29:23 +0000245 a CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000246
mblighdcd57a82007-07-11 23:06:47 +0000247 Raises:
mbligh0e4613b2007-10-29 16:55:07 +0000248 AutoservRunError: the exit code of the command
mblighdcd57a82007-07-11 23:06:47 +0000249 execution was not 0
mblighdcd57a82007-07-11 23:06:47 +0000250 """
mblighc9f342d2007-11-28 22:29:23 +0000251 result = CmdResult(command)
mbligh0e4613b2007-10-29 16:55:07 +0000252 sp = subprocess.Popen(command, stdout=subprocess.PIPE,
mbligh3c647c32008-02-08 16:51:02 +0000253 stderr=subprocess.PIPE,
mbligh0e4613b2007-10-29 16:55:07 +0000254 shell=True, executable="/bin/bash")
255 stdout_file = StringIO.StringIO()
256 stderr_file = StringIO.StringIO()
mbligh0dd2ae02007-08-01 17:31:10 +0000257
258 try:
259 # We are holding ends to stdin, stdout pipes
260 # hence we need to be sure to close those fds no mater what
mbligh0e4613b2007-10-29 16:55:07 +0000261 start_time = time.time()
mbligh34faa282008-01-16 17:44:49 +0000262 ret = _wait_for_command(sp, start_time, timeout, stdout_file,
263 stderr_file, stdout_tee, stderr_tee)
mblighe13239a2008-03-10 15:06:11 +0000264 result.exit_status = ret
mbligh0dd2ae02007-08-01 17:31:10 +0000265
266 result.duration = time.time() - start_time
mbligh0e4613b2007-10-29 16:55:07 +0000267 # don't use os.read now, so we get all the rest of the output
268 _process_output(sp.stdout, stdout_file, stdout_tee,
269 use_os_read=False)
270 _process_output(sp.stderr, stderr_file, stderr_tee,
271 use_os_read=False)
mbligh0dd2ae02007-08-01 17:31:10 +0000272 finally:
273 # close our ends of the pipes to the sp no matter what
274 sp.stdout.close()
275 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000276
mbligh0e4613b2007-10-29 16:55:07 +0000277 result.stdout = stdout_file.getvalue()
278 result.stderr = stderr_file.getvalue()
279
mblighcf965b02007-07-25 16:49:45 +0000280 if not ignore_status and result.exit_status > 0:
mbligh03f4fc72007-11-29 20:56:14 +0000281 raise AutoservRunError("command execution error", result)
mbligh0dd2ae02007-08-01 17:31:10 +0000282
mblighdcd57a82007-07-11 23:06:47 +0000283 return result
284
285
mbligh5f876ad2007-10-12 23:59:53 +0000286def system(command, timeout=None, ignore_status=False):
mbligh10b3a082008-01-10 16:35:29 +0000287 return run(command, timeout, ignore_status,
288 stdout_tee=sys.stdout, stderr_tee=sys.stderr).exit_status
mbligh5f876ad2007-10-12 23:59:53 +0000289
290
291def system_output(command, timeout=None, ignore_status=False):
mblighd60b0322008-02-12 20:58:43 +0000292 out = run(command, timeout, ignore_status).stdout
293 if out[-1:] == '\n': out = out[:-1]
294 return out
mbligh5f876ad2007-10-12 23:59:53 +0000295
296
mblighdcd57a82007-07-11 23:06:47 +0000297def get_tmp_dir():
298 """Return the pathname of a directory on the host suitable
299 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000300
mblighdcd57a82007-07-11 23:06:47 +0000301 The directory and its content will be deleted automatically
302 at the end of the program execution if they are still present.
303 """
304 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000305
mblighdcd57a82007-07-11 23:06:47 +0000306 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000307 pid = os.getpid()
308 if not pid in __tmp_dirs:
309 __tmp_dirs[pid] = []
310 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000311 return dir_name
312
313
314@atexit.register
315def __clean_tmp_dirs():
316 """Erase temporary directories that were created by the get_tmp_dir()
317 function and that are still present.
318 """
319 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000320
mblighbea56822007-08-31 08:53:40 +0000321 pid = os.getpid()
322 if pid not in __tmp_dirs:
323 return
324 for dir in __tmp_dirs[pid]:
325 try:
326 shutil.rmtree(dir)
327 except OSError, e:
328 if e.errno == 2:
329 pass
330 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000331
332
333def unarchive(host, source_material):
334 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000335
mblighc8949b82007-07-23 16:33:58 +0000336 If the "source_material" is compresses (according to the file
337 extension) it will be uncompressed. Supported compression formats
338 are gzip and bzip2. Afterwards, if the source_material is a tar
339 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000340
mblighc8949b82007-07-23 16:33:58 +0000341 Args:
342 host: the host object on which the archive is located
343 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000344
mblighc8949b82007-07-23 16:33:58 +0000345 Returns:
346 The file or directory name of the unarchived source material.
347 If the material is a tar archive, it will be extracted in the
348 directory where it is and the path returned will be the first
349 entry in the archive, assuming it is the topmost directory.
350 If the material is not an archive, nothing will be done so this
351 function is "harmless" when it is "useless".
352 """
353 # uncompress
354 if (source_material.endswith(".gz") or
355 source_material.endswith(".gzip")):
356 host.run('gunzip "%s"' % (sh_escape(source_material)))
357 source_material= ".".join(source_material.split(".")[:-1])
358 elif source_material.endswith("bz2"):
359 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
360 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000361
mblighc8949b82007-07-23 16:33:58 +0000362 # untar
363 if source_material.endswith(".tar"):
364 retval= host.run('tar -C "%s" -xvf "%s"' % (
365 sh_escape(os.path.dirname(source_material)),
366 sh_escape(source_material),))
367 source_material= os.path.join(os.path.dirname(source_material),
368 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000369
mblighc8949b82007-07-23 16:33:58 +0000370 return source_material
mblighf1c52842007-10-16 15:21:38 +0000371
372
mbligh9708f732007-10-18 03:18:54 +0000373def 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
mbligh34a3fd72007-12-10 17:16:22 +0000378def find_pid(command):
379 for line in system_output('ps -eo pid,cmd').rstrip().split('\n'):
380 (pid, cmd) = line.split(None, 1)
381 if re.search(command, cmd):
382 return int(pid)
383 return None
384
385
386def nohup(command, stdout='/dev/null', stderr='/dev/null', background=True,
387 env = {}):
388 cmd = ' '.join(key+'='+val for key, val in env.iteritems())
389 cmd += ' nohup ' + command
390 cmd += ' > %s' % stdout
391 if stdout == stderr:
392 cmd += ' 2>&1'
393 else:
394 cmd += ' 2> %s' % stderr
395 if background:
396 cmd += ' &'
397 system(cmd)
398
399
mbligh40f122a2007-11-03 23:08:46 +0000400class AutoservOptionParser:
401 """Custom command-line options parser for autoserv.
402
403 We can't use the general getopt methods here, as there will be unknown
404 extra arguments that we pass down into the control file instead.
405 Thus we process the arguments by hand, for which we are duly repentant.
406 Making a single function here just makes it harder to read. Suck it up.
407 """
408
409 def __init__(self, args):
410 self.args = args
411
412
413 def parse_opts(self, flag):
414 if self.args.count(flag):
415 idx = self.args.index(flag)
416 self.args[idx : idx+1] = []
417 return True
418 else:
419 return False
420
421
422 def parse_opts_param(self, flag, default = None, split = False):
423 if self.args.count(flag):
424 idx = self.args.index(flag)
425 ret = self.args[idx+1]
426 self.args[idx : idx+2] = []
427 if split:
428 return ret.split(split)
429 else:
430 return ret
431 else:
432 return default
mblighc9f342d2007-11-28 22:29:23 +0000433
434
435class CmdResult(object):
436 """
437 Command execution result.
438
439 command: String containing the command line itself
440 exit_status: Integer exit code of the process
441 stdout: String containing stdout of the process
442 stderr: String containing stderr of the process
443 duration: Elapsed wall clock time running the process
444 """
445
446 def __init__(self, command = None):
447 self.command = command
448 self.exit_status = None
449 self.stdout = ""
450 self.stderr = ""
451 self.duration = 0
452
453
454 def __repr__(self):
455 wrapper = textwrap.TextWrapper(width = 78,
456 initial_indent="\n ",
457 subsequent_indent=" ")
458
459 stdout = self.stdout.rstrip()
460 if stdout:
461 stdout = "\nstdout:\n%s" % stdout
462
463 stderr = self.stderr.rstrip()
464 if stderr:
465 stderr = "\nstderr:\n%s" % stderr
466
467 return ("* Command: %s\n"
468 "Exit status: %s\n"
469 "Duration: %s\n"
470 "%s"
471 "%s"
472 % (wrapper.fill(self.command), self.exit_status,
473 self.duration, stdout, stderr))