blob: 9ea339db5802b33615b6dd0c17ed852ae31bb9df [file] [log] [blame]
mblighdcd57a82007-07-11 23:06:47 +00001#!/usr/bin/python
2#
mbligh63073c92008-03-31 16:49:32 +00003# Copyright 2008 Google Inc. Released under the GPL v2
mblighdcd57a82007-07-11 23:06:47 +00004
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
mbligh63073c92008-03-31 16:49:32 +000015import atexit, os, re, shutil, textwrap, sys, tempfile, types, urllib
mblighccb9e182008-04-17 15:42:10 +000016
mblighc25b58f2008-05-29 21:05:25 +000017from autotest_lib.client.common_lib import utils
mblighea397bb2008-02-02 19:17:51 +000018
19
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
mblighc25b58f2008-05-29 21:05:25 +000024############# we need pass throughs for the methods in client/common_lib/utils
25def run(command, timeout=None, ignore_status=False,
26 stdout_tee=None, stderr_tee=None):
27 return utils.run(command, timeout, ignore_status,
28 stdout_tee, stderr_tee)
29
30
31def run_bg(command):
32 return utils.run_bg(command)
33
34
35def join_bg_job(bg_job, timeout=None, ignore_status=False,
36 stdout_tee=None, stderr_tee=None):
37 return utils.join_bg_job(bg_job, timeout, ignore_status,
38 stdout_tee, stderr_tee)
39
40
41def nuke_subprocess(subproc):
42 return utils.nuke_subprocess(subproc)
43
44
45def nuke_pid(pid):
46 return utils.nuke_pid(pid)
47
48
49def system(command, timeout=None, ignore_status=False):
50 return utils.system(command, timeout, ignore_status)
51
52
53def system_output(command, timeout=None, ignore_status=False,
54 retain_output=False):
55 return utils.system_output(command, timeout, ignore_status,
56 retain_output)
57
58
59def read_keyval(path):
60 return utils.read_keyval(path)
61
62
63def write_keyval(path, dictionary):
64 return utils.write_keyval(path, dictionary)
65
66
67####################################################################
68
mblighdcd57a82007-07-11 23:06:47 +000069def sh_escape(command):
mblighdc735a22007-08-02 16:54:37 +000070 """
71 Escape special characters from a command so that it can be passed
mblighc8949b82007-07-23 16:33:58 +000072 as a double quoted (" ") string in a (ba)sh command.
mblighdc735a22007-08-02 16:54:37 +000073
mblighdcd57a82007-07-11 23:06:47 +000074 Args:
75 command: the command string to escape.
mblighdc735a22007-08-02 16:54:37 +000076
mblighdcd57a82007-07-11 23:06:47 +000077 Returns:
78 The escaped command string. The required englobing double
79 quotes are NOT added and so should be added at some point by
80 the caller.
mblighdc735a22007-08-02 16:54:37 +000081
mblighdcd57a82007-07-11 23:06:47 +000082 See also: http://www.tldp.org/LDP/abs/html/escapingsection.html
83 """
mblighc25b58f2008-05-29 21:05:25 +000084 command = command.replace("\\", "\\\\")
85 command = command.replace("$", r'\$')
86 command = command.replace('"', r'\"')
87 command = command.replace('`', r'\`')
mblighdcd57a82007-07-11 23:06:47 +000088 return command
89
90
91def scp_remote_escape(filename):
mblighdc735a22007-08-02 16:54:37 +000092 """
93 Escape special characters from a filename so that it can be passed
mblighdcd57a82007-07-11 23:06:47 +000094 to scp (within double quotes) as a remote file.
mblighdc735a22007-08-02 16:54:37 +000095
mblighdcd57a82007-07-11 23:06:47 +000096 Bis-quoting has to be used with scp for remote files, "bis-quoting"
97 as in quoting x 2
98 scp does not support a newline in the filename
mblighdc735a22007-08-02 16:54:37 +000099
mblighdcd57a82007-07-11 23:06:47 +0000100 Args:
101 filename: the filename string to escape.
mblighdc735a22007-08-02 16:54:37 +0000102
mblighdcd57a82007-07-11 23:06:47 +0000103 Returns:
104 The escaped filename string. The required englobing double
105 quotes are NOT added and so should be added at some point by
106 the caller.
107 """
108 escape_chars= r' !"$&' "'" r'()*,:;<=>?[\]^`{|}'
mblighdc735a22007-08-02 16:54:37 +0000109
mblighdcd57a82007-07-11 23:06:47 +0000110 new_name= []
111 for char in filename:
112 if char in escape_chars:
113 new_name.append("\\%s" % (char,))
114 else:
115 new_name.append(char)
mblighdc735a22007-08-02 16:54:37 +0000116
mblighdcd57a82007-07-11 23:06:47 +0000117 return sh_escape("".join(new_name))
118
119
mbligh6e18dab2007-10-24 21:27:18 +0000120def get(location, local_copy = False):
mblighdcd57a82007-07-11 23:06:47 +0000121 """Get a file or directory to a local temporary directory.
mblighdc735a22007-08-02 16:54:37 +0000122
mblighdcd57a82007-07-11 23:06:47 +0000123 Args:
124 location: the source of the material to get. This source may
125 be one of:
126 * a local file or directory
127 * a URL (http or ftp)
128 * a python file-like object
mblighdc735a22007-08-02 16:54:37 +0000129
mblighdcd57a82007-07-11 23:06:47 +0000130 Returns:
131 The location of the file or directory where the requested
132 content was saved. This will be contained in a temporary
mblighc8949b82007-07-23 16:33:58 +0000133 directory on the local host. If the material to get was a
134 directory, the location will contain a trailing '/'
mblighdcd57a82007-07-11 23:06:47 +0000135 """
136 tmpdir = get_tmp_dir()
mblighdc735a22007-08-02 16:54:37 +0000137
mblighdcd57a82007-07-11 23:06:47 +0000138 # location is a file-like object
139 if hasattr(location, "read"):
140 tmpfile = os.path.join(tmpdir, "file")
141 tmpfileobj = file(tmpfile, 'w')
142 shutil.copyfileobj(location, tmpfileobj)
143 tmpfileobj.close()
144 return tmpfile
mblighdc735a22007-08-02 16:54:37 +0000145
mblighdcd57a82007-07-11 23:06:47 +0000146 if isinstance(location, types.StringTypes):
147 # location is a URL
148 if location.startswith('http') or location.startswith('ftp'):
149 tmpfile = os.path.join(tmpdir, os.path.basename(location))
150 urllib.urlretrieve(location, tmpfile)
151 return tmpfile
152 # location is a local path
153 elif os.path.exists(os.path.abspath(location)):
mbligh6e18dab2007-10-24 21:27:18 +0000154 if not local_copy:
mbligh59f70aa2007-10-25 14:44:38 +0000155 if os.path.isdir(location):
156 return location.rstrip('/') + '/'
157 else:
158 return location
mblighdcd57a82007-07-11 23:06:47 +0000159 tmpfile = os.path.join(tmpdir, os.path.basename(location))
160 if os.path.isdir(location):
161 tmpfile += '/'
162 shutil.copytree(location, tmpfile, symlinks=True)
163 return tmpfile
164 shutil.copyfile(location, tmpfile)
165 return tmpfile
166 # location is just a string, dump it to a file
167 else:
168 tmpfd, tmpfile = tempfile.mkstemp(dir=tmpdir)
169 tmpfileobj = os.fdopen(tmpfd, 'w')
170 tmpfileobj.write(location)
171 tmpfileobj.close()
172 return tmpfile
173
mbligh5f876ad2007-10-12 23:59:53 +0000174
mblighdcd57a82007-07-11 23:06:47 +0000175def get_tmp_dir():
176 """Return the pathname of a directory on the host suitable
177 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000178
mblighdcd57a82007-07-11 23:06:47 +0000179 The directory and its content will be deleted automatically
180 at the end of the program execution if they are still present.
181 """
182 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000183
mblighdcd57a82007-07-11 23:06:47 +0000184 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000185 pid = os.getpid()
186 if not pid in __tmp_dirs:
187 __tmp_dirs[pid] = []
188 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000189 return dir_name
190
191
192@atexit.register
193def __clean_tmp_dirs():
194 """Erase temporary directories that were created by the get_tmp_dir()
195 function and that are still present.
196 """
197 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000198
mblighbea56822007-08-31 08:53:40 +0000199 pid = os.getpid()
200 if pid not in __tmp_dirs:
201 return
202 for dir in __tmp_dirs[pid]:
203 try:
204 shutil.rmtree(dir)
205 except OSError, e:
206 if e.errno == 2:
207 pass
208 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000209
210
211def unarchive(host, source_material):
212 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000213
mblighc8949b82007-07-23 16:33:58 +0000214 If the "source_material" is compresses (according to the file
215 extension) it will be uncompressed. Supported compression formats
216 are gzip and bzip2. Afterwards, if the source_material is a tar
217 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000218
mblighc8949b82007-07-23 16:33:58 +0000219 Args:
220 host: the host object on which the archive is located
221 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000222
mblighc8949b82007-07-23 16:33:58 +0000223 Returns:
224 The file or directory name of the unarchived source material.
225 If the material is a tar archive, it will be extracted in the
226 directory where it is and the path returned will be the first
227 entry in the archive, assuming it is the topmost directory.
228 If the material is not an archive, nothing will be done so this
229 function is "harmless" when it is "useless".
230 """
231 # uncompress
232 if (source_material.endswith(".gz") or
233 source_material.endswith(".gzip")):
234 host.run('gunzip "%s"' % (sh_escape(source_material)))
235 source_material= ".".join(source_material.split(".")[:-1])
236 elif source_material.endswith("bz2"):
237 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
238 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000239
mblighc8949b82007-07-23 16:33:58 +0000240 # untar
241 if source_material.endswith(".tar"):
242 retval= host.run('tar -C "%s" -xvf "%s"' % (
243 sh_escape(os.path.dirname(source_material)),
244 sh_escape(source_material),))
245 source_material= os.path.join(os.path.dirname(source_material),
246 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000247
mblighc8949b82007-07-23 16:33:58 +0000248 return source_material
mblighf1c52842007-10-16 15:21:38 +0000249
250
mbligh9708f732007-10-18 03:18:54 +0000251def get_server_dir():
mbligh8fc3b912008-05-06 20:43:02 +0000252 path = os.path.dirname(sys.modules['autotest_lib.server.utils'].__file__)
mbligh9708f732007-10-18 03:18:54 +0000253 return os.path.abspath(path)
mbligh40f122a2007-11-03 23:08:46 +0000254
255
mbligh34a3fd72007-12-10 17:16:22 +0000256def find_pid(command):
mblighc25b58f2008-05-29 21:05:25 +0000257 for line in utils.system_output('ps -eo pid,cmd').rstrip().split('\n'):
mbligh34a3fd72007-12-10 17:16:22 +0000258 (pid, cmd) = line.split(None, 1)
259 if re.search(command, cmd):
260 return int(pid)
261 return None
262
263
264def nohup(command, stdout='/dev/null', stderr='/dev/null', background=True,
265 env = {}):
266 cmd = ' '.join(key+'='+val for key, val in env.iteritems())
267 cmd += ' nohup ' + command
268 cmd += ' > %s' % stdout
269 if stdout == stderr:
270 cmd += ' 2>&1'
271 else:
272 cmd += ' 2> %s' % stderr
273 if background:
274 cmd += ' &'
mblighc25b58f2008-05-29 21:05:25 +0000275 utils.system(cmd)
mbligh34a3fd72007-12-10 17:16:22 +0000276
277
mbligh0b4fe6e2008-05-06 20:41:37 +0000278def default_mappings(machines):
279 """
280 Returns a simple mapping in which all machines are assigned to the
281 same key. Provides the default behavior for
282 form_ntuples_from_machines. """
283 mappings = {}
284 failures = []
285
286 mach = machines[0]
287 mappings['ident'] = [mach]
288 if len(machines) > 1:
289 machines = machines[1:]
290 for machine in machines:
291 mappings['ident'].append(machine)
292
293 return (mappings, failures)
294
295
296def form_ntuples_from_machines(machines, n=2, mapping_func=default_mappings):
297 """Returns a set of ntuples from machines where the machines in an
298 ntuple are in the same mapping, and a set of failures which are
299 (machine name, reason) tuples."""
300 ntuples = []
301 (mappings, failures) = mapping_func(machines)
302
303 # now run through the mappings and create n-tuples.
304 # throw out the odd guys out
305 for key in mappings:
306 key_machines = mappings[key]
307 total_machines = len(key_machines)
308
309 # form n-tuples
310 while len(key_machines) >= n:
311 ntuples.append(key_machines[0:n])
312 key_machines = key_machines[n:]
313
314 for mach in key_machines:
315 failures.append((mach, "machine can not be tupled"))
316
317 return (ntuples, failures)
318
319
mbligh40f122a2007-11-03 23:08:46 +0000320class AutoservOptionParser:
321 """Custom command-line options parser for autoserv.
322
323 We can't use the general getopt methods here, as there will be unknown
324 extra arguments that we pass down into the control file instead.
325 Thus we process the arguments by hand, for which we are duly repentant.
326 Making a single function here just makes it harder to read. Suck it up.
327 """
328
329 def __init__(self, args):
330 self.args = args
331
332
333 def parse_opts(self, flag):
334 if self.args.count(flag):
335 idx = self.args.index(flag)
336 self.args[idx : idx+1] = []
337 return True
338 else:
339 return False
340
341
342 def parse_opts_param(self, flag, default = None, split = False):
343 if self.args.count(flag):
344 idx = self.args.index(flag)
345 ret = self.args[idx+1]
346 self.args[idx : idx+2] = []
347 if split:
348 return ret.split(split)
349 else:
350 return ret
351 else:
352 return default