blob: e0a07793605efde3c7600fa2cb34ca751a940737 [file] [log] [blame]
mbligh57e78662008-06-17 19:53:49 +00001"""
2The main job wrapper for the server side.
3
4This is the core infrastructure. Derived from the client side job.py
5
6Copyright Martin J. Bligh, Andy Whitcroft 2007
7"""
8
jadmanski025099d2008-09-23 14:13:48 +00009import getpass, os, sys, re, stat, tempfile, time, select, subprocess, traceback
mbligh084bc172008-10-18 14:02:45 +000010import shutil, warnings
jadmanskic09fc152008-10-15 17:56:59 +000011from autotest_lib.client.bin import fd_stack, sysinfo
mbligh09108442008-10-15 16:27:38 +000012from autotest_lib.client.common_lib import error, log, utils, packages
jadmanski043e1132008-11-19 17:10:32 +000013from autotest_lib.server import test, subcommand, profilers
jadmanski10646442008-08-13 14:05:21 +000014from autotest_lib.tko import db as tko_db, status_lib, utils as tko_utils
jadmanski10646442008-08-13 14:05:21 +000015
16
mbligh084bc172008-10-18 14:02:45 +000017def _control_segment_path(name):
18 """Get the pathname of the named control segment file."""
jadmanski10646442008-08-13 14:05:21 +000019 server_dir = os.path.dirname(os.path.abspath(__file__))
mbligh084bc172008-10-18 14:02:45 +000020 return os.path.join(server_dir, "control_segments", name)
jadmanski10646442008-08-13 14:05:21 +000021
22
mbligh084bc172008-10-18 14:02:45 +000023CLIENT_CONTROL_FILENAME = 'control'
24SERVER_CONTROL_FILENAME = 'control.srv'
25MACHINES_FILENAME = '.machines'
jadmanski10646442008-08-13 14:05:21 +000026
mbligh084bc172008-10-18 14:02:45 +000027CLIENT_WRAPPER_CONTROL_FILE = _control_segment_path('client_wrapper')
28CRASHDUMPS_CONTROL_FILE = _control_segment_path('crashdumps')
29CRASHINFO_CONTROL_FILE = _control_segment_path('crashinfo')
mbligh084bc172008-10-18 14:02:45 +000030INSTALL_CONTROL_FILE = _control_segment_path('install')
showard45ae8192008-11-05 19:32:53 +000031CLEANUP_CONTROL_FILE = _control_segment_path('cleanup')
jadmanski10646442008-08-13 14:05:21 +000032
mbligh084bc172008-10-18 14:02:45 +000033VERIFY_CONTROL_FILE = _control_segment_path('verify')
mbligh084bc172008-10-18 14:02:45 +000034REPAIR_CONTROL_FILE = _control_segment_path('repair')
jadmanski10646442008-08-13 14:05:21 +000035
36
mbligh062ed152009-01-13 00:57:14 +000037# by default provide a stub that generates no site data
38def _get_site_job_data_dummy(job):
39 return {}
40
41
jadmanski10646442008-08-13 14:05:21 +000042# load up site-specific code for generating site-specific job data
mbligh062ed152009-01-13 00:57:14 +000043get_site_job_data = utils.import_site_function(__file__,
jadmanskic0a623d2009-03-03 21:11:48 +000044 "autotest_lib.server.site_server_job", "get_site_job_data",
mbligh062ed152009-01-13 00:57:14 +000045 _get_site_job_data_dummy)
jadmanski10646442008-08-13 14:05:21 +000046
47
48class base_server_job(object):
mbligh2b92b862008-11-22 13:25:32 +000049 """
50 The actual job against which we do everything.
jadmanski10646442008-08-13 14:05:21 +000051
52 Properties:
53 autodir
54 The top level autotest directory (/usr/local/autotest).
55 serverdir
56 <autodir>/server/
57 clientdir
58 <autodir>/client/
59 conmuxdir
60 <autodir>/conmux/
61 testdir
62 <autodir>/server/tests/
63 site_testdir
64 <autodir>/server/site_tests/
65 control
66 the control file for this job
mblighb5dac432008-11-27 00:38:44 +000067 drop_caches_between_iterations
68 drop the pagecache between each iteration
jadmanski10646442008-08-13 14:05:21 +000069 """
70
71 STATUS_VERSION = 1
72
73
74 def __init__(self, control, args, resultdir, label, user, machines,
75 client=False, parse_job='',
76 ssh_user='root', ssh_port=22, ssh_pass=''):
77 """
mblighb5dac432008-11-27 00:38:44 +000078 Server side job object.
79
80 Parameters:
81 control: The control file (pathname of)
82 args: args to pass to the control file
83 resultdir: where to throw the results
84 label: label for the job
85 user: Username for the job (email address)
86 client: True if a client-side control file
jadmanski10646442008-08-13 14:05:21 +000087 """
88 path = os.path.dirname(__file__)
89 self.autodir = os.path.abspath(os.path.join(path, '..'))
90 self.serverdir = os.path.join(self.autodir, 'server')
91 self.testdir = os.path.join(self.serverdir, 'tests')
92 self.site_testdir = os.path.join(self.serverdir, 'site_tests')
93 self.tmpdir = os.path.join(self.serverdir, 'tmp')
94 self.conmuxdir = os.path.join(self.autodir, 'conmux')
95 self.clientdir = os.path.join(self.autodir, 'client')
96 self.toolsdir = os.path.join(self.autodir, 'client/tools')
97 if control:
jadmanskie432dd22009-01-30 15:04:51 +000098 self.control = self._load_control_file(control)
jadmanski10646442008-08-13 14:05:21 +000099 else:
showard45ae8192008-11-05 19:32:53 +0000100 self.control = ''
jadmanski10646442008-08-13 14:05:21 +0000101 self.resultdir = resultdir
mbligh80e1eba2008-11-19 00:26:18 +0000102 if resultdir:
103 if not os.path.exists(resultdir):
104 os.mkdir(resultdir)
105 self.debugdir = os.path.join(resultdir, 'debug')
106 if not os.path.exists(self.debugdir):
107 os.mkdir(self.debugdir)
jadmanski779bd292009-03-19 17:33:33 +0000108 status_log = self.get_status_log_path()
109 if os.path.exists(status_log):
110 os.remove(status_log)
jadmanski10646442008-08-13 14:05:21 +0000111 self.label = label
112 self.user = user
113 self.args = args
114 self.machines = machines
115 self.client = client
116 self.record_prefix = ''
117 self.warning_loggers = set()
jadmanskif37df842009-02-11 00:03:26 +0000118 self.warning_manager = warning_manager()
jadmanski10646442008-08-13 14:05:21 +0000119 self.ssh_user = ssh_user
120 self.ssh_port = ssh_port
121 self.ssh_pass = ssh_pass
jadmanski23afbec2008-09-17 18:12:07 +0000122 self.run_test_cleanup = True
mbligh09108442008-10-15 16:27:38 +0000123 self.last_boot_tag = None
jadmanski53aaf382008-11-17 16:22:31 +0000124 self.hosts = set()
mblighb5dac432008-11-27 00:38:44 +0000125 self.drop_caches_between_iterations = False
jadmanski10646442008-08-13 14:05:21 +0000126
127 self.stdout = fd_stack.fd_stack(1, sys.stdout)
128 self.stderr = fd_stack.fd_stack(2, sys.stderr)
129
mbligh80e1eba2008-11-19 00:26:18 +0000130 if resultdir:
131 self.sysinfo = sysinfo.sysinfo(self.resultdir)
jadmanski043e1132008-11-19 17:10:32 +0000132 self.profilers = profilers.profilers(self)
jadmanskic09fc152008-10-15 17:56:59 +0000133
jadmanski025099d2008-09-23 14:13:48 +0000134 if not os.access(self.tmpdir, os.W_OK):
135 try:
136 os.makedirs(self.tmpdir, 0700)
137 except os.error, e:
138 # Thrown if the directory already exists, which it may.
139 pass
140
mbligh2b92b862008-11-22 13:25:32 +0000141 if not (os.access(self.tmpdir, os.W_OK) and os.path.isdir(self.tmpdir)):
jadmanski025099d2008-09-23 14:13:48 +0000142 self.tmpdir = os.path.join(tempfile.gettempdir(),
143 'autotest-' + getpass.getuser())
144 try:
145 os.makedirs(self.tmpdir, 0700)
146 except os.error, e:
147 # Thrown if the directory already exists, which it may.
148 # If the problem was something other than the
149 # directory already existing, this chmod should throw as well
150 # exception.
151 os.chmod(self.tmpdir, stat.S_IRWXU)
152
jadmanski10646442008-08-13 14:05:21 +0000153 job_data = {'label' : label, 'user' : user,
154 'hostname' : ','.join(machines),
showard170873e2009-01-07 00:22:26 +0000155 'status_version' : str(self.STATUS_VERSION),
156 'job_started' : str(int(time.time()))}
mbligh80e1eba2008-11-19 00:26:18 +0000157 if self.resultdir:
158 job_data.update(get_site_job_data(self))
159 utils.write_keyval(self.resultdir, job_data)
jadmanski10646442008-08-13 14:05:21 +0000160
161 self.parse_job = parse_job
162 if self.parse_job and len(machines) == 1:
163 self.using_parser = True
164 self.init_parser(resultdir)
165 else:
166 self.using_parser = False
mbligh2b92b862008-11-22 13:25:32 +0000167 self.pkgmgr = packages.PackageManager(self.autodir,
168 run_function_dargs={'timeout':600})
jadmanski10646442008-08-13 14:05:21 +0000169 self.pkgdir = os.path.join(self.autodir, 'packages')
170
showard21baa452008-10-21 00:08:39 +0000171 self.num_tests_run = 0
172 self.num_tests_failed = 0
173
jadmanski550fdc22008-11-20 16:32:08 +0000174 self._register_subcommand_hooks()
175
176
jadmanskie432dd22009-01-30 15:04:51 +0000177 @staticmethod
178 def _load_control_file(path):
179 f = open(path)
180 try:
181 control_file = f.read()
182 finally:
183 f.close()
184 return re.sub('\r', '', control_file)
185
186
jadmanski550fdc22008-11-20 16:32:08 +0000187 def _register_subcommand_hooks(self):
mbligh2b92b862008-11-22 13:25:32 +0000188 """
189 Register some hooks into the subcommand modules that allow us
190 to properly clean up self.hosts created in forked subprocesses.
191 """
jadmanski550fdc22008-11-20 16:32:08 +0000192 def on_fork(cmd):
193 self._existing_hosts_on_fork = set(self.hosts)
194 def on_join(cmd):
195 new_hosts = self.hosts - self._existing_hosts_on_fork
196 for host in new_hosts:
197 host.close()
198 subcommand.subcommand.register_fork_hook(on_fork)
199 subcommand.subcommand.register_join_hook(on_join)
200
jadmanski10646442008-08-13 14:05:21 +0000201
202 def init_parser(self, resultdir):
mbligh2b92b862008-11-22 13:25:32 +0000203 """
204 Start the continuous parsing of resultdir. This sets up
jadmanski10646442008-08-13 14:05:21 +0000205 the database connection and inserts the basic job object into
mbligh2b92b862008-11-22 13:25:32 +0000206 the database if necessary.
207 """
jadmanski10646442008-08-13 14:05:21 +0000208 # redirect parser debugging to .parse.log
209 parse_log = os.path.join(resultdir, '.parse.log')
210 parse_log = open(parse_log, 'w', 0)
211 tko_utils.redirect_parser_debugging(parse_log)
212 # create a job model object and set up the db
213 self.results_db = tko_db.db(autocommit=True)
214 self.parser = status_lib.parser(self.STATUS_VERSION)
215 self.job_model = self.parser.make_job(resultdir)
216 self.parser.start(self.job_model)
217 # check if a job already exists in the db and insert it if
218 # it does not
219 job_idx = self.results_db.find_job(self.parse_job)
220 if job_idx is None:
mbligh2b92b862008-11-22 13:25:32 +0000221 self.results_db.insert_job(self.parse_job, self.job_model)
jadmanski10646442008-08-13 14:05:21 +0000222 else:
mbligh2b92b862008-11-22 13:25:32 +0000223 machine_idx = self.results_db.lookup_machine(self.job_model.machine)
jadmanski10646442008-08-13 14:05:21 +0000224 self.job_model.index = job_idx
225 self.job_model.machine_idx = machine_idx
226
227
228 def cleanup_parser(self):
mbligh2b92b862008-11-22 13:25:32 +0000229 """
230 This should be called after the server job is finished
jadmanski10646442008-08-13 14:05:21 +0000231 to carry out any remaining cleanup (e.g. flushing any
mbligh2b92b862008-11-22 13:25:32 +0000232 remaining test results to the results db)
233 """
jadmanski10646442008-08-13 14:05:21 +0000234 if not self.using_parser:
235 return
236 final_tests = self.parser.end()
237 for test in final_tests:
238 self.__insert_test(test)
239 self.using_parser = False
240
241
242 def verify(self):
243 if not self.machines:
mbligh084bc172008-10-18 14:02:45 +0000244 raise error.AutoservError('No machines specified to verify')
mbligh0fce4112008-11-27 00:37:17 +0000245 if self.resultdir:
246 os.chdir(self.resultdir)
jadmanski10646442008-08-13 14:05:21 +0000247 try:
jadmanskicdd0c402008-09-19 21:21:31 +0000248 namespace = {'machines' : self.machines, 'job' : self,
249 'ssh_user' : self.ssh_user,
250 'ssh_port' : self.ssh_port,
251 'ssh_pass' : self.ssh_pass}
mbligh084bc172008-10-18 14:02:45 +0000252 self._execute_code(VERIFY_CONTROL_FILE, namespace, protect=False)
jadmanski10646442008-08-13 14:05:21 +0000253 except Exception, e:
mbligh2b92b862008-11-22 13:25:32 +0000254 msg = ('Verify failed\n' + str(e) + '\n' + traceback.format_exc())
jadmanski10646442008-08-13 14:05:21 +0000255 self.record('ABORT', None, None, msg)
256 raise
257
258
259 def repair(self, host_protection):
260 if not self.machines:
261 raise error.AutoservError('No machines specified to repair')
mbligh0fce4112008-11-27 00:37:17 +0000262 if self.resultdir:
263 os.chdir(self.resultdir)
jadmanski10646442008-08-13 14:05:21 +0000264 namespace = {'machines': self.machines, 'job': self,
265 'ssh_user': self.ssh_user, 'ssh_port': self.ssh_port,
266 'ssh_pass': self.ssh_pass,
267 'protection_level': host_protection}
mbligh25c0b8c2009-01-24 01:44:17 +0000268 # no matter what happens during repair (except if it succeeded in
269 # initiating hardware repair procedure), go on to try to reverify
jadmanski10646442008-08-13 14:05:21 +0000270 try:
mbligh2b92b862008-11-22 13:25:32 +0000271 self._execute_code(REPAIR_CONTROL_FILE, namespace, protect=False)
mbligh25c0b8c2009-01-24 01:44:17 +0000272 except error.AutoservHardwareRepairRequestedError:
273 raise
jadmanski10646442008-08-13 14:05:21 +0000274 except Exception, exc:
275 print 'Exception occured during repair'
276 traceback.print_exc()
mbligh25c0b8c2009-01-24 01:44:17 +0000277
jadmanski10646442008-08-13 14:05:21 +0000278 self.verify()
279
280
281 def precheck(self):
282 """
283 perform any additional checks in derived classes.
284 """
285 pass
286
287
288 def enable_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000289 """
290 Start or restart external logging mechanism.
jadmanski10646442008-08-13 14:05:21 +0000291 """
292 pass
293
294
295 def disable_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000296 """
297 Pause or stop external logging mechanism.
jadmanski10646442008-08-13 14:05:21 +0000298 """
299 pass
300
301
jadmanski23afbec2008-09-17 18:12:07 +0000302 def enable_test_cleanup(self):
mbligh2b92b862008-11-22 13:25:32 +0000303 """
304 By default tests run test.cleanup
305 """
jadmanski23afbec2008-09-17 18:12:07 +0000306 self.run_test_cleanup = True
307
308
309 def disable_test_cleanup(self):
mbligh2b92b862008-11-22 13:25:32 +0000310 """
311 By default tests do not run test.cleanup
312 """
jadmanski23afbec2008-09-17 18:12:07 +0000313 self.run_test_cleanup = False
314
315
jadmanski10646442008-08-13 14:05:21 +0000316 def use_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000317 """
318 Return True if external logging should be used.
jadmanski10646442008-08-13 14:05:21 +0000319 """
320 return False
321
322
323 def parallel_simple(self, function, machines, log=True, timeout=None):
mbligh2b92b862008-11-22 13:25:32 +0000324 """
325 Run 'function' using parallel_simple, with an extra wrapper to handle
326 the necessary setup for continuous parsing, if possible. If continuous
327 parsing is already properly initialized then this should just work.
328 """
329 is_forking = not (len(machines) == 1 and self.machines == machines)
jadmanski4dd1a002008-09-05 20:27:30 +0000330 if self.parse_job and is_forking and log:
jadmanski10646442008-08-13 14:05:21 +0000331 def wrapper(machine):
332 self.parse_job += "/" + machine
333 self.using_parser = True
334 self.machines = [machine]
mbligh2b92b862008-11-22 13:25:32 +0000335 self.resultdir = os.path.join(self.resultdir, machine)
jadmanski609a5f42008-08-26 20:52:42 +0000336 os.chdir(self.resultdir)
showard2bab8f42008-11-12 18:15:22 +0000337 utils.write_keyval(self.resultdir, {"hostname": machine})
jadmanski10646442008-08-13 14:05:21 +0000338 self.init_parser(self.resultdir)
339 result = function(machine)
340 self.cleanup_parser()
341 return result
jadmanski4dd1a002008-09-05 20:27:30 +0000342 elif len(machines) > 1 and log:
jadmanski10646442008-08-13 14:05:21 +0000343 def wrapper(machine):
344 self.resultdir = os.path.join(self.resultdir, machine)
jadmanski609a5f42008-08-26 20:52:42 +0000345 os.chdir(self.resultdir)
mbligh838d82d2009-03-11 17:14:31 +0000346 machine_data = {'hostname' : machine,
347 'status_version' : str(self.STATUS_VERSION)}
348 utils.write_keyval(self.resultdir, machine_data)
jadmanski10646442008-08-13 14:05:21 +0000349 result = function(machine)
350 return result
351 else:
352 wrapper = function
353 subcommand.parallel_simple(wrapper, machines, log, timeout)
354
355
jadmanskie432dd22009-01-30 15:04:51 +0000356 USE_TEMP_DIR = object()
mbligh2b92b862008-11-22 13:25:32 +0000357 def run(self, cleanup=False, install_before=False, install_after=False,
jadmanskie432dd22009-01-30 15:04:51 +0000358 collect_crashdumps=True, namespace={}, control=None,
359 control_file_dir=None):
jadmanski10646442008-08-13 14:05:21 +0000360 # use a copy so changes don't affect the original dictionary
361 namespace = namespace.copy()
362 machines = self.machines
jadmanskie432dd22009-01-30 15:04:51 +0000363 if control is None:
364 control = self.control
365 if control_file_dir is None:
366 control_file_dir = self.resultdir
jadmanski10646442008-08-13 14:05:21 +0000367
368 self.aborted = False
369 namespace['machines'] = machines
370 namespace['args'] = self.args
371 namespace['job'] = self
372 namespace['ssh_user'] = self.ssh_user
373 namespace['ssh_port'] = self.ssh_port
374 namespace['ssh_pass'] = self.ssh_pass
375 test_start_time = int(time.time())
376
mbligh80e1eba2008-11-19 00:26:18 +0000377 if self.resultdir:
378 os.chdir(self.resultdir)
jadmanski779bd292009-03-19 17:33:33 +0000379 # touch status.log so that the parser knows a job is running here
380 open(self.get_status_log_path(), 'w').close()
mbligh80e1eba2008-11-19 00:26:18 +0000381 self.enable_external_logging()
jadmanskie432dd22009-01-30 15:04:51 +0000382
jadmanskicdd0c402008-09-19 21:21:31 +0000383 collect_crashinfo = True
mblighaebe3b62008-12-22 14:45:40 +0000384 temp_control_file_dir = None
jadmanski10646442008-08-13 14:05:21 +0000385 try:
386 if install_before and machines:
mbligh084bc172008-10-18 14:02:45 +0000387 self._execute_code(INSTALL_CONTROL_FILE, namespace)
jadmanskie432dd22009-01-30 15:04:51 +0000388
389 # determine the dir to write the control files to
390 if control_file_dir and control_file_dir is not self.USE_TEMP_DIR:
391 temp_control_file_dir = None
mblighaebe3b62008-12-22 14:45:40 +0000392 else:
jadmanskie432dd22009-01-30 15:04:51 +0000393 temp_control_file_dir = control_file_dir = tempfile.mkdtemp(
394 suffix='temp_control_file_dir')
395 server_control_file = os.path.join(control_file_dir,
396 SERVER_CONTROL_FILENAME)
397 client_control_file = os.path.join(control_file_dir,
398 CLIENT_CONTROL_FILENAME)
jadmanski10646442008-08-13 14:05:21 +0000399 if self.client:
jadmanskie432dd22009-01-30 15:04:51 +0000400 namespace['control'] = control
401 utils.open_write_close(client_control_file, control)
mblighaebe3b62008-12-22 14:45:40 +0000402 shutil.copy(CLIENT_WRAPPER_CONTROL_FILE, server_control_file)
jadmanski10646442008-08-13 14:05:21 +0000403 else:
mbligh181b7c22008-11-22 14:22:08 +0000404 namespace['utils'] = utils
jadmanskie432dd22009-01-30 15:04:51 +0000405 utils.open_write_close(server_control_file, control)
mblighaebe3b62008-12-22 14:45:40 +0000406 self._execute_code(server_control_file, namespace)
jadmanski10646442008-08-13 14:05:21 +0000407
jadmanskicdd0c402008-09-19 21:21:31 +0000408 # disable crashinfo collection if we get this far without error
409 collect_crashinfo = False
jadmanski10646442008-08-13 14:05:21 +0000410 finally:
mblighaebe3b62008-12-22 14:45:40 +0000411 if temp_control_file_dir:
jadmanskie432dd22009-01-30 15:04:51 +0000412 # Clean up temp directory used for copies of the control files
mblighaebe3b62008-12-22 14:45:40 +0000413 try:
414 shutil.rmtree(temp_control_file_dir)
415 except Exception, e:
jadmanskie432dd22009-01-30 15:04:51 +0000416 print 'Error %s removing dir %s' % (e,
417 temp_control_file_dir)
418
jadmanskicdd0c402008-09-19 21:21:31 +0000419 if machines and (collect_crashdumps or collect_crashinfo):
jadmanski10646442008-08-13 14:05:21 +0000420 namespace['test_start_time'] = test_start_time
jadmanskicdd0c402008-09-19 21:21:31 +0000421 if collect_crashinfo:
mbligh084bc172008-10-18 14:02:45 +0000422 # includes crashdumps
423 self._execute_code(CRASHINFO_CONTROL_FILE, namespace)
jadmanskicdd0c402008-09-19 21:21:31 +0000424 else:
mbligh084bc172008-10-18 14:02:45 +0000425 self._execute_code(CRASHDUMPS_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000426 self.disable_external_logging()
showard45ae8192008-11-05 19:32:53 +0000427 if cleanup and machines:
428 self._execute_code(CLEANUP_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000429 if install_after and machines:
mbligh084bc172008-10-18 14:02:45 +0000430 self._execute_code(INSTALL_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000431
432
433 def run_test(self, url, *args, **dargs):
mbligh2b92b862008-11-22 13:25:32 +0000434 """
435 Summon a test object and run it.
jadmanski10646442008-08-13 14:05:21 +0000436
437 tag
438 tag to add to testname
439 url
440 url of the test to run
441 """
442
443 (group, testname) = self.pkgmgr.get_package_name(url, 'test')
jadmanski10646442008-08-13 14:05:21 +0000444
445 tag = dargs.pop('tag', None)
446 if tag:
mbligh8ad24202009-01-07 16:49:36 +0000447 testname += '.' + str(tag)
jadmanskide292df2008-08-26 20:51:14 +0000448 subdir = testname
jadmanski10646442008-08-13 14:05:21 +0000449
450 outputdir = os.path.join(self.resultdir, subdir)
451 if os.path.exists(outputdir):
452 msg = ("%s already exists, test <%s> may have"
mbligh2b92b862008-11-22 13:25:32 +0000453 " already run with tag <%s>" % (outputdir, testname, tag))
jadmanski10646442008-08-13 14:05:21 +0000454 raise error.TestError(msg)
455 os.mkdir(outputdir)
456
457 def group_func():
458 try:
459 test.runtest(self, url, tag, args, dargs)
460 except error.TestBaseException, e:
461 self.record(e.exit_status, subdir, testname, str(e))
462 raise
463 except Exception, e:
464 info = str(e) + "\n" + traceback.format_exc()
465 self.record('FAIL', subdir, testname, info)
466 raise
467 else:
mbligh2b92b862008-11-22 13:25:32 +0000468 self.record('GOOD', subdir, testname, 'completed successfully')
jadmanskide292df2008-08-26 20:51:14 +0000469
470 result, exc_info = self._run_group(testname, subdir, group_func)
471 if exc_info and isinstance(exc_info[1], error.TestBaseException):
472 return False
473 elif exc_info:
474 raise exc_info[0], exc_info[1], exc_info[2]
475 else:
476 return True
jadmanski10646442008-08-13 14:05:21 +0000477
478
479 def _run_group(self, name, subdir, function, *args, **dargs):
480 """\
481 Underlying method for running something inside of a group.
482 """
jadmanskide292df2008-08-26 20:51:14 +0000483 result, exc_info = None, None
jadmanski10646442008-08-13 14:05:21 +0000484 old_record_prefix = self.record_prefix
485 try:
486 self.record('START', subdir, name)
487 self.record_prefix += '\t'
488 try:
489 result = function(*args, **dargs)
490 finally:
491 self.record_prefix = old_record_prefix
492 except error.TestBaseException, e:
jadmanskib88d6dc2009-01-10 00:33:18 +0000493 self.record("END %s" % e.exit_status, subdir, name)
jadmanskide292df2008-08-26 20:51:14 +0000494 exc_info = sys.exc_info()
jadmanski10646442008-08-13 14:05:21 +0000495 except Exception, e:
496 err_msg = str(e) + '\n'
497 err_msg += traceback.format_exc()
498 self.record('END ABORT', subdir, name, err_msg)
499 raise error.JobError(name + ' failed\n' + traceback.format_exc())
500 else:
501 self.record('END GOOD', subdir, name)
502
jadmanskide292df2008-08-26 20:51:14 +0000503 return result, exc_info
jadmanski10646442008-08-13 14:05:21 +0000504
505
506 def run_group(self, function, *args, **dargs):
507 """\
508 function:
509 subroutine to run
510 *args:
511 arguments for the function
512 """
513
514 name = function.__name__
515
516 # Allow the tag for the group to be specified.
517 tag = dargs.pop('tag', None)
518 if tag:
519 name = tag
520
jadmanskide292df2008-08-26 20:51:14 +0000521 return self._run_group(name, None, function, *args, **dargs)[0]
jadmanski10646442008-08-13 14:05:21 +0000522
523
524 def run_reboot(self, reboot_func, get_kernel_func):
525 """\
526 A specialization of run_group meant specifically for handling
527 a reboot. Includes support for capturing the kernel version
528 after the reboot.
529
530 reboot_func: a function that carries out the reboot
531
532 get_kernel_func: a function that returns a string
533 representing the kernel version.
534 """
535
536 old_record_prefix = self.record_prefix
537 try:
538 self.record('START', None, 'reboot')
539 self.record_prefix += '\t'
540 reboot_func()
541 except Exception, e:
542 self.record_prefix = old_record_prefix
543 err_msg = str(e) + '\n' + traceback.format_exc()
544 self.record('END FAIL', None, 'reboot', err_msg)
545 else:
546 kernel = get_kernel_func()
547 self.record_prefix = old_record_prefix
548 self.record('END GOOD', None, 'reboot',
549 optional_fields={"kernel": kernel})
550
551
jadmanskie432dd22009-01-30 15:04:51 +0000552 def run_control(self, path):
553 """Execute a control file found at path (relative to the autotest
554 path). Intended for executing a control file within a control file,
555 not for running the top-level job control file."""
556 path = os.path.join(self.autodir, path)
557 control_file = self._load_control_file(path)
558 self.run(control=control_file, control_file_dir=self.USE_TEMP_DIR)
559
560
jadmanskic09fc152008-10-15 17:56:59 +0000561 def add_sysinfo_command(self, command, logfile=None, on_every_test=False):
562 self._add_sysinfo_loggable(sysinfo.command(command, logfile),
563 on_every_test)
564
565
566 def add_sysinfo_logfile(self, file, on_every_test=False):
567 self._add_sysinfo_loggable(sysinfo.logfile(file), on_every_test)
568
569
570 def _add_sysinfo_loggable(self, loggable, on_every_test):
571 if on_every_test:
572 self.sysinfo.test_loggables.add(loggable)
573 else:
574 self.sysinfo.boot_loggables.add(loggable)
575
576
jadmanski10646442008-08-13 14:05:21 +0000577 def record(self, status_code, subdir, operation, status='',
578 optional_fields=None):
579 """
580 Record job-level status
581
582 The intent is to make this file both machine parseable and
583 human readable. That involves a little more complexity, but
584 really isn't all that bad ;-)
585
586 Format is <status code>\t<subdir>\t<operation>\t<status>
587
mbligh1b3b3762008-09-25 02:46:34 +0000588 status code: see common_lib.log.is_valid_status()
jadmanski10646442008-08-13 14:05:21 +0000589 for valid status definition
590
591 subdir: MUST be a relevant subdirectory in the results,
592 or None, which will be represented as '----'
593
594 operation: description of what you ran (e.g. "dbench", or
595 "mkfs -t foobar /dev/sda9")
596
597 status: error message or "completed sucessfully"
598
599 ------------------------------------------------------------
600
601 Initial tabs indicate indent levels for grouping, and is
602 governed by self.record_prefix
603
604 multiline messages have secondary lines prefaced by a double
605 space (' ')
606
607 Executing this method will trigger the logging of all new
608 warnings to date from the various console loggers.
609 """
610 # poll all our warning loggers for new warnings
611 warnings = self._read_warnings()
612 for timestamp, msg in warnings:
613 self._record("WARN", None, None, msg, timestamp)
614
615 # write out the actual status log line
616 self._record(status_code, subdir, operation, status,
617 optional_fields=optional_fields)
618
619
620 def _read_warnings(self):
jadmanskif37df842009-02-11 00:03:26 +0000621 """Poll all the warning loggers and extract any new warnings that have
622 been logged. If the warnings belong to a category that is currently
623 disabled, this method will discard them and they will no longer be
624 retrievable.
625
626 Returns a list of (timestamp, message) tuples, where timestamp is an
627 integer epoch timestamp."""
jadmanski10646442008-08-13 14:05:21 +0000628 warnings = []
629 while True:
630 # pull in a line of output from every logger that has
631 # output ready to be read
mbligh2b92b862008-11-22 13:25:32 +0000632 loggers, _, _ = select.select(self.warning_loggers, [], [], 0)
jadmanski10646442008-08-13 14:05:21 +0000633 closed_loggers = set()
634 for logger in loggers:
635 line = logger.readline()
636 # record any broken pipes (aka line == empty)
637 if len(line) == 0:
638 closed_loggers.add(logger)
639 continue
jadmanskif37df842009-02-11 00:03:26 +0000640 # parse out the warning
641 timestamp, msgtype, msg = line.split('\t', 2)
642 timestamp = int(timestamp)
643 # if the warning is valid, add it to the results
644 if self.warning_manager.is_valid(timestamp, msgtype):
645 warnings.append((timestamp, msg.strip()))
jadmanski10646442008-08-13 14:05:21 +0000646
647 # stop listening to loggers that are closed
648 self.warning_loggers -= closed_loggers
649
650 # stop if none of the loggers have any output left
651 if not loggers:
652 break
653
654 # sort into timestamp order
655 warnings.sort()
656 return warnings
657
658
jadmanskif37df842009-02-11 00:03:26 +0000659 def disable_warnings(self, warning_type, record=True):
660 self.warning_manager.disable_warnings(warning_type)
661 if record:
662 self.record("INFO", None, None,
663 "disabling %s warnings" % warning_type,
664 {"warnings.disable": warning_type})
665
666
667 def enable_warnings(self, warning_type, record=True):
668 self.warning_manager.enable_warnings(warning_type)
669 if record:
670 self.record("INFO", None, None,
671 "enabling %s warnings" % warning_type,
672 {"warnings.enable": warning_type})
673
674
jadmanski779bd292009-03-19 17:33:33 +0000675 def get_status_log_path(self, subdir=None):
676 """Return the path to the job status log.
677
678 @param subdir - Optional paramter indicating that you want the path
679 to a subdirectory status log.
680
681 @returns The path where the status log should be.
682 """
683 if subdir:
684 return os.path.join(self.resultdir, subdir, "status.log")
685 else:
686 return os.path.join(self.resultdir, "status.log")
687
688
jadmanski10646442008-08-13 14:05:21 +0000689 def _render_record(self, status_code, subdir, operation, status='',
690 epoch_time=None, record_prefix=None,
691 optional_fields=None):
692 """
693 Internal Function to generate a record to be written into a
694 status log. For use by server_job.* classes only.
695 """
696 if subdir:
697 if re.match(r'[\n\t]', subdir):
mbligh2b92b862008-11-22 13:25:32 +0000698 raise ValueError('Invalid character in subdir string')
jadmanski10646442008-08-13 14:05:21 +0000699 substr = subdir
700 else:
701 substr = '----'
702
mbligh1b3b3762008-09-25 02:46:34 +0000703 if not log.is_valid_status(status_code):
mbligh2b92b862008-11-22 13:25:32 +0000704 raise ValueError('Invalid status code supplied: %s' % status_code)
jadmanski10646442008-08-13 14:05:21 +0000705 if not operation:
706 operation = '----'
707 if re.match(r'[\n\t]', operation):
mbligh2b92b862008-11-22 13:25:32 +0000708 raise ValueError('Invalid character in operation string')
jadmanski10646442008-08-13 14:05:21 +0000709 operation = operation.rstrip()
710 status = status.rstrip()
711 status = re.sub(r"\t", " ", status)
712 # Ensure any continuation lines are marked so we can
713 # detect them in the status file to ensure it is parsable.
714 status = re.sub(r"\n", "\n" + self.record_prefix + " ", status)
715
716 if not optional_fields:
717 optional_fields = {}
718
719 # Generate timestamps for inclusion in the logs
720 if epoch_time is None:
721 epoch_time = int(time.time())
722 local_time = time.localtime(epoch_time)
723 optional_fields["timestamp"] = str(epoch_time)
724 optional_fields["localtime"] = time.strftime("%b %d %H:%M:%S",
725 local_time)
726
727 fields = [status_code, substr, operation]
728 fields += ["%s=%s" % x for x in optional_fields.iteritems()]
729 fields.append(status)
730
731 if record_prefix is None:
732 record_prefix = self.record_prefix
733
734 msg = '\t'.join(str(x) for x in fields)
jadmanski10646442008-08-13 14:05:21 +0000735 return record_prefix + msg + '\n'
736
737
738 def _record_prerendered(self, msg):
739 """
740 Record a pre-rendered msg into the status logs. The only
741 change this makes to the message is to add on the local
742 indentation. Should not be called outside of server_job.*
743 classes. Unlike _record, this does not write the message
744 to standard output.
745 """
746 lines = []
jadmanski779bd292009-03-19 17:33:33 +0000747 status_file = self.get_status_log_path()
jadmanski10646442008-08-13 14:05:21 +0000748 status_log = open(status_file, 'a')
749 for line in msg.splitlines():
750 line = self.record_prefix + line + '\n'
751 lines.append(line)
752 status_log.write(line)
753 status_log.close()
754 self.__parse_status(lines)
755
756
mbligh084bc172008-10-18 14:02:45 +0000757 def _fill_server_control_namespace(self, namespace, protect=True):
mbligh2b92b862008-11-22 13:25:32 +0000758 """
759 Prepare a namespace to be used when executing server control files.
mbligh084bc172008-10-18 14:02:45 +0000760
761 This sets up the control file API by importing modules and making them
762 available under the appropriate names within namespace.
763
764 For use by _execute_code().
765
766 Args:
767 namespace: The namespace dictionary to fill in.
768 protect: Boolean. If True (the default) any operation that would
769 clobber an existing entry in namespace will cause an error.
770 Raises:
771 error.AutoservError: When a name would be clobbered by import.
772 """
773 def _import_names(module_name, names=()):
mbligh2b92b862008-11-22 13:25:32 +0000774 """
775 Import a module and assign named attributes into namespace.
mbligh084bc172008-10-18 14:02:45 +0000776
777 Args:
778 module_name: The string module name.
779 names: A limiting list of names to import from module_name. If
780 empty (the default), all names are imported from the module
781 similar to a "from foo.bar import *" statement.
782 Raises:
783 error.AutoservError: When a name being imported would clobber
784 a name already in namespace.
785 """
786 module = __import__(module_name, {}, {}, names)
787
788 # No names supplied? Import * from the lowest level module.
789 # (Ugh, why do I have to implement this part myself?)
790 if not names:
791 for submodule_name in module_name.split('.')[1:]:
792 module = getattr(module, submodule_name)
793 if hasattr(module, '__all__'):
794 names = getattr(module, '__all__')
795 else:
796 names = dir(module)
797
798 # Install each name into namespace, checking to make sure it
799 # doesn't override anything that already exists.
800 for name in names:
801 # Check for conflicts to help prevent future problems.
802 if name in namespace and protect:
803 if namespace[name] is not getattr(module, name):
804 raise error.AutoservError('importing name '
805 '%s from %s %r would override %r' %
806 (name, module_name, getattr(module, name),
807 namespace[name]))
808 else:
809 # Encourage cleanliness and the use of __all__ for a
810 # more concrete API with less surprises on '*' imports.
811 warnings.warn('%s (%r) being imported from %s for use '
812 'in server control files is not the '
813 'first occurrance of that import.' %
814 (name, namespace[name], module_name))
815
816 namespace[name] = getattr(module, name)
817
818
819 # This is the equivalent of prepending a bunch of import statements to
820 # the front of the control script.
821 namespace.update(os=os, sys=sys)
822 _import_names('autotest_lib.server',
823 ('hosts', 'autotest', 'kvm', 'git', 'standalone_profiler',
824 'source_kernel', 'rpm_kernel', 'deb_kernel', 'git_kernel'))
825 _import_names('autotest_lib.server.subcommand',
826 ('parallel', 'parallel_simple', 'subcommand'))
827 _import_names('autotest_lib.server.utils',
828 ('run', 'get_tmp_dir', 'sh_escape', 'parse_machine'))
829 _import_names('autotest_lib.client.common_lib.error')
830 _import_names('autotest_lib.client.common_lib.barrier', ('barrier',))
831
832 # Inject ourself as the job object into other classes within the API.
833 # (Yuck, this injection is a gross thing be part of a public API. -gps)
834 #
835 # XXX Base & SiteAutotest do not appear to use .job. Who does?
836 namespace['autotest'].Autotest.job = self
837 # server.hosts.base_classes.Host uses .job.
838 namespace['hosts'].Host.job = self
839
840
841 def _execute_code(self, code_file, namespace, protect=True):
mbligh2b92b862008-11-22 13:25:32 +0000842 """
843 Execute code using a copy of namespace as a server control script.
mbligh084bc172008-10-18 14:02:45 +0000844
845 Unless protect_namespace is explicitly set to False, the dict will not
846 be modified.
847
848 Args:
849 code_file: The filename of the control file to execute.
850 namespace: A dict containing names to make available during execution.
851 protect: Boolean. If True (the default) a copy of the namespace dict
852 is used during execution to prevent the code from modifying its
853 contents outside of this function. If False the raw dict is
854 passed in and modifications will be allowed.
855 """
856 if protect:
857 namespace = namespace.copy()
858 self._fill_server_control_namespace(namespace, protect=protect)
859 # TODO: Simplify and get rid of the special cases for only 1 machine.
showard3e66e8c2008-10-27 19:20:51 +0000860 if len(self.machines) > 1:
mbligh084bc172008-10-18 14:02:45 +0000861 machines_text = '\n'.join(self.machines) + '\n'
862 # Only rewrite the file if it does not match our machine list.
863 try:
864 machines_f = open(MACHINES_FILENAME, 'r')
865 existing_machines_text = machines_f.read()
866 machines_f.close()
867 except EnvironmentError:
868 existing_machines_text = None
869 if machines_text != existing_machines_text:
870 utils.open_write_close(MACHINES_FILENAME, machines_text)
871 execfile(code_file, namespace, namespace)
jadmanski10646442008-08-13 14:05:21 +0000872
873
874 def _record(self, status_code, subdir, operation, status='',
875 epoch_time=None, optional_fields=None):
876 """
877 Actual function for recording a single line into the status
878 logs. Should never be called directly, only by job.record as
879 this would bypass the console monitor logging.
880 """
881
mbligh2b92b862008-11-22 13:25:32 +0000882 msg = self._render_record(status_code, subdir, operation, status,
883 epoch_time, optional_fields=optional_fields)
jadmanski10646442008-08-13 14:05:21 +0000884
jadmanski779bd292009-03-19 17:33:33 +0000885 status_file = self.get_status_log_path()
jadmanski10646442008-08-13 14:05:21 +0000886 sys.stdout.write(msg)
887 open(status_file, "a").write(msg)
888 if subdir:
jadmanski779bd292009-03-19 17:33:33 +0000889 sub_status_file = self.get_status_log_path(subdir)
890 open(sub_status_file, "a").write(msg)
jadmanski10646442008-08-13 14:05:21 +0000891 self.__parse_status(msg.splitlines())
892
893
894 def __parse_status(self, new_lines):
895 if not self.using_parser:
896 return
897 new_tests = self.parser.process_lines(new_lines)
898 for test in new_tests:
899 self.__insert_test(test)
900
901
902 def __insert_test(self, test):
mbligh2b92b862008-11-22 13:25:32 +0000903 """
904 An internal method to insert a new test result into the
jadmanski10646442008-08-13 14:05:21 +0000905 database. This method will not raise an exception, even if an
906 error occurs during the insert, to avoid failing a test
907 simply because of unexpected database issues."""
showard21baa452008-10-21 00:08:39 +0000908 self.num_tests_run += 1
909 if status_lib.is_worse_than_or_equal_to(test.status, 'FAIL'):
910 self.num_tests_failed += 1
jadmanski10646442008-08-13 14:05:21 +0000911 try:
912 self.results_db.insert_test(self.job_model, test)
913 except Exception:
914 msg = ("WARNING: An unexpected error occured while "
915 "inserting test results into the database. "
916 "Ignoring error.\n" + traceback.format_exc())
917 print >> sys.stderr, msg
918
mblighcaa62c22008-04-07 21:51:17 +0000919
mbligha7007722009-01-13 00:37:11 +0000920site_server_job = utils.import_site_class(
921 __file__, "autotest_lib.server.site_server_job", "site_server_job",
922 base_server_job)
jadmanski0afbb632008-06-06 21:10:57 +0000923
jadmanski10646442008-08-13 14:05:21 +0000924class server_job(site_server_job, base_server_job):
jadmanski0afbb632008-06-06 21:10:57 +0000925 pass
jadmanskif37df842009-02-11 00:03:26 +0000926
927
928class warning_manager(object):
929 """Class for controlling warning logs. Manages the enabling and disabling
930 of warnings."""
931 def __init__(self):
932 # a map of warning types to a list of disabled time intervals
933 self.disabled_warnings = {}
934
935
936 def is_valid(self, timestamp, warning_type):
937 """Indicates if a warning (based on the time it occured and its type)
938 is a valid warning. A warning is considered "invalid" if this type of
939 warning was marked as "disabled" at the time the warning occured."""
940 disabled_intervals = self.disabled_warnings.get(warning_type, [])
941 for start, end in disabled_intervals:
942 if timestamp >= start and (end is None or timestamp < end):
943 return False
944 return True
945
946
947 def disable_warnings(self, warning_type, current_time_func=time.time):
948 """As of now, disables all further warnings of this type."""
949 intervals = self.disabled_warnings.setdefault(warning_type, [])
950 if not intervals or intervals[-1][1] is not None:
951 intervals.append((current_time_func(), None))
952
953
954 def enable_warnings(self, warning_type, current_time_func=time.time):
955 """As of now, enables all further warnings of this type."""
956 intervals = self.disabled_warnings.get(warning_type, [])
957 if intervals and intervals[-1][1] is None:
958 intervals[-1] = (intervals[-1][0], current_time_func())