blob: 2dc7c908b863e55eaab0b2054617dee51e58749e [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)
mblighff6b4022008-01-10 16:20:51 +0000191 raise AutoservRunError('Command not complete within %s seconds'
mbligh6a2a2df2008-01-16 17:41:55 +0000192 % timeout, None)
mbligh0e4613b2007-10-29 16:55:07 +0000193
194
195def run(command, timeout=None, ignore_status=False,
196 stdout_tee=None, stderr_tee=None):
mblighdc735a22007-08-02 16:54:37 +0000197 """
198 Run a command on the host.
199
mblighdcd57a82007-07-11 23:06:47 +0000200 Args:
201 command: the command line string
mbligh0e4613b2007-10-29 16:55:07 +0000202 timeout: time limit in seconds before attempting to
mblighdcd57a82007-07-11 23:06:47 +0000203 kill the running process. The run() function
204 will take a few seconds longer than 'timeout'
205 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000206 ignore_status: do not raise an exception, no matter what
207 the exit code of the command is.
mbligh0e4613b2007-10-29 16:55:07 +0000208 stdout_tee: optional file-like object to which stdout data
209 will be written as it is generated (data will still
210 be stored in result.stdout)
211 stderr_tee: likewise for stderr
212
mblighdcd57a82007-07-11 23:06:47 +0000213 Returns:
mblighc9f342d2007-11-28 22:29:23 +0000214 a CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000215
mblighdcd57a82007-07-11 23:06:47 +0000216 Raises:
mbligh0e4613b2007-10-29 16:55:07 +0000217 AutoservRunError: the exit code of the command
mblighdcd57a82007-07-11 23:06:47 +0000218 execution was not 0
mblighdcd57a82007-07-11 23:06:47 +0000219 """
mblighc9f342d2007-11-28 22:29:23 +0000220 result = CmdResult(command)
mbligh0e4613b2007-10-29 16:55:07 +0000221 sp = subprocess.Popen(command, stdout=subprocess.PIPE,
222 stderr=subprocess.PIPE, close_fds=True,
223 shell=True, executable="/bin/bash")
224 stdout_file = StringIO.StringIO()
225 stderr_file = StringIO.StringIO()
mbligh0dd2ae02007-08-01 17:31:10 +0000226
227 try:
228 # We are holding ends to stdin, stdout pipes
229 # hence we need to be sure to close those fds no mater what
mbligh0e4613b2007-10-29 16:55:07 +0000230 start_time = time.time()
mbligh34faa282008-01-16 17:44:49 +0000231 ret = _wait_for_command(sp, start_time, timeout, stdout_file,
232 stderr_file, stdout_tee, stderr_tee)
233 result.exit_status = ret >> 8
mbligh0dd2ae02007-08-01 17:31:10 +0000234
235 result.duration = time.time() - start_time
mbligh0e4613b2007-10-29 16:55:07 +0000236 # don't use os.read now, so we get all the rest of the output
237 _process_output(sp.stdout, stdout_file, stdout_tee,
238 use_os_read=False)
239 _process_output(sp.stderr, stderr_file, stderr_tee,
240 use_os_read=False)
mbligh0dd2ae02007-08-01 17:31:10 +0000241 finally:
242 # close our ends of the pipes to the sp no matter what
243 sp.stdout.close()
244 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000245
mbligh0e4613b2007-10-29 16:55:07 +0000246 result.stdout = stdout_file.getvalue()
247 result.stderr = stderr_file.getvalue()
248
mblighcf965b02007-07-25 16:49:45 +0000249 if not ignore_status and result.exit_status > 0:
mbligh03f4fc72007-11-29 20:56:14 +0000250 raise AutoservRunError("command execution error", result)
mbligh0dd2ae02007-08-01 17:31:10 +0000251
mblighdcd57a82007-07-11 23:06:47 +0000252 return result
253
254
mbligh5f876ad2007-10-12 23:59:53 +0000255def system(command, timeout=None, ignore_status=False):
mbligh10b3a082008-01-10 16:35:29 +0000256 return run(command, timeout, ignore_status,
257 stdout_tee=sys.stdout, stderr_tee=sys.stderr).exit_status
mbligh5f876ad2007-10-12 23:59:53 +0000258
259
260def system_output(command, timeout=None, ignore_status=False):
261 return run(command, timeout, ignore_status).stdout
262
263
mblighdcd57a82007-07-11 23:06:47 +0000264def get_tmp_dir():
265 """Return the pathname of a directory on the host suitable
266 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000267
mblighdcd57a82007-07-11 23:06:47 +0000268 The directory and its content will be deleted automatically
269 at the end of the program execution if they are still present.
270 """
271 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000272
mblighdcd57a82007-07-11 23:06:47 +0000273 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000274 pid = os.getpid()
275 if not pid in __tmp_dirs:
276 __tmp_dirs[pid] = []
277 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000278 return dir_name
279
280
281@atexit.register
282def __clean_tmp_dirs():
283 """Erase temporary directories that were created by the get_tmp_dir()
284 function and that are still present.
285 """
286 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000287
mblighbea56822007-08-31 08:53:40 +0000288 pid = os.getpid()
289 if pid not in __tmp_dirs:
290 return
291 for dir in __tmp_dirs[pid]:
292 try:
293 shutil.rmtree(dir)
294 except OSError, e:
295 if e.errno == 2:
296 pass
297 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000298
299
300def unarchive(host, source_material):
301 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000302
mblighc8949b82007-07-23 16:33:58 +0000303 If the "source_material" is compresses (according to the file
304 extension) it will be uncompressed. Supported compression formats
305 are gzip and bzip2. Afterwards, if the source_material is a tar
306 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000307
mblighc8949b82007-07-23 16:33:58 +0000308 Args:
309 host: the host object on which the archive is located
310 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000311
mblighc8949b82007-07-23 16:33:58 +0000312 Returns:
313 The file or directory name of the unarchived source material.
314 If the material is a tar archive, it will be extracted in the
315 directory where it is and the path returned will be the first
316 entry in the archive, assuming it is the topmost directory.
317 If the material is not an archive, nothing will be done so this
318 function is "harmless" when it is "useless".
319 """
320 # uncompress
321 if (source_material.endswith(".gz") or
322 source_material.endswith(".gzip")):
323 host.run('gunzip "%s"' % (sh_escape(source_material)))
324 source_material= ".".join(source_material.split(".")[:-1])
325 elif source_material.endswith("bz2"):
326 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
327 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000328
mblighc8949b82007-07-23 16:33:58 +0000329 # untar
330 if source_material.endswith(".tar"):
331 retval= host.run('tar -C "%s" -xvf "%s"' % (
332 sh_escape(os.path.dirname(source_material)),
333 sh_escape(source_material),))
334 source_material= os.path.join(os.path.dirname(source_material),
335 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000336
mblighc8949b82007-07-23 16:33:58 +0000337 return source_material
mblighf1c52842007-10-16 15:21:38 +0000338
339
340def write_keyval(dirname, dictionary):
341 keyval = open(os.path.join(dirname, 'keyval'), 'w')
342 for key in dictionary.keys():
343 value = '%s' % dictionary[key] # convert numbers to strings
344 if re.search(r'\W', key):
mbligh4d6feff2008-01-14 16:48:56 +0000345 raise ValueError('Invalid key: %s' % key)
mblighf1c52842007-10-16 15:21:38 +0000346 keyval.write('%s=%s\n' % (key, str(value)))
347 keyval.close()
348
mbligh05269362007-10-16 16:58:11 +0000349
350def update_version(srcdir, preserve_srcdir, new_version, install, *args, **dargs):
351 """
352 Make sure srcdir is version new_version
353
354 If not, delete it and install() the new version.
355
356 In the preserve_srcdir case, we just check it's up to date,
357 and if not, we rerun install, without removing srcdir
358 """
359 versionfile = srcdir + '/.version'
360 install_needed = True
361
362 if os.path.exists(srcdir):
363 if os.path.exists(versionfile):
364 old_version = pickle.load(open(versionfile, 'r'))
365 if (old_version == new_version):
366 install_needed = False
367
368 if install_needed:
369 if not preserve_srcdir:
370 system('rm -rf ' + srcdir)
371 install(*args, **dargs)
372 if os.path.exists(srcdir):
373 pickle.dump(new_version, open(versionfile, 'w'))
mbligh9708f732007-10-18 03:18:54 +0000374
375
376def get_server_dir():
377 path = os.path.dirname(sys.modules['utils'].__file__)
378 return os.path.abspath(path)
mbligh40f122a2007-11-03 23:08:46 +0000379
380
mbligh34a3fd72007-12-10 17:16:22 +0000381def find_pid(command):
382 for line in system_output('ps -eo pid,cmd').rstrip().split('\n'):
383 (pid, cmd) = line.split(None, 1)
384 if re.search(command, cmd):
385 return int(pid)
386 return None
387
388
389def nohup(command, stdout='/dev/null', stderr='/dev/null', background=True,
390 env = {}):
391 cmd = ' '.join(key+'='+val for key, val in env.iteritems())
392 cmd += ' nohup ' + command
393 cmd += ' > %s' % stdout
394 if stdout == stderr:
395 cmd += ' 2>&1'
396 else:
397 cmd += ' 2> %s' % stderr
398 if background:
399 cmd += ' &'
400 system(cmd)
401
402
mbligh40f122a2007-11-03 23:08:46 +0000403class AutoservOptionParser:
404 """Custom command-line options parser for autoserv.
405
406 We can't use the general getopt methods here, as there will be unknown
407 extra arguments that we pass down into the control file instead.
408 Thus we process the arguments by hand, for which we are duly repentant.
409 Making a single function here just makes it harder to read. Suck it up.
410 """
411
412 def __init__(self, args):
413 self.args = args
414
415
416 def parse_opts(self, flag):
417 if self.args.count(flag):
418 idx = self.args.index(flag)
419 self.args[idx : idx+1] = []
420 return True
421 else:
422 return False
423
424
425 def parse_opts_param(self, flag, default = None, split = False):
426 if self.args.count(flag):
427 idx = self.args.index(flag)
428 ret = self.args[idx+1]
429 self.args[idx : idx+2] = []
430 if split:
431 return ret.split(split)
432 else:
433 return ret
434 else:
435 return default
mblighc9f342d2007-11-28 22:29:23 +0000436
437
438class CmdResult(object):
439 """
440 Command execution result.
441
442 command: String containing the command line itself
443 exit_status: Integer exit code of the process
444 stdout: String containing stdout of the process
445 stderr: String containing stderr of the process
446 duration: Elapsed wall clock time running the process
447 """
448
449 def __init__(self, command = None):
450 self.command = command
451 self.exit_status = None
452 self.stdout = ""
453 self.stderr = ""
454 self.duration = 0
455
456
457 def __repr__(self):
458 wrapper = textwrap.TextWrapper(width = 78,
459 initial_indent="\n ",
460 subsequent_indent=" ")
461
462 stdout = self.stdout.rstrip()
463 if stdout:
464 stdout = "\nstdout:\n%s" % stdout
465
466 stderr = self.stderr.rstrip()
467 if stderr:
468 stderr = "\nstderr:\n%s" % stderr
469
470 return ("* Command: %s\n"
471 "Exit status: %s\n"
472 "Duration: %s\n"
473 "%s"
474 "%s"
475 % (wrapper.fill(self.command), self.exit_status,
476 self.duration, stdout, stderr))