blob: 59232f47a8f53bea92208d3affe44081c177b2c4 [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
17from autotest_lib.client.common_lib.utils import *
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
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
mbligh5f876ad2007-10-12 23:59:53 +0000129
mblighdcd57a82007-07-11 23:06:47 +0000130def get_tmp_dir():
131 """Return the pathname of a directory on the host suitable
132 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000133
mblighdcd57a82007-07-11 23:06:47 +0000134 The directory and its content will be deleted automatically
135 at the end of the program execution if they are still present.
136 """
137 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000138
mblighdcd57a82007-07-11 23:06:47 +0000139 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000140 pid = os.getpid()
141 if not pid in __tmp_dirs:
142 __tmp_dirs[pid] = []
143 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000144 return dir_name
145
146
147@atexit.register
148def __clean_tmp_dirs():
149 """Erase temporary directories that were created by the get_tmp_dir()
150 function and that are still present.
151 """
152 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000153
mblighbea56822007-08-31 08:53:40 +0000154 pid = os.getpid()
155 if pid not in __tmp_dirs:
156 return
157 for dir in __tmp_dirs[pid]:
158 try:
159 shutil.rmtree(dir)
160 except OSError, e:
161 if e.errno == 2:
162 pass
163 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000164
165
166def unarchive(host, source_material):
167 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000168
mblighc8949b82007-07-23 16:33:58 +0000169 If the "source_material" is compresses (according to the file
170 extension) it will be uncompressed. Supported compression formats
171 are gzip and bzip2. Afterwards, if the source_material is a tar
172 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000173
mblighc8949b82007-07-23 16:33:58 +0000174 Args:
175 host: the host object on which the archive is located
176 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000177
mblighc8949b82007-07-23 16:33:58 +0000178 Returns:
179 The file or directory name of the unarchived source material.
180 If the material is a tar archive, it will be extracted in the
181 directory where it is and the path returned will be the first
182 entry in the archive, assuming it is the topmost directory.
183 If the material is not an archive, nothing will be done so this
184 function is "harmless" when it is "useless".
185 """
186 # uncompress
187 if (source_material.endswith(".gz") or
188 source_material.endswith(".gzip")):
189 host.run('gunzip "%s"' % (sh_escape(source_material)))
190 source_material= ".".join(source_material.split(".")[:-1])
191 elif source_material.endswith("bz2"):
192 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
193 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000194
mblighc8949b82007-07-23 16:33:58 +0000195 # untar
196 if source_material.endswith(".tar"):
197 retval= host.run('tar -C "%s" -xvf "%s"' % (
198 sh_escape(os.path.dirname(source_material)),
199 sh_escape(source_material),))
200 source_material= os.path.join(os.path.dirname(source_material),
201 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000202
mblighc8949b82007-07-23 16:33:58 +0000203 return source_material
mblighf1c52842007-10-16 15:21:38 +0000204
205
mbligh9708f732007-10-18 03:18:54 +0000206def get_server_dir():
207 path = os.path.dirname(sys.modules['utils'].__file__)
208 return os.path.abspath(path)
mbligh40f122a2007-11-03 23:08:46 +0000209
210
mbligh34a3fd72007-12-10 17:16:22 +0000211def find_pid(command):
212 for line in system_output('ps -eo pid,cmd').rstrip().split('\n'):
213 (pid, cmd) = line.split(None, 1)
214 if re.search(command, cmd):
215 return int(pid)
216 return None
217
218
219def nohup(command, stdout='/dev/null', stderr='/dev/null', background=True,
220 env = {}):
221 cmd = ' '.join(key+'='+val for key, val in env.iteritems())
222 cmd += ' nohup ' + command
223 cmd += ' > %s' % stdout
224 if stdout == stderr:
225 cmd += ' 2>&1'
226 else:
227 cmd += ' 2> %s' % stderr
228 if background:
229 cmd += ' &'
230 system(cmd)
231
232
mbligh0b4fe6e2008-05-06 20:41:37 +0000233def default_mappings(machines):
234 """
235 Returns a simple mapping in which all machines are assigned to the
236 same key. Provides the default behavior for
237 form_ntuples_from_machines. """
238 mappings = {}
239 failures = []
240
241 mach = machines[0]
242 mappings['ident'] = [mach]
243 if len(machines) > 1:
244 machines = machines[1:]
245 for machine in machines:
246 mappings['ident'].append(machine)
247
248 return (mappings, failures)
249
250
251def form_ntuples_from_machines(machines, n=2, mapping_func=default_mappings):
252 """Returns a set of ntuples from machines where the machines in an
253 ntuple are in the same mapping, and a set of failures which are
254 (machine name, reason) tuples."""
255 ntuples = []
256 (mappings, failures) = mapping_func(machines)
257
258 # now run through the mappings and create n-tuples.
259 # throw out the odd guys out
260 for key in mappings:
261 key_machines = mappings[key]
262 total_machines = len(key_machines)
263
264 # form n-tuples
265 while len(key_machines) >= n:
266 ntuples.append(key_machines[0:n])
267 key_machines = key_machines[n:]
268
269 for mach in key_machines:
270 failures.append((mach, "machine can not be tupled"))
271
272 return (ntuples, failures)
273
274
mbligh40f122a2007-11-03 23:08:46 +0000275class AutoservOptionParser:
276 """Custom command-line options parser for autoserv.
277
278 We can't use the general getopt methods here, as there will be unknown
279 extra arguments that we pass down into the control file instead.
280 Thus we process the arguments by hand, for which we are duly repentant.
281 Making a single function here just makes it harder to read. Suck it up.
282 """
283
284 def __init__(self, args):
285 self.args = args
286
287
288 def parse_opts(self, flag):
289 if self.args.count(flag):
290 idx = self.args.index(flag)
291 self.args[idx : idx+1] = []
292 return True
293 else:
294 return False
295
296
297 def parse_opts_param(self, flag, default = None, split = False):
298 if self.args.count(flag):
299 idx = self.args.index(flag)
300 ret = self.args[idx+1]
301 self.args[idx : idx+2] = []
302 if split:
303 return ret.split(split)
304 else:
305 return ret
306 else:
307 return default