autotest.py revision 548197f1292597dfa5247bcea157379680d1f5e8
1#!/usr/bin/python
2#
3# Copyright 2007 Google Inc. Released under the GPL v2
4
5"""
6This module defines the Autotest class
7
8	Autotest: software to run tests automatically
9"""
10
11__author__ = """
12mbligh@google.com (Martin J. Bligh),
13poirier@google.com (Benjamin Poirier),
14stutsman@google.com (Ryan Stutsman)
15"""
16
17import re
18import os
19import sys
20import subprocess
21import urllib
22import tempfile
23import shutil
24import time
25
26import installable_object
27import utils
28from common import logging
29from common.error import *
30
31
32AUTOTEST_SVN  = 'svn://test.kernel.org/autotest/trunk/client'
33AUTOTEST_HTTP = 'http://test.kernel.org/svn/autotest/trunk/client'
34
35# Timeouts for powering down and up respectively
36HALT_TIME = 300
37BOOT_TIME = 1800
38
39
40class AutotestRunError(AutoservRunError):
41	pass
42
43class AutotestTimeoutError(AutoservRunError):
44	"""This exception is raised when an autotest test exceeds the timeout
45	parameter passed to run_timed_test and is killed.
46	"""
47
48
49class Autotest(installable_object.InstallableObject):
50	"""
51	This class represents the Autotest program.
52
53	Autotest is used to run tests automatically and collect the results.
54	It also supports profilers.
55
56	Implementation details:
57	This is a leaf class in an abstract class hierarchy, it must
58	implement the unimplemented methods in parent classes.
59	"""
60	job = None
61
62
63	def __init__(self, host = None):
64		self.host = host
65		self.got = False
66		self.installed = False
67		self.serverdir = utils.get_server_dir()
68		super(Autotest, self).__init__()
69
70
71	@logging.record
72	def install(self, host = None):
73		"""
74		Install autotest.  If get() was not called previously, an
75		attempt will be made to install from the autotest svn
76		repository.
77
78		Args:
79			host: a Host instance on which autotest will be
80				installed
81
82		Raises:
83			AutoservError: if a tarball was not specified and
84				the target host does not have svn installed in its path
85
86		TODO(poirier): check dependencies
87		autotest needs:
88		bzcat
89		liboptdev (oprofile)
90		binutils-dev (oprofile)
91		make
92		psutils (netperf)
93		"""
94		if not host:
95			host = self.host
96		if not self.got:
97			self.get()
98		host.ensure_up()
99		host.setup()
100		print "Installing autotest on %s" % host.hostname
101
102		autodir = _get_autodir(host)
103		host.run('mkdir -p "%s"' % utils.sh_escape(autodir))
104
105		if getattr(host, 'site_install_autotest', None):
106			if host.site_install_autotest():
107				self.installed = True
108				return
109
110		# try to install from file or directory
111		if self.source_material:
112			if os.path.isdir(self.source_material):
113				# Copy autotest recursively
114				host.send_file(self.source_material, autodir)
115			else:
116				# Copy autotest via tarball
117				raise "Not yet implemented!"
118			print "Installation of autotest completed"
119			self.installed = True
120			return
121
122		# if that fails try to install using svn
123		if utils.run('which svn').exit_status:
124			raise AutoservError('svn not found in path on \
125			target machine: %s' % host.name)
126		try:
127			host.run('svn checkout %s %s' %
128				 (AUTOTEST_SVN, autodir))
129		except AutoservRunError, e:
130			host.run('svn checkout %s %s' %
131				 (AUTOTEST_HTTP, autodir))
132		print "Installation of autotest completed"
133		self.installed = True
134
135
136	def get(self, location = None):
137		if not location:
138			location = os.path.join(self.serverdir, '../client')
139			location = os.path.abspath(location)
140		# If there's stuff run on our client directory already, it
141		# can cause problems. Try giving it a quick clean first.
142		cwd = os.getcwd()
143		os.chdir(location)
144		os.system('tools/make_clean')
145		os.chdir(cwd)
146		super(Autotest, self).get(location)
147		self.got = True
148
149
150	def run(self, control_file, results_dir = '.', host = None,
151		timeout=None):
152		"""
153		Run an autotest job on the remote machine.
154
155		Args:
156			control_file: an open file-like-obj of the control file
157			results_dir: a str path where the results should be stored
158				on the local filesystem
159			host: a Host instance on which the control file should
160				be run
161
162		Raises:
163			AutotestRunError: if there is a problem executing
164				the control file
165		"""
166		results_dir = os.path.abspath(results_dir)
167		if not host:
168			host = self.host
169		if not self.installed:
170			self.install(host)
171
172		host.ensure_up()
173
174		atrun = _Run(host, results_dir)
175		try:
176			atrun.verify_machine()
177		except:
178			print "Verify machine failed on %s. Reinstalling" % \
179								host.hostname
180			self.install(host)
181		atrun.verify_machine()
182		debug = os.path.join(results_dir, 'debug')
183		try:
184			os.makedirs(debug)
185		except:
186			pass
187
188		# Ready .... Aim ....
189		for control in [atrun.remote_control_file,
190				atrun.remote_control_file + '.state',
191				atrun.manual_control_file,
192				atrun.manual_control_file + '.state']:
193			host.run('rm -f ' + control)
194
195		# Copy control_file to remote_control_file on the host
196		tmppath = utils.get(control_file)
197		host.send_file(tmppath, atrun.remote_control_file)
198		os.remove(tmppath)
199
200		try:
201			atrun.execute_control(timeout=timeout)
202		finally:
203			# get the results
204			results = os.path.join(atrun.autodir, 'results',
205					       'default')
206			# Copy all dirs in default to results_dir
207			host.get_file(results + '/', results_dir)
208
209
210	def run_timed_test(self, test_name, results_dir = '.', host = None,
211			   timeout=None, *args, **dargs):
212		"""
213		Assemble a tiny little control file to just run one test,
214		and run it as an autotest client-side test
215		"""
216		if not host:
217			host = self.host
218		if not self.installed:
219			self.install(host)
220		opts = ["%s=%s" % (o[0], repr(o[1])) for o in dargs.items()]
221		cmd = ", ".join([repr(test_name)] + map(repr, args) + opts)
222		control = "job.run_test(%s)\n" % cmd
223		self.run(control, results_dir, host, timeout=timeout)
224
225
226	def run_test(self, test_name, results_dir = '.', host = None,
227		     *args, **dargs):
228		self.run_timed_test(test_name, results_dir, host, None,
229				    *args, **dargs)
230
231
232class _Run(object):
233	"""
234	Represents a run of autotest control file.  This class maintains
235	all the state necessary as an autotest control file is executed.
236
237	It is not intended to be used directly, rather control files
238	should be run using the run method in Autotest.
239	"""
240	def __init__(self, host, results_dir):
241		self.host = host
242		self.results_dir = results_dir
243		self.env = host.env
244
245		self.autodir = _get_autodir(self.host)
246		self.manual_control_file = os.path.join(self.autodir, 'control')
247		self.remote_control_file = os.path.join(self.autodir,
248							     'control.autoserv')
249
250
251	def verify_machine(self):
252		binary = os.path.join(self.autodir, 'bin/autotest')
253		try:
254			self.host.run('ls %s > /dev/null' % binary)
255		except:
256			raise "Autotest does not appear to be installed"
257		tmpdir = os.path.join(self.autodir, 'tmp')
258		self.host.run('umount %s' % tmpdir, ignore_status=True)
259
260
261	def __execute_section(self, section, timeout):
262		print "Executing %s/bin/autotest %s/control phase %d" % \
263					(self.autodir, self.autodir,
264					 section)
265
266		# open up the files we need for our logging
267		client_log_file = os.path.join(self.results_dir, 'debug',
268					       'client.log.%d' % section)
269		client_log = open(client_log_file, 'w', 0)
270		status_log_file = os.path.join(self.results_dir, 'status.log')
271		status_log = open(status_log_file, 'a', 0)
272
273		# create a file-like object for catching the stderr text
274		# from the autotest client and extracting status logs from it
275		class StdErrRedirector(object):
276			"""Partial file object to write to both stdout and
277			the status log file.  We only implement those methods
278			utils.run() actually calls.
279			"""
280			def __init__(self):
281				self.leftover = ""
282				self.last_line = ""
283
284			def _process_line(self, line):
285				"""Write out a line of data to the appropriate
286				stream. Status lines sent by autotest will be
287				prepended with "AUTOTEST_STATUS", and all other
288				lines are ssh error messages.
289				"""
290				if line.startswith("AUTOTEST_STATUS:"):
291					line = line[16:] + "\n"
292					sys.stdout.write(line)
293					status_log.write(line)
294					self.last_line = line
295				else:
296					sys.stderr.write(line + "\n")
297
298			def write(self, data):
299				data = self.leftover + data
300				lines = data.split("\n")
301				# process every line but the last one
302				for line in lines[:-1]:
303					self._process_line(line)
304				# save the last line for later processing
305				# since we may not have the whole line yet
306				self.leftover = lines[-1]
307
308			def flush(self):
309				sys.stdout.flush()
310				sys.stderr.flush()
311				status_log.flush()
312
313			def close(self):
314				if self.leftover:
315					self._process_line(self.leftover)
316					self.flush()
317		redirector = StdErrRedirector()
318
319		# build up the full command we want to run over the host
320		cmd = [os.path.join(self.autodir, 'bin/autotest_client')]
321		if section > 0:
322			cmd.append('-c')
323		cmd.append(self.remote_control_file)
324		full_cmd = ' '.join(cmd)
325
326		result = self.host.run(full_cmd, ignore_status=True,
327				       timeout=timeout,
328				       stdout_tee=client_log,
329				       stderr_tee=redirector)
330		redirector.close()
331
332		if result.exit_status == 1:
333			self.host.job.aborted = True
334		if not result.stderr:
335  			raise AutotestRunError(
336			    "execute_section: %s failed to return anything\n"
337			    "stdout:%s\n" % (full_cmd, result.stdout))
338
339		return redirector.last_line
340
341
342	def execute_control(self, timeout=None):
343		section = 0
344		time_left = None
345		if timeout:
346			end_time = time.time() + timeout
347			time_left = end_time - time.time()
348		while not timeout or time_left > 0:
349			last = self.__execute_section(section, time_left)
350			if timeout:
351				time_left = end_time - time.time()
352				if time_left <= 0:
353					break
354			section += 1
355			if re.match(r'^END .*\t----\t----\t.*$', last):
356				print "Client complete"
357				return
358			elif re.match('^\t*GOOD\t----\treboot\.start.*$', last):
359				print "Client is rebooting"
360				print "Waiting for client to halt"
361				if not self.host.wait_down(HALT_TIME):
362					raise AutotestRunError("%s \
363					failed to shutdown after %ds" %
364							(self.host.hostname,
365							HALT_TIME))
366				print "Client down, waiting for restart"
367				if not self.host.wait_up(BOOT_TIME):
368					# since reboot failed
369					# hardreset the machine once if possible
370					# before failing this control file
371					print "Hardresetting %s" % (
372					    self.host.hostname,)
373					try:
374						self.host.hardreset(wait=False)
375					except AutoservUnsupportedError:
376						print "Hardreset unsupported on %s" % (
377						    self.host.hostname,)
378					raise AutotestRunError("%s failed to "
379						"boot after %ds" % (
380						self.host.hostname,
381						BOOT_TIME,))
382				continue
383			raise AutotestRunError("Aborting - unknown "
384				"return code: %s\n" % last)
385
386		# should only get here if we timed out
387		assert timeout
388		raise AutotestTimeoutError()
389
390
391def _get_autodir(host):
392	try:
393		# There's no clean way to do this. readlink may not exist
394		cmd = "python -c 'import os,sys; print os.readlink(sys.argv[1])' /etc/autotest.conf"
395		dir = os.path.dirname(host.run(cmd).stdout)
396		if dir:
397			return dir
398	except AutoservRunError:
399		pass
400	for path in ['/usr/local/autotest', '/home/autotest']:
401		try:
402			host.run('ls %s > /dev/null' % \
403					 os.path.join(path, 'bin/autotest'))
404			return path
405		except AutoservRunError:
406			pass
407	raise AutotestRunError("Cannot figure out autotest directory")
408