blob: 9c534fbd687fc64586f7ec689d9d546484ad6bd8 [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
mblighc3aee0f2008-01-17 16:26:39 +0000148def nuke_pid(pid):
149 # the process has not terminated within timeout,
150 # kill it via an escalating series of signals.
151 signal_queue = [signal.SIGTERM, signal.SIGKILL]
152 for sig in signal_queue:
153 try:
154 os.kill(pid, sig)
155
156 # The process may have died before we could kill it.
157 except OSError:
158 pass
159
160 try:
161 for i in range(5):
162 status = os.waitpid(pid, os.WNOHANG)[0]
163 if status == pid:
164 return
165 time.sleep(1)
166
167 if status != pid:
168 raise AutoservRunError('Could not kill pid %d'
169 % pid, None)
170
171 # the process died before we join it.
172 except OSError:
173 pass
174
175
mbligh0e4613b2007-10-29 16:55:07 +0000176def _process_output(pipe, fbuffer, teefile=None, use_os_read=True):
177 if use_os_read:
178 data = os.read(pipe.fileno(), 1024)
179 else:
180 data = pipe.read()
181 fbuffer.write(data)
182 if teefile:
183 teefile.write(data)
184 teefile.flush()
185
186
187def _wait_for_command(subproc, start_time, timeout, stdout_file, stderr_file,
188 stdout_tee, stderr_tee):
189 if timeout:
190 stop_time = start_time + timeout
191 time_left = stop_time - time.time()
192 else:
193 time_left = None # so that select never times out
194 while not timeout or time_left > 0:
195 # select will return when stdout is ready (including when it is
196 # EOF, that is the process has terminated).
197 ready, _, _ = select.select([subproc.stdout, subproc.stderr],
198 [], [], time_left)
199 # os.read() has to be used instead of
200 # subproc.stdout.read() which will otherwise block
201 if subproc.stdout in ready:
202 _process_output(subproc.stdout, stdout_file,
203 stdout_tee)
204 if subproc.stderr in ready:
205 _process_output(subproc.stderr, stderr_file,
206 stderr_tee)
207
208 pid, exit_status_indication = os.waitpid(subproc.pid,
209 os.WNOHANG)
210 if pid:
mblighc9f342d2007-11-28 22:29:23 +0000211 return exit_status_indication
mbligh0e4613b2007-10-29 16:55:07 +0000212 if timeout:
213 time_left = stop_time - time.time()
214
215 # the process has not terminated within timeout,
216 # kill it via an escalating series of signals.
217 if not pid:
mblighc9f342d2007-11-28 22:29:23 +0000218 __nuke_subprocess(subproc)
mblighff6b4022008-01-10 16:20:51 +0000219 raise AutoservRunError('Command not complete within %s seconds'
mbligh6a2a2df2008-01-16 17:41:55 +0000220 % timeout, None)
mbligh0e4613b2007-10-29 16:55:07 +0000221
222
223def run(command, timeout=None, ignore_status=False,
224 stdout_tee=None, stderr_tee=None):
mblighdc735a22007-08-02 16:54:37 +0000225 """
226 Run a command on the host.
227
mblighdcd57a82007-07-11 23:06:47 +0000228 Args:
229 command: the command line string
mbligh0e4613b2007-10-29 16:55:07 +0000230 timeout: time limit in seconds before attempting to
mblighdcd57a82007-07-11 23:06:47 +0000231 kill the running process. The run() function
232 will take a few seconds longer than 'timeout'
233 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000234 ignore_status: do not raise an exception, no matter what
235 the exit code of the command is.
mbligh0e4613b2007-10-29 16:55:07 +0000236 stdout_tee: optional file-like object to which stdout data
237 will be written as it is generated (data will still
238 be stored in result.stdout)
239 stderr_tee: likewise for stderr
240
mblighdcd57a82007-07-11 23:06:47 +0000241 Returns:
mblighc9f342d2007-11-28 22:29:23 +0000242 a CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000243
mblighdcd57a82007-07-11 23:06:47 +0000244 Raises:
mbligh0e4613b2007-10-29 16:55:07 +0000245 AutoservRunError: the exit code of the command
mblighdcd57a82007-07-11 23:06:47 +0000246 execution was not 0
mblighdcd57a82007-07-11 23:06:47 +0000247 """
mblighc9f342d2007-11-28 22:29:23 +0000248 result = CmdResult(command)
mbligh0e4613b2007-10-29 16:55:07 +0000249 sp = subprocess.Popen(command, stdout=subprocess.PIPE,
250 stderr=subprocess.PIPE, close_fds=True,
251 shell=True, executable="/bin/bash")
252 stdout_file = StringIO.StringIO()
253 stderr_file = StringIO.StringIO()
mbligh0dd2ae02007-08-01 17:31:10 +0000254
255 try:
256 # We are holding ends to stdin, stdout pipes
257 # hence we need to be sure to close those fds no mater what
mbligh0e4613b2007-10-29 16:55:07 +0000258 start_time = time.time()
mbligh34faa282008-01-16 17:44:49 +0000259 ret = _wait_for_command(sp, start_time, timeout, stdout_file,
260 stderr_file, stdout_tee, stderr_tee)
261 result.exit_status = ret >> 8
mbligh0dd2ae02007-08-01 17:31:10 +0000262
263 result.duration = time.time() - start_time
mbligh0e4613b2007-10-29 16:55:07 +0000264 # don't use os.read now, so we get all the rest of the output
265 _process_output(sp.stdout, stdout_file, stdout_tee,
266 use_os_read=False)
267 _process_output(sp.stderr, stderr_file, stderr_tee,
268 use_os_read=False)
mbligh0dd2ae02007-08-01 17:31:10 +0000269 finally:
270 # close our ends of the pipes to the sp no matter what
271 sp.stdout.close()
272 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000273
mbligh0e4613b2007-10-29 16:55:07 +0000274 result.stdout = stdout_file.getvalue()
275 result.stderr = stderr_file.getvalue()
276
mblighcf965b02007-07-25 16:49:45 +0000277 if not ignore_status and result.exit_status > 0:
mbligh03f4fc72007-11-29 20:56:14 +0000278 raise AutoservRunError("command execution error", result)
mbligh0dd2ae02007-08-01 17:31:10 +0000279
mblighdcd57a82007-07-11 23:06:47 +0000280 return result
281
282
mbligh5f876ad2007-10-12 23:59:53 +0000283def system(command, timeout=None, ignore_status=False):
mbligh10b3a082008-01-10 16:35:29 +0000284 return run(command, timeout, ignore_status,
285 stdout_tee=sys.stdout, stderr_tee=sys.stderr).exit_status
mbligh5f876ad2007-10-12 23:59:53 +0000286
287
288def system_output(command, timeout=None, ignore_status=False):
289 return run(command, timeout, ignore_status).stdout
290
291
mblighdcd57a82007-07-11 23:06:47 +0000292def get_tmp_dir():
293 """Return the pathname of a directory on the host suitable
294 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000295
mblighdcd57a82007-07-11 23:06:47 +0000296 The directory and its content will be deleted automatically
297 at the end of the program execution if they are still present.
298 """
299 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000300
mblighdcd57a82007-07-11 23:06:47 +0000301 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000302 pid = os.getpid()
303 if not pid in __tmp_dirs:
304 __tmp_dirs[pid] = []
305 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000306 return dir_name
307
308
309@atexit.register
310def __clean_tmp_dirs():
311 """Erase temporary directories that were created by the get_tmp_dir()
312 function and that are still present.
313 """
314 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000315
mblighbea56822007-08-31 08:53:40 +0000316 pid = os.getpid()
317 if pid not in __tmp_dirs:
318 return
319 for dir in __tmp_dirs[pid]:
320 try:
321 shutil.rmtree(dir)
322 except OSError, e:
323 if e.errno == 2:
324 pass
325 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000326
327
328def unarchive(host, source_material):
329 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000330
mblighc8949b82007-07-23 16:33:58 +0000331 If the "source_material" is compresses (according to the file
332 extension) it will be uncompressed. Supported compression formats
333 are gzip and bzip2. Afterwards, if the source_material is a tar
334 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000335
mblighc8949b82007-07-23 16:33:58 +0000336 Args:
337 host: the host object on which the archive is located
338 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000339
mblighc8949b82007-07-23 16:33:58 +0000340 Returns:
341 The file or directory name of the unarchived source material.
342 If the material is a tar archive, it will be extracted in the
343 directory where it is and the path returned will be the first
344 entry in the archive, assuming it is the topmost directory.
345 If the material is not an archive, nothing will be done so this
346 function is "harmless" when it is "useless".
347 """
348 # uncompress
349 if (source_material.endswith(".gz") or
350 source_material.endswith(".gzip")):
351 host.run('gunzip "%s"' % (sh_escape(source_material)))
352 source_material= ".".join(source_material.split(".")[:-1])
353 elif source_material.endswith("bz2"):
354 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
355 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000356
mblighc8949b82007-07-23 16:33:58 +0000357 # untar
358 if source_material.endswith(".tar"):
359 retval= host.run('tar -C "%s" -xvf "%s"' % (
360 sh_escape(os.path.dirname(source_material)),
361 sh_escape(source_material),))
362 source_material= os.path.join(os.path.dirname(source_material),
363 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000364
mblighc8949b82007-07-23 16:33:58 +0000365 return source_material
mblighf1c52842007-10-16 15:21:38 +0000366
367
368def write_keyval(dirname, dictionary):
369 keyval = open(os.path.join(dirname, 'keyval'), 'w')
370 for key in dictionary.keys():
371 value = '%s' % dictionary[key] # convert numbers to strings
372 if re.search(r'\W', key):
mbligh4d6feff2008-01-14 16:48:56 +0000373 raise ValueError('Invalid key: %s' % key)
mblighf1c52842007-10-16 15:21:38 +0000374 keyval.write('%s=%s\n' % (key, str(value)))
375 keyval.close()
376
mbligh05269362007-10-16 16:58:11 +0000377
378def update_version(srcdir, preserve_srcdir, new_version, install, *args, **dargs):
379 """
380 Make sure srcdir is version new_version
381
382 If not, delete it and install() the new version.
383
384 In the preserve_srcdir case, we just check it's up to date,
385 and if not, we rerun install, without removing srcdir
386 """
387 versionfile = srcdir + '/.version'
388 install_needed = True
389
390 if os.path.exists(srcdir):
391 if os.path.exists(versionfile):
392 old_version = pickle.load(open(versionfile, 'r'))
393 if (old_version == new_version):
394 install_needed = False
395
396 if install_needed:
397 if not preserve_srcdir:
398 system('rm -rf ' + srcdir)
399 install(*args, **dargs)
400 if os.path.exists(srcdir):
401 pickle.dump(new_version, open(versionfile, 'w'))
mbligh9708f732007-10-18 03:18:54 +0000402
403
404def get_server_dir():
405 path = os.path.dirname(sys.modules['utils'].__file__)
406 return os.path.abspath(path)
mbligh40f122a2007-11-03 23:08:46 +0000407
408
mbligh34a3fd72007-12-10 17:16:22 +0000409def find_pid(command):
410 for line in system_output('ps -eo pid,cmd').rstrip().split('\n'):
411 (pid, cmd) = line.split(None, 1)
412 if re.search(command, cmd):
413 return int(pid)
414 return None
415
416
417def nohup(command, stdout='/dev/null', stderr='/dev/null', background=True,
418 env = {}):
419 cmd = ' '.join(key+'='+val for key, val in env.iteritems())
420 cmd += ' nohup ' + command
421 cmd += ' > %s' % stdout
422 if stdout == stderr:
423 cmd += ' 2>&1'
424 else:
425 cmd += ' 2> %s' % stderr
426 if background:
427 cmd += ' &'
428 system(cmd)
429
430
mbligh40f122a2007-11-03 23:08:46 +0000431class AutoservOptionParser:
432 """Custom command-line options parser for autoserv.
433
434 We can't use the general getopt methods here, as there will be unknown
435 extra arguments that we pass down into the control file instead.
436 Thus we process the arguments by hand, for which we are duly repentant.
437 Making a single function here just makes it harder to read. Suck it up.
438 """
439
440 def __init__(self, args):
441 self.args = args
442
443
444 def parse_opts(self, flag):
445 if self.args.count(flag):
446 idx = self.args.index(flag)
447 self.args[idx : idx+1] = []
448 return True
449 else:
450 return False
451
452
453 def parse_opts_param(self, flag, default = None, split = False):
454 if self.args.count(flag):
455 idx = self.args.index(flag)
456 ret = self.args[idx+1]
457 self.args[idx : idx+2] = []
458 if split:
459 return ret.split(split)
460 else:
461 return ret
462 else:
463 return default
mblighc9f342d2007-11-28 22:29:23 +0000464
465
466class CmdResult(object):
467 """
468 Command execution result.
469
470 command: String containing the command line itself
471 exit_status: Integer exit code of the process
472 stdout: String containing stdout of the process
473 stderr: String containing stderr of the process
474 duration: Elapsed wall clock time running the process
475 """
476
477 def __init__(self, command = None):
478 self.command = command
479 self.exit_status = None
480 self.stdout = ""
481 self.stderr = ""
482 self.duration = 0
483
484
485 def __repr__(self):
486 wrapper = textwrap.TextWrapper(width = 78,
487 initial_indent="\n ",
488 subsequent_indent=" ")
489
490 stdout = self.stdout.rstrip()
491 if stdout:
492 stdout = "\nstdout:\n%s" % stdout
493
494 stderr = self.stderr.rstrip()
495 if stderr:
496 stderr = "\nstderr:\n%s" % stderr
497
498 return ("* Command: %s\n"
499 "Exit status: %s\n"
500 "Duration: %s\n"
501 "%s"
502 "%s"
503 % (wrapper.fill(self.command), self.exit_status,
504 self.duration, stdout, stderr))