blob: d3217c4443467cbc88439086ae055d6342a75141 [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'
192 % timeout)
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()
mblighc9f342d2007-11-28 22:29:23 +0000231 result.exit_status = _wait_for_command(sp, start_time, timeout,
232 stdout_file, stderr_file, stdout_tee, stderr_tee)
mbligh0dd2ae02007-08-01 17:31:10 +0000233
234 result.duration = time.time() - start_time
mbligh0e4613b2007-10-29 16:55:07 +0000235 # don't use os.read now, so we get all the rest of the output
236 _process_output(sp.stdout, stdout_file, stdout_tee,
237 use_os_read=False)
238 _process_output(sp.stderr, stderr_file, stderr_tee,
239 use_os_read=False)
mbligh0dd2ae02007-08-01 17:31:10 +0000240 finally:
241 # close our ends of the pipes to the sp no matter what
242 sp.stdout.close()
243 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000244
mbligh0e4613b2007-10-29 16:55:07 +0000245 result.stdout = stdout_file.getvalue()
246 result.stderr = stderr_file.getvalue()
247
mblighcf965b02007-07-25 16:49:45 +0000248 if not ignore_status and result.exit_status > 0:
mbligh03f4fc72007-11-29 20:56:14 +0000249 raise AutoservRunError("command execution error", result)
mbligh0dd2ae02007-08-01 17:31:10 +0000250
mblighdcd57a82007-07-11 23:06:47 +0000251 return result
252
253
mbligh5f876ad2007-10-12 23:59:53 +0000254def system(command, timeout=None, ignore_status=False):
255 return run(command, timeout, ignore_status).exit_status
256
257
258def system_output(command, timeout=None, ignore_status=False):
259 return run(command, timeout, ignore_status).stdout
260
261
mblighdcd57a82007-07-11 23:06:47 +0000262def get_tmp_dir():
263 """Return the pathname of a directory on the host suitable
264 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000265
mblighdcd57a82007-07-11 23:06:47 +0000266 The directory and its content will be deleted automatically
267 at the end of the program execution if they are still present.
268 """
269 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000270
mblighdcd57a82007-07-11 23:06:47 +0000271 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000272 pid = os.getpid()
273 if not pid in __tmp_dirs:
274 __tmp_dirs[pid] = []
275 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000276 return dir_name
277
278
279@atexit.register
280def __clean_tmp_dirs():
281 """Erase temporary directories that were created by the get_tmp_dir()
282 function and that are still present.
283 """
284 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000285
mblighbea56822007-08-31 08:53:40 +0000286 pid = os.getpid()
287 if pid not in __tmp_dirs:
288 return
289 for dir in __tmp_dirs[pid]:
290 try:
291 shutil.rmtree(dir)
292 except OSError, e:
293 if e.errno == 2:
294 pass
295 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000296
297
298def unarchive(host, source_material):
299 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000300
mblighc8949b82007-07-23 16:33:58 +0000301 If the "source_material" is compresses (according to the file
302 extension) it will be uncompressed. Supported compression formats
303 are gzip and bzip2. Afterwards, if the source_material is a tar
304 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000305
mblighc8949b82007-07-23 16:33:58 +0000306 Args:
307 host: the host object on which the archive is located
308 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000309
mblighc8949b82007-07-23 16:33:58 +0000310 Returns:
311 The file or directory name of the unarchived source material.
312 If the material is a tar archive, it will be extracted in the
313 directory where it is and the path returned will be the first
314 entry in the archive, assuming it is the topmost directory.
315 If the material is not an archive, nothing will be done so this
316 function is "harmless" when it is "useless".
317 """
318 # uncompress
319 if (source_material.endswith(".gz") or
320 source_material.endswith(".gzip")):
321 host.run('gunzip "%s"' % (sh_escape(source_material)))
322 source_material= ".".join(source_material.split(".")[:-1])
323 elif source_material.endswith("bz2"):
324 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
325 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000326
mblighc8949b82007-07-23 16:33:58 +0000327 # untar
328 if source_material.endswith(".tar"):
329 retval= host.run('tar -C "%s" -xvf "%s"' % (
330 sh_escape(os.path.dirname(source_material)),
331 sh_escape(source_material),))
332 source_material= os.path.join(os.path.dirname(source_material),
333 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000334
mblighc8949b82007-07-23 16:33:58 +0000335 return source_material
mblighf1c52842007-10-16 15:21:38 +0000336
337
338def write_keyval(dirname, dictionary):
339 keyval = open(os.path.join(dirname, 'keyval'), 'w')
340 for key in dictionary.keys():
341 value = '%s' % dictionary[key] # convert numbers to strings
342 if re.search(r'\W', key):
343 raise 'Invalid key: ' + key
344 keyval.write('%s=%s\n' % (key, str(value)))
345 keyval.close()
346
mbligh05269362007-10-16 16:58:11 +0000347
348def update_version(srcdir, preserve_srcdir, new_version, install, *args, **dargs):
349 """
350 Make sure srcdir is version new_version
351
352 If not, delete it and install() the new version.
353
354 In the preserve_srcdir case, we just check it's up to date,
355 and if not, we rerun install, without removing srcdir
356 """
357 versionfile = srcdir + '/.version'
358 install_needed = True
359
360 if os.path.exists(srcdir):
361 if os.path.exists(versionfile):
362 old_version = pickle.load(open(versionfile, 'r'))
363 if (old_version == new_version):
364 install_needed = False
365
366 if install_needed:
367 if not preserve_srcdir:
368 system('rm -rf ' + srcdir)
369 install(*args, **dargs)
370 if os.path.exists(srcdir):
371 pickle.dump(new_version, open(versionfile, 'w'))
mbligh9708f732007-10-18 03:18:54 +0000372
373
374def get_server_dir():
375 path = os.path.dirname(sys.modules['utils'].__file__)
376 return os.path.abspath(path)
mbligh40f122a2007-11-03 23:08:46 +0000377
378
mbligh34a3fd72007-12-10 17:16:22 +0000379def find_pid(command):
380 for line in system_output('ps -eo pid,cmd').rstrip().split('\n'):
381 (pid, cmd) = line.split(None, 1)
382 if re.search(command, cmd):
383 return int(pid)
384 return None
385
386
387def nohup(command, stdout='/dev/null', stderr='/dev/null', background=True,
388 env = {}):
389 cmd = ' '.join(key+'='+val for key, val in env.iteritems())
390 cmd += ' nohup ' + command
391 cmd += ' > %s' % stdout
392 if stdout == stderr:
393 cmd += ' 2>&1'
394 else:
395 cmd += ' 2> %s' % stderr
396 if background:
397 cmd += ' &'
398 system(cmd)
399
400
mbligh40f122a2007-11-03 23:08:46 +0000401class AutoservOptionParser:
402 """Custom command-line options parser for autoserv.
403
404 We can't use the general getopt methods here, as there will be unknown
405 extra arguments that we pass down into the control file instead.
406 Thus we process the arguments by hand, for which we are duly repentant.
407 Making a single function here just makes it harder to read. Suck it up.
408 """
409
410 def __init__(self, args):
411 self.args = args
412
413
414 def parse_opts(self, flag):
415 if self.args.count(flag):
416 idx = self.args.index(flag)
417 self.args[idx : idx+1] = []
418 return True
419 else:
420 return False
421
422
423 def parse_opts_param(self, flag, default = None, split = False):
424 if self.args.count(flag):
425 idx = self.args.index(flag)
426 ret = self.args[idx+1]
427 self.args[idx : idx+2] = []
428 if split:
429 return ret.split(split)
430 else:
431 return ret
432 else:
433 return default
mblighc9f342d2007-11-28 22:29:23 +0000434
435
436class CmdResult(object):
437 """
438 Command execution result.
439
440 command: String containing the command line itself
441 exit_status: Integer exit code of the process
442 stdout: String containing stdout of the process
443 stderr: String containing stderr of the process
444 duration: Elapsed wall clock time running the process
445 """
446
447 def __init__(self, command = None):
448 self.command = command
449 self.exit_status = None
450 self.stdout = ""
451 self.stderr = ""
452 self.duration = 0
453
454
455 def __repr__(self):
456 wrapper = textwrap.TextWrapper(width = 78,
457 initial_indent="\n ",
458 subsequent_indent=" ")
459
460 stdout = self.stdout.rstrip()
461 if stdout:
462 stdout = "\nstdout:\n%s" % stdout
463
464 stderr = self.stderr.rstrip()
465 if stderr:
466 stderr = "\nstderr:\n%s" % stderr
467
468 return ("* Command: %s\n"
469 "Exit status: %s\n"
470 "Duration: %s\n"
471 "%s"
472 "%s"
473 % (wrapper.fill(self.command), self.exit_status,
474 self.duration, stdout, stderr))