blob: 4f4b4ea27385ef24ac0cd6e92bc9dc092c820b22 [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
mbligh9708f732007-10-18 03:18:54 +000016import time, types, urllib, re, sys
mblighdcd57a82007-07-11 23:06:47 +000017
mblighf1c52842007-10-16 15:21:38 +000018import hosts, errors
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
75def get(location):
76 """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)):
109 tmpfile = os.path.join(tmpdir, os.path.basename(location))
110 if os.path.isdir(location):
111 tmpfile += '/'
112 shutil.copytree(location, tmpfile, symlinks=True)
113 return tmpfile
114 shutil.copyfile(location, tmpfile)
115 return tmpfile
116 # location is just a string, dump it to a file
117 else:
118 tmpfd, tmpfile = tempfile.mkstemp(dir=tmpdir)
119 tmpfileobj = os.fdopen(tmpfd, 'w')
120 tmpfileobj.write(location)
121 tmpfileobj.close()
122 return tmpfile
123
124
mblighcf965b02007-07-25 16:49:45 +0000125def run(command, timeout=None, ignore_status=False):
mblighdc735a22007-08-02 16:54:37 +0000126 """
127 Run a command on the host.
128
mblighdcd57a82007-07-11 23:06:47 +0000129 Args:
130 command: the command line string
131 timeout: time limit in seconds before attempting to
132 kill the running process. The run() function
133 will take a few seconds longer than 'timeout'
134 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000135 ignore_status: do not raise an exception, no matter what
136 the exit code of the command is.
137
mblighdcd57a82007-07-11 23:06:47 +0000138 Returns:
139 a hosts.CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000140
mblighdcd57a82007-07-11 23:06:47 +0000141 Raises:
142 AutoservRunError: the exit code of the command
143 execution was not 0
mblighdc735a22007-08-02 16:54:37 +0000144
mblighdcd57a82007-07-11 23:06:47 +0000145 TODO(poirier): Add a "tee" option to send the command's
146 stdout and stderr to python's stdout and stderr? At
147 the moment, there is no way to see the command's
148 output as it is running.
149 TODO(poirier): Should a timeout raise an exception? Should
150 exceptions be raised at all?
151 """
152 result= hosts.CmdResult()
153 result.command= command
154 sp= subprocess.Popen(command, stdout=subprocess.PIPE,
155 stderr=subprocess.PIPE, close_fds=True, shell=True,
156 executable="/bin/bash")
mbligh0dd2ae02007-08-01 17:31:10 +0000157
158 try:
159 # We are holding ends to stdin, stdout pipes
160 # hence we need to be sure to close those fds no mater what
161 start_time= time.time()
162 if timeout:
163 stop_time= start_time + timeout
mblighdcd57a82007-07-11 23:06:47 +0000164 time_left= stop_time - time.time()
mbligh0dd2ae02007-08-01 17:31:10 +0000165 while time_left > 0:
166 # select will return when stdout is ready
167 # (including when it is EOF, that is the
168 # process has terminated).
169 (retval, tmp, tmp) = select.select(
170 [sp.stdout], [], [], time_left)
171 if len(retval):
172 # os.read() has to be used instead of
173 # sp.stdout.read() which will
174 # otherwise block
175 result.stdout += os.read(
176 sp.stdout.fileno(), 1024)
177
178 (pid, exit_status_indication) = os.waitpid(
179 sp.pid, os.WNOHANG)
180 if pid:
181 stop_time= time.time()
182 time_left= stop_time - time.time()
183
184 # the process has not terminated within timeout,
185 # kill it via an escalating series of signals.
186 if not pid:
187 signal_queue = [signal.SIGTERM, signal.SIGKILL]
188 for sig in signal_queue:
189 try:
190 os.kill(sp.pid, sig)
191 # handle race condition in which
192 # process died before we could kill it.
193 except OSError:
194 pass
195
196 for i in range(5):
197 (pid, exit_status_indication
198 ) = os.waitpid(sp.pid,
199 os.WNOHANG)
200 if pid:
201 break
202 else:
203 time.sleep(1)
mblighdcd57a82007-07-11 23:06:47 +0000204 if pid:
205 break
mbligh0dd2ae02007-08-01 17:31:10 +0000206 else:
207 exit_status_indication = os.waitpid(sp.pid, 0)[1]
208
209 result.duration = time.time() - start_time
210 result.aborted = exit_status_indication & 127
211 if result.aborted:
212 result.exit_status= None
213 else:
214 result.exit_status= exit_status_indication / 256
215 result.stdout += sp.stdout.read()
216 result.stderr = sp.stderr.read()
217
218 finally:
219 # close our ends of the pipes to the sp no matter what
220 sp.stdout.close()
221 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000222
223 if not ignore_status and result.exit_status > 0:
mblighdcd57a82007-07-11 23:06:47 +0000224 raise errors.AutoservRunError("command execution error",
225 result)
mbligh0dd2ae02007-08-01 17:31:10 +0000226
mblighdcd57a82007-07-11 23:06:47 +0000227 return result
228
229
mbligh5f876ad2007-10-12 23:59:53 +0000230def system(command, timeout=None, ignore_status=False):
231 return run(command, timeout, ignore_status).exit_status
232
233
234def system_output(command, timeout=None, ignore_status=False):
235 return run(command, timeout, ignore_status).stdout
236
237
mblighdcd57a82007-07-11 23:06:47 +0000238def get_tmp_dir():
239 """Return the pathname of a directory on the host suitable
240 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000241
mblighdcd57a82007-07-11 23:06:47 +0000242 The directory and its content will be deleted automatically
243 at the end of the program execution if they are still present.
244 """
245 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000246
mblighdcd57a82007-07-11 23:06:47 +0000247 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000248 pid = os.getpid()
249 if not pid in __tmp_dirs:
250 __tmp_dirs[pid] = []
251 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000252 return dir_name
253
254
255@atexit.register
256def __clean_tmp_dirs():
257 """Erase temporary directories that were created by the get_tmp_dir()
258 function and that are still present.
259 """
260 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000261
mblighbea56822007-08-31 08:53:40 +0000262 pid = os.getpid()
263 if pid not in __tmp_dirs:
264 return
265 for dir in __tmp_dirs[pid]:
266 try:
267 shutil.rmtree(dir)
268 except OSError, e:
269 if e.errno == 2:
270 pass
271 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000272
273
274def unarchive(host, source_material):
275 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000276
mblighc8949b82007-07-23 16:33:58 +0000277 If the "source_material" is compresses (according to the file
278 extension) it will be uncompressed. Supported compression formats
279 are gzip and bzip2. Afterwards, if the source_material is a tar
280 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000281
mblighc8949b82007-07-23 16:33:58 +0000282 Args:
283 host: the host object on which the archive is located
284 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000285
mblighc8949b82007-07-23 16:33:58 +0000286 Returns:
287 The file or directory name of the unarchived source material.
288 If the material is a tar archive, it will be extracted in the
289 directory where it is and the path returned will be the first
290 entry in the archive, assuming it is the topmost directory.
291 If the material is not an archive, nothing will be done so this
292 function is "harmless" when it is "useless".
293 """
294 # uncompress
295 if (source_material.endswith(".gz") or
296 source_material.endswith(".gzip")):
297 host.run('gunzip "%s"' % (sh_escape(source_material)))
298 source_material= ".".join(source_material.split(".")[:-1])
299 elif source_material.endswith("bz2"):
300 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
301 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000302
mblighc8949b82007-07-23 16:33:58 +0000303 # untar
304 if source_material.endswith(".tar"):
305 retval= host.run('tar -C "%s" -xvf "%s"' % (
306 sh_escape(os.path.dirname(source_material)),
307 sh_escape(source_material),))
308 source_material= os.path.join(os.path.dirname(source_material),
309 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000310
mblighc8949b82007-07-23 16:33:58 +0000311 return source_material
mblighf1c52842007-10-16 15:21:38 +0000312
313
314def write_keyval(dirname, dictionary):
315 keyval = open(os.path.join(dirname, 'keyval'), 'w')
316 for key in dictionary.keys():
317 value = '%s' % dictionary[key] # convert numbers to strings
318 if re.search(r'\W', key):
319 raise 'Invalid key: ' + key
320 keyval.write('%s=%s\n' % (key, str(value)))
321 keyval.close()
322
mbligh05269362007-10-16 16:58:11 +0000323
324def update_version(srcdir, preserve_srcdir, new_version, install, *args, **dargs):
325 """
326 Make sure srcdir is version new_version
327
328 If not, delete it and install() the new version.
329
330 In the preserve_srcdir case, we just check it's up to date,
331 and if not, we rerun install, without removing srcdir
332 """
333 versionfile = srcdir + '/.version'
334 install_needed = True
335
336 if os.path.exists(srcdir):
337 if os.path.exists(versionfile):
338 old_version = pickle.load(open(versionfile, 'r'))
339 if (old_version == new_version):
340 install_needed = False
341
342 if install_needed:
343 if not preserve_srcdir:
344 system('rm -rf ' + srcdir)
345 install(*args, **dargs)
346 if os.path.exists(srcdir):
347 pickle.dump(new_version, open(versionfile, 'w'))
mbligh9708f732007-10-18 03:18:54 +0000348
349
350def get_server_dir():
351 path = os.path.dirname(sys.modules['utils'].__file__)
352 return os.path.abspath(path)