blob: 7738f52e0f9badae9064f99c171589786273a6d4 [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
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:
110 return location
mblighdcd57a82007-07-11 23:06:47 +0000111 tmpfile = os.path.join(tmpdir, os.path.basename(location))
112 if os.path.isdir(location):
113 tmpfile += '/'
114 shutil.copytree(location, tmpfile, symlinks=True)
115 return tmpfile
116 shutil.copyfile(location, tmpfile)
117 return tmpfile
118 # location is just a string, dump it to a file
119 else:
120 tmpfd, tmpfile = tempfile.mkstemp(dir=tmpdir)
121 tmpfileobj = os.fdopen(tmpfd, 'w')
122 tmpfileobj.write(location)
123 tmpfileobj.close()
124 return tmpfile
125
126
mblighcf965b02007-07-25 16:49:45 +0000127def run(command, timeout=None, ignore_status=False):
mblighdc735a22007-08-02 16:54:37 +0000128 """
129 Run a command on the host.
130
mblighdcd57a82007-07-11 23:06:47 +0000131 Args:
132 command: the command line string
133 timeout: time limit in seconds before attempting to
134 kill the running process. The run() function
135 will take a few seconds longer than 'timeout'
136 to complete if it has to kill the process.
mbligh8b85dfb2007-08-28 09:50:31 +0000137 ignore_status: do not raise an exception, no matter what
138 the exit code of the command is.
139
mblighdcd57a82007-07-11 23:06:47 +0000140 Returns:
141 a hosts.CmdResult object
mblighdc735a22007-08-02 16:54:37 +0000142
mblighdcd57a82007-07-11 23:06:47 +0000143 Raises:
144 AutoservRunError: the exit code of the command
145 execution was not 0
mblighdc735a22007-08-02 16:54:37 +0000146
mblighdcd57a82007-07-11 23:06:47 +0000147 TODO(poirier): Add a "tee" option to send the command's
148 stdout and stderr to python's stdout and stderr? At
149 the moment, there is no way to see the command's
150 output as it is running.
151 TODO(poirier): Should a timeout raise an exception? Should
152 exceptions be raised at all?
153 """
154 result= hosts.CmdResult()
155 result.command= command
156 sp= subprocess.Popen(command, stdout=subprocess.PIPE,
157 stderr=subprocess.PIPE, close_fds=True, shell=True,
158 executable="/bin/bash")
mbligh0dd2ae02007-08-01 17:31:10 +0000159
160 try:
161 # We are holding ends to stdin, stdout pipes
162 # hence we need to be sure to close those fds no mater what
163 start_time= time.time()
164 if timeout:
165 stop_time= start_time + timeout
mblighdcd57a82007-07-11 23:06:47 +0000166 time_left= stop_time - time.time()
mbligh0dd2ae02007-08-01 17:31:10 +0000167 while time_left > 0:
168 # select will return when stdout is ready
169 # (including when it is EOF, that is the
170 # process has terminated).
171 (retval, tmp, tmp) = select.select(
172 [sp.stdout], [], [], time_left)
173 if len(retval):
174 # os.read() has to be used instead of
175 # sp.stdout.read() which will
176 # otherwise block
177 result.stdout += os.read(
178 sp.stdout.fileno(), 1024)
179
180 (pid, exit_status_indication) = os.waitpid(
181 sp.pid, os.WNOHANG)
182 if pid:
183 stop_time= time.time()
184 time_left= stop_time - time.time()
185
186 # the process has not terminated within timeout,
187 # kill it via an escalating series of signals.
188 if not pid:
189 signal_queue = [signal.SIGTERM, signal.SIGKILL]
190 for sig in signal_queue:
191 try:
192 os.kill(sp.pid, sig)
193 # handle race condition in which
194 # process died before we could kill it.
195 except OSError:
196 pass
197
198 for i in range(5):
199 (pid, exit_status_indication
200 ) = os.waitpid(sp.pid,
201 os.WNOHANG)
202 if pid:
203 break
204 else:
205 time.sleep(1)
mblighdcd57a82007-07-11 23:06:47 +0000206 if pid:
207 break
mbligh0dd2ae02007-08-01 17:31:10 +0000208 else:
209 exit_status_indication = os.waitpid(sp.pid, 0)[1]
210
211 result.duration = time.time() - start_time
212 result.aborted = exit_status_indication & 127
213 if result.aborted:
214 result.exit_status= None
215 else:
216 result.exit_status= exit_status_indication / 256
217 result.stdout += sp.stdout.read()
218 result.stderr = sp.stderr.read()
219
220 finally:
221 # close our ends of the pipes to the sp no matter what
222 sp.stdout.close()
223 sp.stderr.close()
mblighcf965b02007-07-25 16:49:45 +0000224
225 if not ignore_status and result.exit_status > 0:
mblighdcd57a82007-07-11 23:06:47 +0000226 raise errors.AutoservRunError("command execution error",
227 result)
mbligh0dd2ae02007-08-01 17:31:10 +0000228
mblighdcd57a82007-07-11 23:06:47 +0000229 return result
230
231
mbligh5f876ad2007-10-12 23:59:53 +0000232def system(command, timeout=None, ignore_status=False):
233 return run(command, timeout, ignore_status).exit_status
234
235
236def system_output(command, timeout=None, ignore_status=False):
237 return run(command, timeout, ignore_status).stdout
238
239
mblighdcd57a82007-07-11 23:06:47 +0000240def get_tmp_dir():
241 """Return the pathname of a directory on the host suitable
242 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000243
mblighdcd57a82007-07-11 23:06:47 +0000244 The directory and its content will be deleted automatically
245 at the end of the program execution if they are still present.
246 """
247 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000248
mblighdcd57a82007-07-11 23:06:47 +0000249 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000250 pid = os.getpid()
251 if not pid in __tmp_dirs:
252 __tmp_dirs[pid] = []
253 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000254 return dir_name
255
256
257@atexit.register
258def __clean_tmp_dirs():
259 """Erase temporary directories that were created by the get_tmp_dir()
260 function and that are still present.
261 """
262 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000263
mblighbea56822007-08-31 08:53:40 +0000264 pid = os.getpid()
265 if pid not in __tmp_dirs:
266 return
267 for dir in __tmp_dirs[pid]:
268 try:
269 shutil.rmtree(dir)
270 except OSError, e:
271 if e.errno == 2:
272 pass
273 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000274
275
276def unarchive(host, source_material):
277 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000278
mblighc8949b82007-07-23 16:33:58 +0000279 If the "source_material" is compresses (according to the file
280 extension) it will be uncompressed. Supported compression formats
281 are gzip and bzip2. Afterwards, if the source_material is a tar
282 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000283
mblighc8949b82007-07-23 16:33:58 +0000284 Args:
285 host: the host object on which the archive is located
286 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000287
mblighc8949b82007-07-23 16:33:58 +0000288 Returns:
289 The file or directory name of the unarchived source material.
290 If the material is a tar archive, it will be extracted in the
291 directory where it is and the path returned will be the first
292 entry in the archive, assuming it is the topmost directory.
293 If the material is not an archive, nothing will be done so this
294 function is "harmless" when it is "useless".
295 """
296 # uncompress
297 if (source_material.endswith(".gz") or
298 source_material.endswith(".gzip")):
299 host.run('gunzip "%s"' % (sh_escape(source_material)))
300 source_material= ".".join(source_material.split(".")[:-1])
301 elif source_material.endswith("bz2"):
302 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
303 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000304
mblighc8949b82007-07-23 16:33:58 +0000305 # untar
306 if source_material.endswith(".tar"):
307 retval= host.run('tar -C "%s" -xvf "%s"' % (
308 sh_escape(os.path.dirname(source_material)),
309 sh_escape(source_material),))
310 source_material= os.path.join(os.path.dirname(source_material),
311 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000312
mblighc8949b82007-07-23 16:33:58 +0000313 return source_material
mblighf1c52842007-10-16 15:21:38 +0000314
315
316def write_keyval(dirname, dictionary):
317 keyval = open(os.path.join(dirname, 'keyval'), 'w')
318 for key in dictionary.keys():
319 value = '%s' % dictionary[key] # convert numbers to strings
320 if re.search(r'\W', key):
321 raise 'Invalid key: ' + key
322 keyval.write('%s=%s\n' % (key, str(value)))
323 keyval.close()
324
mbligh05269362007-10-16 16:58:11 +0000325
326def update_version(srcdir, preserve_srcdir, new_version, install, *args, **dargs):
327 """
328 Make sure srcdir is version new_version
329
330 If not, delete it and install() the new version.
331
332 In the preserve_srcdir case, we just check it's up to date,
333 and if not, we rerun install, without removing srcdir
334 """
335 versionfile = srcdir + '/.version'
336 install_needed = True
337
338 if os.path.exists(srcdir):
339 if os.path.exists(versionfile):
340 old_version = pickle.load(open(versionfile, 'r'))
341 if (old_version == new_version):
342 install_needed = False
343
344 if install_needed:
345 if not preserve_srcdir:
346 system('rm -rf ' + srcdir)
347 install(*args, **dargs)
348 if os.path.exists(srcdir):
349 pickle.dump(new_version, open(versionfile, 'w'))
mbligh9708f732007-10-18 03:18:54 +0000350
351
352def get_server_dir():
353 path = os.path.dirname(sys.modules['utils'].__file__)
354 return os.path.abspath(path)