blob: 56a102ee8a39762a091fe2662e2b628b8a978eeb [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
mbligh02ff2d52008-06-03 15:00:21 +000015import atexit, os, re, shutil, textwrap, sys, tempfile, types
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
mblighc25b58f2008-05-29 21:05:25 +000031def system(command, timeout=None, ignore_status=False):
32 return utils.system(command, timeout, ignore_status)
33
34
35def system_output(command, timeout=None, ignore_status=False,
36 retain_output=False):
37 return utils.system_output(command, timeout, ignore_status,
38 retain_output)
39
40
mbligh02ff2d52008-06-03 15:00:21 +000041def urlopen(url, data=None, proxies=None, timeout=300):
42 return utils.urlopen(url, data=data, proxies=proxies, timeout=timeout)
43
44
45def urlretrieve(url, filename=None, reporthook=None, data=None, timeout=300):
46 return utils.urlretrieve(url, filename=filename, reporthook=reporthook,
47 data=data, timeout=timeout)
48
49
mblighc25b58f2008-05-29 21:05:25 +000050def read_keyval(path):
51 return utils.read_keyval(path)
52
53
54def write_keyval(path, dictionary):
55 return utils.write_keyval(path, dictionary)
56
57
58####################################################################
59
mblighdcd57a82007-07-11 23:06:47 +000060def sh_escape(command):
mblighdc735a22007-08-02 16:54:37 +000061 """
62 Escape special characters from a command so that it can be passed
mblighc8949b82007-07-23 16:33:58 +000063 as a double quoted (" ") string in a (ba)sh command.
mblighdc735a22007-08-02 16:54:37 +000064
mblighdcd57a82007-07-11 23:06:47 +000065 Args:
66 command: the command string to escape.
mblighdc735a22007-08-02 16:54:37 +000067
mblighdcd57a82007-07-11 23:06:47 +000068 Returns:
69 The escaped command string. The required englobing double
70 quotes are NOT added and so should be added at some point by
71 the caller.
mblighdc735a22007-08-02 16:54:37 +000072
mblighdcd57a82007-07-11 23:06:47 +000073 See also: http://www.tldp.org/LDP/abs/html/escapingsection.html
74 """
mblighc25b58f2008-05-29 21:05:25 +000075 command = command.replace("\\", "\\\\")
76 command = command.replace("$", r'\$')
77 command = command.replace('"', r'\"')
78 command = command.replace('`', r'\`')
mblighdcd57a82007-07-11 23:06:47 +000079 return command
80
81
82def scp_remote_escape(filename):
mblighdc735a22007-08-02 16:54:37 +000083 """
84 Escape special characters from a filename so that it can be passed
mblighdcd57a82007-07-11 23:06:47 +000085 to scp (within double quotes) as a remote file.
mblighdc735a22007-08-02 16:54:37 +000086
mblighdcd57a82007-07-11 23:06:47 +000087 Bis-quoting has to be used with scp for remote files, "bis-quoting"
88 as in quoting x 2
89 scp does not support a newline in the filename
mblighdc735a22007-08-02 16:54:37 +000090
mblighdcd57a82007-07-11 23:06:47 +000091 Args:
92 filename: the filename string to escape.
mblighdc735a22007-08-02 16:54:37 +000093
mblighdcd57a82007-07-11 23:06:47 +000094 Returns:
95 The escaped filename string. The required englobing double
96 quotes are NOT added and so should be added at some point by
97 the caller.
98 """
99 escape_chars= r' !"$&' "'" r'()*,:;<=>?[\]^`{|}'
mblighdc735a22007-08-02 16:54:37 +0000100
mblighdcd57a82007-07-11 23:06:47 +0000101 new_name= []
102 for char in filename:
103 if char in escape_chars:
104 new_name.append("\\%s" % (char,))
105 else:
106 new_name.append(char)
mblighdc735a22007-08-02 16:54:37 +0000107
mblighdcd57a82007-07-11 23:06:47 +0000108 return sh_escape("".join(new_name))
109
110
mbligh6e18dab2007-10-24 21:27:18 +0000111def get(location, local_copy = False):
mblighdcd57a82007-07-11 23:06:47 +0000112 """Get a file or directory to a local temporary directory.
mblighdc735a22007-08-02 16:54:37 +0000113
mblighdcd57a82007-07-11 23:06:47 +0000114 Args:
115 location: the source of the material to get. This source may
116 be one of:
117 * a local file or directory
118 * a URL (http or ftp)
119 * a python file-like object
mblighdc735a22007-08-02 16:54:37 +0000120
mblighdcd57a82007-07-11 23:06:47 +0000121 Returns:
122 The location of the file or directory where the requested
123 content was saved. This will be contained in a temporary
mblighc8949b82007-07-23 16:33:58 +0000124 directory on the local host. If the material to get was a
125 directory, the location will contain a trailing '/'
mblighdcd57a82007-07-11 23:06:47 +0000126 """
127 tmpdir = get_tmp_dir()
mblighdc735a22007-08-02 16:54:37 +0000128
mblighdcd57a82007-07-11 23:06:47 +0000129 # location is a file-like object
130 if hasattr(location, "read"):
131 tmpfile = os.path.join(tmpdir, "file")
132 tmpfileobj = file(tmpfile, 'w')
133 shutil.copyfileobj(location, tmpfileobj)
134 tmpfileobj.close()
135 return tmpfile
mblighdc735a22007-08-02 16:54:37 +0000136
mblighdcd57a82007-07-11 23:06:47 +0000137 if isinstance(location, types.StringTypes):
138 # location is a URL
139 if location.startswith('http') or location.startswith('ftp'):
140 tmpfile = os.path.join(tmpdir, os.path.basename(location))
mbligh02ff2d52008-06-03 15:00:21 +0000141 utils.urlretrieve(location, tmpfile)
mblighdcd57a82007-07-11 23:06:47 +0000142 return tmpfile
143 # location is a local path
144 elif os.path.exists(os.path.abspath(location)):
mbligh6e18dab2007-10-24 21:27:18 +0000145 if not local_copy:
mbligh59f70aa2007-10-25 14:44:38 +0000146 if os.path.isdir(location):
147 return location.rstrip('/') + '/'
148 else:
149 return location
mblighdcd57a82007-07-11 23:06:47 +0000150 tmpfile = os.path.join(tmpdir, os.path.basename(location))
151 if os.path.isdir(location):
152 tmpfile += '/'
153 shutil.copytree(location, tmpfile, symlinks=True)
154 return tmpfile
155 shutil.copyfile(location, tmpfile)
156 return tmpfile
157 # location is just a string, dump it to a file
158 else:
159 tmpfd, tmpfile = tempfile.mkstemp(dir=tmpdir)
160 tmpfileobj = os.fdopen(tmpfd, 'w')
161 tmpfileobj.write(location)
162 tmpfileobj.close()
163 return tmpfile
164
mbligh5f876ad2007-10-12 23:59:53 +0000165
mblighdcd57a82007-07-11 23:06:47 +0000166def get_tmp_dir():
167 """Return the pathname of a directory on the host suitable
168 for temporary file storage.
mblighdc735a22007-08-02 16:54:37 +0000169
mblighdcd57a82007-07-11 23:06:47 +0000170 The directory and its content will be deleted automatically
171 at the end of the program execution if they are still present.
172 """
173 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000174
mblighdcd57a82007-07-11 23:06:47 +0000175 dir_name= tempfile.mkdtemp(prefix="autoserv-")
mblighbea56822007-08-31 08:53:40 +0000176 pid = os.getpid()
177 if not pid in __tmp_dirs:
178 __tmp_dirs[pid] = []
179 __tmp_dirs[pid].append(dir_name)
mblighdcd57a82007-07-11 23:06:47 +0000180 return dir_name
181
182
183@atexit.register
184def __clean_tmp_dirs():
185 """Erase temporary directories that were created by the get_tmp_dir()
186 function and that are still present.
187 """
188 global __tmp_dirs
mblighdc735a22007-08-02 16:54:37 +0000189
mblighbea56822007-08-31 08:53:40 +0000190 pid = os.getpid()
191 if pid not in __tmp_dirs:
192 return
193 for dir in __tmp_dirs[pid]:
194 try:
195 shutil.rmtree(dir)
196 except OSError, e:
197 if e.errno == 2:
198 pass
199 __tmp_dirs[pid] = []
mblighc8949b82007-07-23 16:33:58 +0000200
201
202def unarchive(host, source_material):
203 """Uncompress and untar an archive on a host.
mblighdc735a22007-08-02 16:54:37 +0000204
mblighc8949b82007-07-23 16:33:58 +0000205 If the "source_material" is compresses (according to the file
206 extension) it will be uncompressed. Supported compression formats
207 are gzip and bzip2. Afterwards, if the source_material is a tar
208 archive, it will be untarred.
mblighdc735a22007-08-02 16:54:37 +0000209
mblighc8949b82007-07-23 16:33:58 +0000210 Args:
211 host: the host object on which the archive is located
212 source_material: the path of the archive on the host
mblighdc735a22007-08-02 16:54:37 +0000213
mblighc8949b82007-07-23 16:33:58 +0000214 Returns:
215 The file or directory name of the unarchived source material.
216 If the material is a tar archive, it will be extracted in the
217 directory where it is and the path returned will be the first
218 entry in the archive, assuming it is the topmost directory.
219 If the material is not an archive, nothing will be done so this
220 function is "harmless" when it is "useless".
221 """
222 # uncompress
223 if (source_material.endswith(".gz") or
224 source_material.endswith(".gzip")):
225 host.run('gunzip "%s"' % (sh_escape(source_material)))
226 source_material= ".".join(source_material.split(".")[:-1])
227 elif source_material.endswith("bz2"):
228 host.run('bunzip2 "%s"' % (sh_escape(source_material)))
229 source_material= ".".join(source_material.split(".")[:-1])
mblighdc735a22007-08-02 16:54:37 +0000230
mblighc8949b82007-07-23 16:33:58 +0000231 # untar
232 if source_material.endswith(".tar"):
233 retval= host.run('tar -C "%s" -xvf "%s"' % (
234 sh_escape(os.path.dirname(source_material)),
235 sh_escape(source_material),))
236 source_material= os.path.join(os.path.dirname(source_material),
237 retval.stdout.split()[0])
mblighdc735a22007-08-02 16:54:37 +0000238
mblighc8949b82007-07-23 16:33:58 +0000239 return source_material
mblighf1c52842007-10-16 15:21:38 +0000240
241
mbligh9708f732007-10-18 03:18:54 +0000242def get_server_dir():
mbligh8fc3b912008-05-06 20:43:02 +0000243 path = os.path.dirname(sys.modules['autotest_lib.server.utils'].__file__)
mbligh9708f732007-10-18 03:18:54 +0000244 return os.path.abspath(path)
mbligh40f122a2007-11-03 23:08:46 +0000245
246
mbligh34a3fd72007-12-10 17:16:22 +0000247def find_pid(command):
mblighc25b58f2008-05-29 21:05:25 +0000248 for line in utils.system_output('ps -eo pid,cmd').rstrip().split('\n'):
mbligh34a3fd72007-12-10 17:16:22 +0000249 (pid, cmd) = line.split(None, 1)
250 if re.search(command, cmd):
251 return int(pid)
252 return None
253
254
255def nohup(command, stdout='/dev/null', stderr='/dev/null', background=True,
256 env = {}):
257 cmd = ' '.join(key+'='+val for key, val in env.iteritems())
258 cmd += ' nohup ' + command
259 cmd += ' > %s' % stdout
260 if stdout == stderr:
261 cmd += ' 2>&1'
262 else:
263 cmd += ' 2> %s' % stderr
264 if background:
265 cmd += ' &'
mblighc25b58f2008-05-29 21:05:25 +0000266 utils.system(cmd)
mbligh34a3fd72007-12-10 17:16:22 +0000267
268
mbligh0b4fe6e2008-05-06 20:41:37 +0000269def default_mappings(machines):
270 """
271 Returns a simple mapping in which all machines are assigned to the
272 same key. Provides the default behavior for
273 form_ntuples_from_machines. """
274 mappings = {}
275 failures = []
276
277 mach = machines[0]
278 mappings['ident'] = [mach]
279 if len(machines) > 1:
280 machines = machines[1:]
281 for machine in machines:
282 mappings['ident'].append(machine)
283
284 return (mappings, failures)
285
286
287def form_ntuples_from_machines(machines, n=2, mapping_func=default_mappings):
288 """Returns a set of ntuples from machines where the machines in an
289 ntuple are in the same mapping, and a set of failures which are
290 (machine name, reason) tuples."""
291 ntuples = []
292 (mappings, failures) = mapping_func(machines)
293
294 # now run through the mappings and create n-tuples.
295 # throw out the odd guys out
296 for key in mappings:
297 key_machines = mappings[key]
298 total_machines = len(key_machines)
299
300 # form n-tuples
301 while len(key_machines) >= n:
302 ntuples.append(key_machines[0:n])
303 key_machines = key_machines[n:]
304
305 for mach in key_machines:
306 failures.append((mach, "machine can not be tupled"))
307
308 return (ntuples, failures)
mblighaa9e6742008-06-05 21:14:05 +0000309
jadmanskib0605d92008-06-06 16:12:34 +0000310
mblighaa9e6742008-06-05 21:14:05 +0000311def parse_machine(machine, user = 'root', port = 22, password = ''):
312 """
313 Parse the machine string user:pass@host:port and return it separately,
314 if the machine string is not complete, use the default parameters
315 when appropriate.
316 """
317
318 user = user
319 port = port
320 password = password
321
322 if re.search('@', machine):
323 machine = machine.split('@')
324
325 if re.search(':', machine[0]):
326 machine[0] = machine[0].split(':')
327 user = machine[0][0]
328 password = machine[0][1]
329
330 else:
331 user = machine[0]
332
333 if re.search(':', machine[1]):
334 machine[1] = machine[1].split(':')
335 hostname = machine[1][0]
336 port = int(machine[1][1])
337
338 else:
339 hostname = machine[1]
340
341 elif re.search(':', machine):
342 machine = machine.split(':')
343 hostname = machine[0]
344 port = int(machine[1])
345
346 else:
347 hostname = machine
348
349 return hostname, user, password, port
350
351
352def get_public_key():
353 """
354 Return a valid string ssh public key for the user executing autoserv or
355 autotest. If there's no DSA or RSA public key, create a DSA keypair with
356 ssh-keygen and return it.
357 """
358
359 ssh_conf_path = os.path.join(os.environ['HOME'], '.ssh')
360
361 dsa_public_key_path = os.path.join(ssh_conf_path, 'id_dsa.pub')
362 dsa_private_key_path = os.path.join(ssh_conf_path, 'id_dsa')
363
364 rsa_public_key_path = os.path.join(ssh_conf_path, 'id_rsa.pub')
365 rsa_private_key_path = os.path.join(ssh_conf_path, 'id_rsa')
366
367 has_dsa_keypair = os.path.isfile(dsa_public_key_path) and \
368 os.path.isfile(dsa_private_key_path)
369 has_rsa_keypair = os.path.isfile(rsa_public_key_path) and \
370 os.path.isfile(rsa_private_key_path)
371
372 if has_dsa_keypair:
373 print 'DSA keypair found, using it'
374 public_key_path = dsa_public_key_path
375
376 elif has_rsa_keypair:
377 print 'RSA keypair found, using it'
378 public_key_path = rsa_public_key_path
379
380 else:
381 print 'Neither RSA nor DSA keypair found, creating DSA ssh key pair'
382 system('ssh-keygen -t dsa -q -N "" -f %s' % dsa_private_key_path)
383 public_key_path = dsa_public_key_path
384
385 public_key = open(public_key_path, 'r')
386 public_key_str = public_key.read()
387 public_key.close()
388
389 return public_key_str