blob: 75c472537558ec2a3fe9c720731a701caa64a64f [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
211 pid, exit_status_indication = os.waitpid(subproc.pid,
212 os.WNOHANG)
213 if pid:
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.
220 if not pid:
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,
253 stderr=subprocess.PIPE, close_fds=True,
254 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)
264 result.exit_status = ret >> 8
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):
292 return run(command, timeout, ignore_status).stdout
293
294
mblighdcd57a82007-07-11 23:06:47 +0000295def get_tmp_dir():
296 """Return the pathname of a directory on the host suitable
297 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000298
mblighdcd57a82007-07-11 23:06:47 +0000299 The directory and its content will be deleted automatically
300 at the end of the program execution if they are still present.
301 """
302 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000303
mblighdcd57a82007-07-11 23:06:47 +0000304 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000305 pid = os.getpid()
306 if not pid in __tmp_dirs:
307 __tmp_dirs[pid] = []
308 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000309 return dir_name
310
311
312@atexit.register
313def __clean_tmp_dirs():
314 """Erase temporary directories that were created by the get_tmp_dir()
315 function and that are still present.
316 """
317 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000318
mblighbea56822007-08-31 08:53:40 +0000319 pid = os.getpid()
320 if pid not in __tmp_dirs:
321 return
322 for dir in __tmp_dirs[pid]:
323 try:
324 shutil.rmtree(dir)
325 except OSError, e:
326 if e.errno == 2:
327 pass
328 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000329
330
331def unarchive(host, source_material):
332 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000333
mblighc8949b82007-07-23 16:33:58 +0000334 If the "source_material" is compresses (according to the file
335 extension) it will be uncompressed. Supported compression formats
336 are gzip and bzip2. Afterwards, if the source_material is a tar
337 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000338
mblighc8949b82007-07-23 16:33:58 +0000339 Args:
340 host: the host object on which the archive is located
341 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000342
mblighc8949b82007-07-23 16:33:58 +0000343 Returns:
344 The file or directory name of the unarchived source material.
345 If the material is a tar archive, it will be extracted in the
346 directory where it is and the path returned will be the first
347 entry in the archive, assuming it is the topmost directory.
348 If the material is not an archive, nothing will be done so this
349 function is "harmless" when it is "useless".
350 """
351 # uncompress
352 if (source_material.endswith(".gz") or
353 source_material.endswith(".gzip")):
354 host.run('gunzip "%s"' % (sh_escape(source_material)))
355 source_material= ".".join(source_material.split(".")[:-1])
356 elif source_material.endswith("bz2"):
357 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
358 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000359
mblighc8949b82007-07-23 16:33:58 +0000360 # untar
361 if source_material.endswith(".tar"):
362 retval= host.run('tar -C "%s" -xvf "%s"' % (
363 sh_escape(os.path.dirname(source_material)),
364 sh_escape(source_material),))
365 source_material= os.path.join(os.path.dirname(source_material),
366 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000367
mblighc8949b82007-07-23 16:33:58 +0000368 return source_material
mblighf1c52842007-10-16 15:21:38 +0000369
370
mbligh9708f732007-10-18 03:18:54 +0000371def get_server_dir():
372 path = os.path.dirname(sys.modules['utils'].__file__)
373 return os.path.abspath(path)
mbligh40f122a2007-11-03 23:08:46 +0000374
375
mbligh34a3fd72007-12-10 17:16:22 +0000376def find_pid(command):
377 for line in system_output('ps -eo pid,cmd').rstrip().split('\n'):
378 (pid, cmd) = line.split(None, 1)
379 if re.search(command, cmd):
380 return int(pid)
381 return None
382
383
384def nohup(command, stdout='/dev/null', stderr='/dev/null', background=True,
385 env = {}):
386 cmd = ' '.join(key+'='+val for key, val in env.iteritems())
387 cmd += ' nohup ' + command
388 cmd += ' > %s' % stdout
389 if stdout == stderr:
390 cmd += ' 2>&1'
391 else:
392 cmd += ' 2> %s' % stderr
393 if background:
394 cmd += ' &'
395 system(cmd)
396
397
mbligh40f122a2007-11-03 23:08:46 +0000398class AutoservOptionParser:
399 """Custom command-line options parser for autoserv.
400
401 We can't use the general getopt methods here, as there will be unknown
402 extra arguments that we pass down into the control file instead.
403 Thus we process the arguments by hand, for which we are duly repentant.
404 Making a single function here just makes it harder to read. Suck it up.
405 """
406
407 def __init__(self, args):
408 self.args = args
409
410
411 def parse_opts(self, flag):
412 if self.args.count(flag):
413 idx = self.args.index(flag)
414 self.args[idx : idx+1] = []
415 return True
416 else:
417 return False
418
419
420 def parse_opts_param(self, flag, default = None, split = False):
421 if self.args.count(flag):
422 idx = self.args.index(flag)
423 ret = self.args[idx+1]
424 self.args[idx : idx+2] = []
425 if split:
426 return ret.split(split)
427 else:
428 return ret
429 else:
430 return default
mblighc9f342d2007-11-28 22:29:23 +0000431
432
433class CmdResult(object):
434 """
435 Command execution result.
436
437 command: String containing the command line itself
438 exit_status: Integer exit code of the process
439 stdout: String containing stdout of the process
440 stderr: String containing stderr of the process
441 duration: Elapsed wall clock time running the process
442 """
443
444 def __init__(self, command = None):
445 self.command = command
446 self.exit_status = None
447 self.stdout = ""
448 self.stderr = ""
449 self.duration = 0
450
451
452 def __repr__(self):
453 wrapper = textwrap.TextWrapper(width = 78,
454 initial_indent="\n ",
455 subsequent_indent=" ")
456
457 stdout = self.stdout.rstrip()
458 if stdout:
459 stdout = "\nstdout:\n%s" % stdout
460
461 stderr = self.stderr.rstrip()
462 if stderr:
463 stderr = "\nstderr:\n%s" % stderr
464
465 return ("* Command: %s\n"
466 "Exit status: %s\n"
467 "Duration: %s\n"
468 "%s"
469 "%s"
470 % (wrapper.fill(self.command), self.exit_status,
471 self.duration, stdout, stderr))