blob: 88862c3c7c5d4cce771d1d4fdb94e7ac6cf77445 [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)
191 raise CmdError('Command not complete within %s seconds' % timeout)
mbligh0e4613b2007-10-29 16:55:07 +0000192
193
194def run(command, timeout=None, ignore_status=False,
195 stdout_tee=None, stderr_tee=None):
mblighdc735a22007-08-02 16:54:37 +0000196 """
197 Run a command on the host.
198
mblighdcd57a82007-07-11 23:06:47 +0000199 Args:
200 command: the command line string
mbligh0e4613b2007-10-29 16:55:07 +0000201 timeout: time limit in seconds before attempting to
mblighdcd57a82007-07-11 23:06:47 +0000202 kill the running process. The run() function
203 will take a few seconds longer than 'timeout'
204 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000205 ignore_status: do not raise an exception, no matter what
206 the exit code of the command is.
mbligh0e4613b2007-10-29 16:55:07 +0000207 stdout_tee: optional file-like object to which stdout data
208 will be written as it is generated (data will still
209 be stored in result.stdout)
210 stderr_tee: likewise for stderr
211
mblighdcd57a82007-07-11 23:06:47 +0000212 Returns:
mblighc9f342d2007-11-28 22:29:23 +0000213 a CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000214
mblighdcd57a82007-07-11 23:06:47 +0000215 Raises:
mbligh0e4613b2007-10-29 16:55:07 +0000216 AutoservRunError: the exit code of the command
mblighdcd57a82007-07-11 23:06:47 +0000217 execution was not 0
mblighdcd57a82007-07-11 23:06:47 +0000218 """
mblighc9f342d2007-11-28 22:29:23 +0000219 result = CmdResult(command)
mbligh0e4613b2007-10-29 16:55:07 +0000220 sp = subprocess.Popen(command, stdout=subprocess.PIPE,
221 stderr=subprocess.PIPE, close_fds=True,
222 shell=True, executable="/bin/bash")
223 stdout_file = StringIO.StringIO()
224 stderr_file = StringIO.StringIO()
mbligh0dd2ae02007-08-01 17:31:10 +0000225
226 try:
227 # We are holding ends to stdin, stdout pipes
228 # hence we need to be sure to close those fds no mater what
mbligh0e4613b2007-10-29 16:55:07 +0000229 start_time = time.time()
mblighc9f342d2007-11-28 22:29:23 +0000230 result.exit_status = _wait_for_command(sp, start_time, timeout,
231 stdout_file, stderr_file, stdout_tee, stderr_tee)
mbligh0dd2ae02007-08-01 17:31:10 +0000232
233 result.duration = time.time() - start_time
mbligh0e4613b2007-10-29 16:55:07 +0000234 # don't use os.read now, so we get all the rest of the output
235 _process_output(sp.stdout, stdout_file, stdout_tee,
236 use_os_read=False)
237 _process_output(sp.stderr, stderr_file, stderr_tee,
238 use_os_read=False)
mbligh0dd2ae02007-08-01 17:31:10 +0000239 finally:
240 # close our ends of the pipes to the sp no matter what
241 sp.stdout.close()
242 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000243
mbligh0e4613b2007-10-29 16:55:07 +0000244 result.stdout = stdout_file.getvalue()
245 result.stderr = stderr_file.getvalue()
246
mblighcf965b02007-07-25 16:49:45 +0000247 if not ignore_status and result.exit_status > 0:
mbligh03f4fc72007-11-29 20:56:14 +0000248 raise AutoservRunError("command execution error", result)
mbligh0dd2ae02007-08-01 17:31:10 +0000249
mblighdcd57a82007-07-11 23:06:47 +0000250 return result
251
252
mbligh5f876ad2007-10-12 23:59:53 +0000253def system(command, timeout=None, ignore_status=False):
254 return run(command, timeout, ignore_status).exit_status
255
256
257def system_output(command, timeout=None, ignore_status=False):
258 return run(command, timeout, ignore_status).stdout
259
260
mblighdcd57a82007-07-11 23:06:47 +0000261def get_tmp_dir():
262 """Return the pathname of a directory on the host suitable
263 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000264
mblighdcd57a82007-07-11 23:06:47 +0000265 The directory and its content will be deleted automatically
266 at the end of the program execution if they are still present.
267 """
268 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000269
mblighdcd57a82007-07-11 23:06:47 +0000270 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000271 pid = os.getpid()
272 if not pid in __tmp_dirs:
273 __tmp_dirs[pid] = []
274 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000275 return dir_name
276
277
278@atexit.register
279def __clean_tmp_dirs():
280 """Erase temporary directories that were created by the get_tmp_dir()
281 function and that are still present.
282 """
283 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000284
mblighbea56822007-08-31 08:53:40 +0000285 pid = os.getpid()
286 if pid not in __tmp_dirs:
287 return
288 for dir in __tmp_dirs[pid]:
289 try:
290 shutil.rmtree(dir)
291 except OSError, e:
292 if e.errno == 2:
293 pass
294 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000295
296
297def unarchive(host, source_material):
298 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000299
mblighc8949b82007-07-23 16:33:58 +0000300 If the "source_material" is compresses (according to the file
301 extension) it will be uncompressed. Supported compression formats
302 are gzip and bzip2. Afterwards, if the source_material is a tar
303 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000304
mblighc8949b82007-07-23 16:33:58 +0000305 Args:
306 host: the host object on which the archive is located
307 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000308
mblighc8949b82007-07-23 16:33:58 +0000309 Returns:
310 The file or directory name of the unarchived source material.
311 If the material is a tar archive, it will be extracted in the
312 directory where it is and the path returned will be the first
313 entry in the archive, assuming it is the topmost directory.
314 If the material is not an archive, nothing will be done so this
315 function is "harmless" when it is "useless".
316 """
317 # uncompress
318 if (source_material.endswith(".gz") or
319 source_material.endswith(".gzip")):
320 host.run('gunzip "%s"' % (sh_escape(source_material)))
321 source_material= ".".join(source_material.split(".")[:-1])
322 elif source_material.endswith("bz2"):
323 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
324 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000325
mblighc8949b82007-07-23 16:33:58 +0000326 # untar
327 if source_material.endswith(".tar"):
328 retval= host.run('tar -C "%s" -xvf "%s"' % (
329 sh_escape(os.path.dirname(source_material)),
330 sh_escape(source_material),))
331 source_material= os.path.join(os.path.dirname(source_material),
332 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000333
mblighc8949b82007-07-23 16:33:58 +0000334 return source_material
mblighf1c52842007-10-16 15:21:38 +0000335
336
337def write_keyval(dirname, dictionary):
338 keyval = open(os.path.join(dirname, 'keyval'), 'w')
339 for key in dictionary.keys():
340 value = '%s' % dictionary[key] # convert numbers to strings
341 if re.search(r'\W', key):
342 raise 'Invalid key: ' + key
343 keyval.write('%s=%s\n' % (key, str(value)))
344 keyval.close()
345
mbligh05269362007-10-16 16:58:11 +0000346
347def update_version(srcdir, preserve_srcdir, new_version, install, *args, **dargs):
348 """
349 Make sure srcdir is version new_version
350
351 If not, delete it and install() the new version.
352
353 In the preserve_srcdir case, we just check it's up to date,
354 and if not, we rerun install, without removing srcdir
355 """
356 versionfile = srcdir + '/.version'
357 install_needed = True
358
359 if os.path.exists(srcdir):
360 if os.path.exists(versionfile):
361 old_version = pickle.load(open(versionfile, 'r'))
362 if (old_version == new_version):
363 install_needed = False
364
365 if install_needed:
366 if not preserve_srcdir:
367 system('rm -rf ' + srcdir)
368 install(*args, **dargs)
369 if os.path.exists(srcdir):
370 pickle.dump(new_version, open(versionfile, 'w'))
mbligh9708f732007-10-18 03:18:54 +0000371
372
373def 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))