blob: a32ac0acd7931e40a3bb3d897d0d7b65edde9511 [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
37# load up site-specific code for generating site-specific job data
38try:
39 import site_job
40 get_site_job_data = site_job.get_site_job_data
41 del site_job
42except ImportError:
43 # by default provide a stub that generates no site data
44 def get_site_job_data(job):
45 return {}
46
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
67 """
68
69 STATUS_VERSION = 1
70
71
72 def __init__(self, control, args, resultdir, label, user, machines,
73 client=False, parse_job='',
74 ssh_user='root', ssh_port=22, ssh_pass=''):
75 """
76 control
77 The control file (pathname of)
78 args
79 args to pass to the control file
80 resultdir
81 where to throw the results
82 label
83 label for the job
84 user
85 Username for the job (email address)
86 client
87 True if a client-side control file
88 """
89 path = os.path.dirname(__file__)
90 self.autodir = os.path.abspath(os.path.join(path, '..'))
91 self.serverdir = os.path.join(self.autodir, 'server')
92 self.testdir = os.path.join(self.serverdir, 'tests')
93 self.site_testdir = os.path.join(self.serverdir, 'site_tests')
94 self.tmpdir = os.path.join(self.serverdir, 'tmp')
95 self.conmuxdir = os.path.join(self.autodir, 'conmux')
96 self.clientdir = os.path.join(self.autodir, 'client')
97 self.toolsdir = os.path.join(self.autodir, 'client/tools')
98 if control:
99 self.control = open(control, 'r').read()
100 self.control = re.sub('\r', '', self.control)
101 else:
showard45ae8192008-11-05 19:32:53 +0000102 self.control = ''
jadmanski10646442008-08-13 14:05:21 +0000103 self.resultdir = resultdir
mbligh80e1eba2008-11-19 00:26:18 +0000104 if resultdir:
105 if not os.path.exists(resultdir):
106 os.mkdir(resultdir)
107 self.debugdir = os.path.join(resultdir, 'debug')
108 if not os.path.exists(self.debugdir):
109 os.mkdir(self.debugdir)
110 self.status = os.path.join(resultdir, 'status')
111 else:
112 self.status = None
jadmanski10646442008-08-13 14:05:21 +0000113 self.label = label
114 self.user = user
115 self.args = args
116 self.machines = machines
117 self.client = client
118 self.record_prefix = ''
119 self.warning_loggers = set()
120 self.ssh_user = ssh_user
121 self.ssh_port = ssh_port
122 self.ssh_pass = ssh_pass
jadmanski23afbec2008-09-17 18:12:07 +0000123 self.run_test_cleanup = True
mbligh09108442008-10-15 16:27:38 +0000124 self.last_boot_tag = None
jadmanski53aaf382008-11-17 16:22:31 +0000125 self.hosts = set()
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
mbligh80e1eba2008-11-19 00:26:18 +0000153 if self.status and os.path.exists(self.status):
jadmanski10646442008-08-13 14:05:21 +0000154 os.unlink(self.status)
155 job_data = {'label' : label, 'user' : user,
156 'hostname' : ','.join(machines),
157 'status_version' : str(self.STATUS_VERSION)}
mbligh80e1eba2008-11-19 00:26:18 +0000158 if self.resultdir:
159 job_data.update(get_site_job_data(self))
160 utils.write_keyval(self.resultdir, job_data)
jadmanski10646442008-08-13 14:05:21 +0000161
162 self.parse_job = parse_job
163 if self.parse_job and len(machines) == 1:
164 self.using_parser = True
165 self.init_parser(resultdir)
166 else:
167 self.using_parser = False
mbligh2b92b862008-11-22 13:25:32 +0000168 self.pkgmgr = packages.PackageManager(self.autodir,
169 run_function_dargs={'timeout':600})
jadmanski10646442008-08-13 14:05:21 +0000170 self.pkgdir = os.path.join(self.autodir, 'packages')
171
showard21baa452008-10-21 00:08:39 +0000172 self.num_tests_run = 0
173 self.num_tests_failed = 0
174
jadmanski550fdc22008-11-20 16:32:08 +0000175 self._register_subcommand_hooks()
176
177
178 def _register_subcommand_hooks(self):
mbligh2b92b862008-11-22 13:25:32 +0000179 """
180 Register some hooks into the subcommand modules that allow us
181 to properly clean up self.hosts created in forked subprocesses.
182 """
jadmanski550fdc22008-11-20 16:32:08 +0000183 def on_fork(cmd):
184 self._existing_hosts_on_fork = set(self.hosts)
185 def on_join(cmd):
186 new_hosts = self.hosts - self._existing_hosts_on_fork
187 for host in new_hosts:
188 host.close()
189 subcommand.subcommand.register_fork_hook(on_fork)
190 subcommand.subcommand.register_join_hook(on_join)
191
jadmanski10646442008-08-13 14:05:21 +0000192
193 def init_parser(self, resultdir):
mbligh2b92b862008-11-22 13:25:32 +0000194 """
195 Start the continuous parsing of resultdir. This sets up
jadmanski10646442008-08-13 14:05:21 +0000196 the database connection and inserts the basic job object into
mbligh2b92b862008-11-22 13:25:32 +0000197 the database if necessary.
198 """
jadmanski10646442008-08-13 14:05:21 +0000199 # redirect parser debugging to .parse.log
200 parse_log = os.path.join(resultdir, '.parse.log')
201 parse_log = open(parse_log, 'w', 0)
202 tko_utils.redirect_parser_debugging(parse_log)
203 # create a job model object and set up the db
204 self.results_db = tko_db.db(autocommit=True)
205 self.parser = status_lib.parser(self.STATUS_VERSION)
206 self.job_model = self.parser.make_job(resultdir)
207 self.parser.start(self.job_model)
208 # check if a job already exists in the db and insert it if
209 # it does not
210 job_idx = self.results_db.find_job(self.parse_job)
211 if job_idx is None:
mbligh2b92b862008-11-22 13:25:32 +0000212 self.results_db.insert_job(self.parse_job, self.job_model)
jadmanski10646442008-08-13 14:05:21 +0000213 else:
mbligh2b92b862008-11-22 13:25:32 +0000214 machine_idx = self.results_db.lookup_machine(self.job_model.machine)
jadmanski10646442008-08-13 14:05:21 +0000215 self.job_model.index = job_idx
216 self.job_model.machine_idx = machine_idx
217
218
219 def cleanup_parser(self):
mbligh2b92b862008-11-22 13:25:32 +0000220 """
221 This should be called after the server job is finished
jadmanski10646442008-08-13 14:05:21 +0000222 to carry out any remaining cleanup (e.g. flushing any
mbligh2b92b862008-11-22 13:25:32 +0000223 remaining test results to the results db)
224 """
jadmanski10646442008-08-13 14:05:21 +0000225 if not self.using_parser:
226 return
227 final_tests = self.parser.end()
228 for test in final_tests:
229 self.__insert_test(test)
230 self.using_parser = False
231
232
233 def verify(self):
234 if not self.machines:
mbligh084bc172008-10-18 14:02:45 +0000235 raise error.AutoservError('No machines specified to verify')
jadmanski10646442008-08-13 14:05:21 +0000236 try:
jadmanskicdd0c402008-09-19 21:21:31 +0000237 namespace = {'machines' : self.machines, 'job' : self,
238 'ssh_user' : self.ssh_user,
239 'ssh_port' : self.ssh_port,
240 'ssh_pass' : self.ssh_pass}
mbligh084bc172008-10-18 14:02:45 +0000241 self._execute_code(VERIFY_CONTROL_FILE, namespace, protect=False)
jadmanski10646442008-08-13 14:05:21 +0000242 except Exception, e:
mbligh2b92b862008-11-22 13:25:32 +0000243 msg = ('Verify failed\n' + str(e) + '\n' + traceback.format_exc())
jadmanski10646442008-08-13 14:05:21 +0000244 self.record('ABORT', None, None, msg)
245 raise
246
247
248 def repair(self, host_protection):
249 if not self.machines:
250 raise error.AutoservError('No machines specified to repair')
251 namespace = {'machines': self.machines, 'job': self,
252 'ssh_user': self.ssh_user, 'ssh_port': self.ssh_port,
253 'ssh_pass': self.ssh_pass,
254 'protection_level': host_protection}
255 # no matter what happens during repair, go on to try to reverify
256 try:
mbligh2b92b862008-11-22 13:25:32 +0000257 self._execute_code(REPAIR_CONTROL_FILE, namespace, protect=False)
jadmanski10646442008-08-13 14:05:21 +0000258 except Exception, exc:
259 print 'Exception occured during repair'
260 traceback.print_exc()
261 self.verify()
262
263
264 def precheck(self):
265 """
266 perform any additional checks in derived classes.
267 """
268 pass
269
270
271 def enable_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000272 """
273 Start or restart external logging mechanism.
jadmanski10646442008-08-13 14:05:21 +0000274 """
275 pass
276
277
278 def disable_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000279 """
280 Pause or stop external logging mechanism.
jadmanski10646442008-08-13 14:05:21 +0000281 """
282 pass
283
284
jadmanski23afbec2008-09-17 18:12:07 +0000285 def enable_test_cleanup(self):
mbligh2b92b862008-11-22 13:25:32 +0000286 """
287 By default tests run test.cleanup
288 """
jadmanski23afbec2008-09-17 18:12:07 +0000289 self.run_test_cleanup = True
290
291
292 def disable_test_cleanup(self):
mbligh2b92b862008-11-22 13:25:32 +0000293 """
294 By default tests do not run test.cleanup
295 """
jadmanski23afbec2008-09-17 18:12:07 +0000296 self.run_test_cleanup = False
297
298
jadmanski10646442008-08-13 14:05:21 +0000299 def use_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000300 """
301 Return True if external logging should be used.
jadmanski10646442008-08-13 14:05:21 +0000302 """
303 return False
304
305
306 def parallel_simple(self, function, machines, log=True, timeout=None):
mbligh2b92b862008-11-22 13:25:32 +0000307 """
308 Run 'function' using parallel_simple, with an extra wrapper to handle
309 the necessary setup for continuous parsing, if possible. If continuous
310 parsing is already properly initialized then this should just work.
311 """
312 is_forking = not (len(machines) == 1 and self.machines == machines)
jadmanski4dd1a002008-09-05 20:27:30 +0000313 if self.parse_job and is_forking and log:
jadmanski10646442008-08-13 14:05:21 +0000314 def wrapper(machine):
315 self.parse_job += "/" + machine
316 self.using_parser = True
317 self.machines = [machine]
mbligh2b92b862008-11-22 13:25:32 +0000318 self.resultdir = os.path.join(self.resultdir, machine)
jadmanski609a5f42008-08-26 20:52:42 +0000319 os.chdir(self.resultdir)
showard2bab8f42008-11-12 18:15:22 +0000320 utils.write_keyval(self.resultdir, {"hostname": machine})
jadmanski10646442008-08-13 14:05:21 +0000321 self.init_parser(self.resultdir)
322 result = function(machine)
323 self.cleanup_parser()
324 return result
jadmanski4dd1a002008-09-05 20:27:30 +0000325 elif len(machines) > 1 and log:
jadmanski10646442008-08-13 14:05:21 +0000326 def wrapper(machine):
327 self.resultdir = os.path.join(self.resultdir, machine)
jadmanski609a5f42008-08-26 20:52:42 +0000328 os.chdir(self.resultdir)
jadmanski10646442008-08-13 14:05:21 +0000329 result = function(machine)
330 return result
331 else:
332 wrapper = function
333 subcommand.parallel_simple(wrapper, machines, log, timeout)
334
335
mbligh2b92b862008-11-22 13:25:32 +0000336 def run(self, cleanup=False, install_before=False, install_after=False,
337 collect_crashdumps=True, namespace={}):
jadmanski10646442008-08-13 14:05:21 +0000338 # use a copy so changes don't affect the original dictionary
339 namespace = namespace.copy()
340 machines = self.machines
341
342 self.aborted = False
343 namespace['machines'] = machines
344 namespace['args'] = self.args
345 namespace['job'] = self
346 namespace['ssh_user'] = self.ssh_user
347 namespace['ssh_port'] = self.ssh_port
348 namespace['ssh_pass'] = self.ssh_pass
349 test_start_time = int(time.time())
350
mbligh80e1eba2008-11-19 00:26:18 +0000351 if self.resultdir:
352 os.chdir(self.resultdir)
jadmanski10646442008-08-13 14:05:21 +0000353
mbligh80e1eba2008-11-19 00:26:18 +0000354 self.enable_external_logging()
355 status_log = os.path.join(self.resultdir, 'status.log')
jadmanskicdd0c402008-09-19 21:21:31 +0000356 collect_crashinfo = True
jadmanski10646442008-08-13 14:05:21 +0000357 try:
358 if install_before and machines:
mbligh084bc172008-10-18 14:02:45 +0000359 self._execute_code(INSTALL_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000360 if self.client:
361 namespace['control'] = self.control
mbligh084bc172008-10-18 14:02:45 +0000362 utils.open_write_close(CLIENT_CONTROL_FILENAME, self.control)
363 shutil.copy(CLIENT_WRAPPER_CONTROL_FILE,
364 SERVER_CONTROL_FILENAME)
jadmanski10646442008-08-13 14:05:21 +0000365 else:
mbligh084bc172008-10-18 14:02:45 +0000366 utils.open_write_close(SERVER_CONTROL_FILENAME, self.control)
367 self._execute_code(SERVER_CONTROL_FILENAME, namespace)
jadmanski10646442008-08-13 14:05:21 +0000368
jadmanskicdd0c402008-09-19 21:21:31 +0000369 # disable crashinfo collection if we get this far without error
370 collect_crashinfo = False
jadmanski10646442008-08-13 14:05:21 +0000371 finally:
jadmanskicdd0c402008-09-19 21:21:31 +0000372 if machines and (collect_crashdumps or collect_crashinfo):
jadmanski10646442008-08-13 14:05:21 +0000373 namespace['test_start_time'] = test_start_time
jadmanskicdd0c402008-09-19 21:21:31 +0000374 if collect_crashinfo:
mbligh084bc172008-10-18 14:02:45 +0000375 # includes crashdumps
376 self._execute_code(CRASHINFO_CONTROL_FILE, namespace)
jadmanskicdd0c402008-09-19 21:21:31 +0000377 else:
mbligh084bc172008-10-18 14:02:45 +0000378 self._execute_code(CRASHDUMPS_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000379 self.disable_external_logging()
showard45ae8192008-11-05 19:32:53 +0000380 if cleanup and machines:
381 self._execute_code(CLEANUP_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000382 if install_after and machines:
mbligh084bc172008-10-18 14:02:45 +0000383 self._execute_code(INSTALL_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000384
385
386 def run_test(self, url, *args, **dargs):
mbligh2b92b862008-11-22 13:25:32 +0000387 """
388 Summon a test object and run it.
jadmanski10646442008-08-13 14:05:21 +0000389
390 tag
391 tag to add to testname
392 url
393 url of the test to run
394 """
395
396 (group, testname) = self.pkgmgr.get_package_name(url, 'test')
jadmanski10646442008-08-13 14:05:21 +0000397
398 tag = dargs.pop('tag', None)
399 if tag:
jadmanskide292df2008-08-26 20:51:14 +0000400 testname += '.' + tag
401 subdir = testname
jadmanski10646442008-08-13 14:05:21 +0000402
403 outputdir = os.path.join(self.resultdir, subdir)
404 if os.path.exists(outputdir):
405 msg = ("%s already exists, test <%s> may have"
mbligh2b92b862008-11-22 13:25:32 +0000406 " already run with tag <%s>" % (outputdir, testname, tag))
jadmanski10646442008-08-13 14:05:21 +0000407 raise error.TestError(msg)
408 os.mkdir(outputdir)
409
410 def group_func():
411 try:
412 test.runtest(self, url, tag, args, dargs)
413 except error.TestBaseException, e:
414 self.record(e.exit_status, subdir, testname, str(e))
415 raise
416 except Exception, e:
417 info = str(e) + "\n" + traceback.format_exc()
418 self.record('FAIL', subdir, testname, info)
419 raise
420 else:
mbligh2b92b862008-11-22 13:25:32 +0000421 self.record('GOOD', subdir, testname, 'completed successfully')
jadmanskide292df2008-08-26 20:51:14 +0000422
423 result, exc_info = self._run_group(testname, subdir, group_func)
424 if exc_info and isinstance(exc_info[1], error.TestBaseException):
425 return False
426 elif exc_info:
427 raise exc_info[0], exc_info[1], exc_info[2]
428 else:
429 return True
jadmanski10646442008-08-13 14:05:21 +0000430
431
432 def _run_group(self, name, subdir, function, *args, **dargs):
433 """\
434 Underlying method for running something inside of a group.
435 """
jadmanskide292df2008-08-26 20:51:14 +0000436 result, exc_info = None, None
jadmanski10646442008-08-13 14:05:21 +0000437 old_record_prefix = self.record_prefix
438 try:
439 self.record('START', subdir, name)
440 self.record_prefix += '\t'
441 try:
442 result = function(*args, **dargs)
443 finally:
444 self.record_prefix = old_record_prefix
445 except error.TestBaseException, e:
446 self.record("END %s" % e.exit_status, subdir, name, str(e))
jadmanskide292df2008-08-26 20:51:14 +0000447 exc_info = sys.exc_info()
jadmanski10646442008-08-13 14:05:21 +0000448 except Exception, e:
449 err_msg = str(e) + '\n'
450 err_msg += traceback.format_exc()
451 self.record('END ABORT', subdir, name, err_msg)
452 raise error.JobError(name + ' failed\n' + traceback.format_exc())
453 else:
454 self.record('END GOOD', subdir, name)
455
jadmanskide292df2008-08-26 20:51:14 +0000456 return result, exc_info
jadmanski10646442008-08-13 14:05:21 +0000457
458
459 def run_group(self, function, *args, **dargs):
460 """\
461 function:
462 subroutine to run
463 *args:
464 arguments for the function
465 """
466
467 name = function.__name__
468
469 # Allow the tag for the group to be specified.
470 tag = dargs.pop('tag', None)
471 if tag:
472 name = tag
473
jadmanskide292df2008-08-26 20:51:14 +0000474 return self._run_group(name, None, function, *args, **dargs)[0]
jadmanski10646442008-08-13 14:05:21 +0000475
476
477 def run_reboot(self, reboot_func, get_kernel_func):
478 """\
479 A specialization of run_group meant specifically for handling
480 a reboot. Includes support for capturing the kernel version
481 after the reboot.
482
483 reboot_func: a function that carries out the reboot
484
485 get_kernel_func: a function that returns a string
486 representing the kernel version.
487 """
488
489 old_record_prefix = self.record_prefix
490 try:
491 self.record('START', None, 'reboot')
492 self.record_prefix += '\t'
493 reboot_func()
494 except Exception, e:
495 self.record_prefix = old_record_prefix
496 err_msg = str(e) + '\n' + traceback.format_exc()
497 self.record('END FAIL', None, 'reboot', err_msg)
498 else:
499 kernel = get_kernel_func()
500 self.record_prefix = old_record_prefix
501 self.record('END GOOD', None, 'reboot',
502 optional_fields={"kernel": kernel})
503
504
jadmanskic09fc152008-10-15 17:56:59 +0000505 def add_sysinfo_command(self, command, logfile=None, on_every_test=False):
506 self._add_sysinfo_loggable(sysinfo.command(command, logfile),
507 on_every_test)
508
509
510 def add_sysinfo_logfile(self, file, on_every_test=False):
511 self._add_sysinfo_loggable(sysinfo.logfile(file), on_every_test)
512
513
514 def _add_sysinfo_loggable(self, loggable, on_every_test):
515 if on_every_test:
516 self.sysinfo.test_loggables.add(loggable)
517 else:
518 self.sysinfo.boot_loggables.add(loggable)
519
520
jadmanski10646442008-08-13 14:05:21 +0000521 def record(self, status_code, subdir, operation, status='',
522 optional_fields=None):
523 """
524 Record job-level status
525
526 The intent is to make this file both machine parseable and
527 human readable. That involves a little more complexity, but
528 really isn't all that bad ;-)
529
530 Format is <status code>\t<subdir>\t<operation>\t<status>
531
mbligh1b3b3762008-09-25 02:46:34 +0000532 status code: see common_lib.log.is_valid_status()
jadmanski10646442008-08-13 14:05:21 +0000533 for valid status definition
534
535 subdir: MUST be a relevant subdirectory in the results,
536 or None, which will be represented as '----'
537
538 operation: description of what you ran (e.g. "dbench", or
539 "mkfs -t foobar /dev/sda9")
540
541 status: error message or "completed sucessfully"
542
543 ------------------------------------------------------------
544
545 Initial tabs indicate indent levels for grouping, and is
546 governed by self.record_prefix
547
548 multiline messages have secondary lines prefaced by a double
549 space (' ')
550
551 Executing this method will trigger the logging of all new
552 warnings to date from the various console loggers.
553 """
554 # poll all our warning loggers for new warnings
555 warnings = self._read_warnings()
556 for timestamp, msg in warnings:
557 self._record("WARN", None, None, msg, timestamp)
558
559 # write out the actual status log line
560 self._record(status_code, subdir, operation, status,
561 optional_fields=optional_fields)
562
563
564 def _read_warnings(self):
565 warnings = []
566 while True:
567 # pull in a line of output from every logger that has
568 # output ready to be read
mbligh2b92b862008-11-22 13:25:32 +0000569 loggers, _, _ = select.select(self.warning_loggers, [], [], 0)
jadmanski10646442008-08-13 14:05:21 +0000570 closed_loggers = set()
571 for logger in loggers:
572 line = logger.readline()
573 # record any broken pipes (aka line == empty)
574 if len(line) == 0:
575 closed_loggers.add(logger)
576 continue
577 timestamp, msg = line.split('\t', 1)
578 warnings.append((int(timestamp), msg.strip()))
579
580 # stop listening to loggers that are closed
581 self.warning_loggers -= closed_loggers
582
583 # stop if none of the loggers have any output left
584 if not loggers:
585 break
586
587 # sort into timestamp order
588 warnings.sort()
589 return warnings
590
591
592 def _render_record(self, status_code, subdir, operation, status='',
593 epoch_time=None, record_prefix=None,
594 optional_fields=None):
595 """
596 Internal Function to generate a record to be written into a
597 status log. For use by server_job.* classes only.
598 """
599 if subdir:
600 if re.match(r'[\n\t]', subdir):
mbligh2b92b862008-11-22 13:25:32 +0000601 raise ValueError('Invalid character in subdir string')
jadmanski10646442008-08-13 14:05:21 +0000602 substr = subdir
603 else:
604 substr = '----'
605
mbligh1b3b3762008-09-25 02:46:34 +0000606 if not log.is_valid_status(status_code):
mbligh2b92b862008-11-22 13:25:32 +0000607 raise ValueError('Invalid status code supplied: %s' % status_code)
jadmanski10646442008-08-13 14:05:21 +0000608 if not operation:
609 operation = '----'
610 if re.match(r'[\n\t]', operation):
mbligh2b92b862008-11-22 13:25:32 +0000611 raise ValueError('Invalid character in operation string')
jadmanski10646442008-08-13 14:05:21 +0000612 operation = operation.rstrip()
613 status = status.rstrip()
614 status = re.sub(r"\t", " ", status)
615 # Ensure any continuation lines are marked so we can
616 # detect them in the status file to ensure it is parsable.
617 status = re.sub(r"\n", "\n" + self.record_prefix + " ", status)
618
619 if not optional_fields:
620 optional_fields = {}
621
622 # Generate timestamps for inclusion in the logs
623 if epoch_time is None:
624 epoch_time = int(time.time())
625 local_time = time.localtime(epoch_time)
626 optional_fields["timestamp"] = str(epoch_time)
627 optional_fields["localtime"] = time.strftime("%b %d %H:%M:%S",
628 local_time)
629
630 fields = [status_code, substr, operation]
631 fields += ["%s=%s" % x for x in optional_fields.iteritems()]
632 fields.append(status)
633
634 if record_prefix is None:
635 record_prefix = self.record_prefix
636
637 msg = '\t'.join(str(x) for x in fields)
jadmanski10646442008-08-13 14:05:21 +0000638 return record_prefix + msg + '\n'
639
640
641 def _record_prerendered(self, msg):
642 """
643 Record a pre-rendered msg into the status logs. The only
644 change this makes to the message is to add on the local
645 indentation. Should not be called outside of server_job.*
646 classes. Unlike _record, this does not write the message
647 to standard output.
648 """
649 lines = []
650 status_file = os.path.join(self.resultdir, 'status.log')
651 status_log = open(status_file, 'a')
652 for line in msg.splitlines():
653 line = self.record_prefix + line + '\n'
654 lines.append(line)
655 status_log.write(line)
656 status_log.close()
657 self.__parse_status(lines)
658
659
mbligh084bc172008-10-18 14:02:45 +0000660 def _fill_server_control_namespace(self, namespace, protect=True):
mbligh2b92b862008-11-22 13:25:32 +0000661 """
662 Prepare a namespace to be used when executing server control files.
mbligh084bc172008-10-18 14:02:45 +0000663
664 This sets up the control file API by importing modules and making them
665 available under the appropriate names within namespace.
666
667 For use by _execute_code().
668
669 Args:
670 namespace: The namespace dictionary to fill in.
671 protect: Boolean. If True (the default) any operation that would
672 clobber an existing entry in namespace will cause an error.
673 Raises:
674 error.AutoservError: When a name would be clobbered by import.
675 """
676 def _import_names(module_name, names=()):
mbligh2b92b862008-11-22 13:25:32 +0000677 """
678 Import a module and assign named attributes into namespace.
mbligh084bc172008-10-18 14:02:45 +0000679
680 Args:
681 module_name: The string module name.
682 names: A limiting list of names to import from module_name. If
683 empty (the default), all names are imported from the module
684 similar to a "from foo.bar import *" statement.
685 Raises:
686 error.AutoservError: When a name being imported would clobber
687 a name already in namespace.
688 """
689 module = __import__(module_name, {}, {}, names)
690
691 # No names supplied? Import * from the lowest level module.
692 # (Ugh, why do I have to implement this part myself?)
693 if not names:
694 for submodule_name in module_name.split('.')[1:]:
695 module = getattr(module, submodule_name)
696 if hasattr(module, '__all__'):
697 names = getattr(module, '__all__')
698 else:
699 names = dir(module)
700
701 # Install each name into namespace, checking to make sure it
702 # doesn't override anything that already exists.
703 for name in names:
704 # Check for conflicts to help prevent future problems.
705 if name in namespace and protect:
706 if namespace[name] is not getattr(module, name):
707 raise error.AutoservError('importing name '
708 '%s from %s %r would override %r' %
709 (name, module_name, getattr(module, name),
710 namespace[name]))
711 else:
712 # Encourage cleanliness and the use of __all__ for a
713 # more concrete API with less surprises on '*' imports.
714 warnings.warn('%s (%r) being imported from %s for use '
715 'in server control files is not the '
716 'first occurrance of that import.' %
717 (name, namespace[name], module_name))
718
719 namespace[name] = getattr(module, name)
720
721
722 # This is the equivalent of prepending a bunch of import statements to
723 # the front of the control script.
724 namespace.update(os=os, sys=sys)
725 _import_names('autotest_lib.server',
726 ('hosts', 'autotest', 'kvm', 'git', 'standalone_profiler',
727 'source_kernel', 'rpm_kernel', 'deb_kernel', 'git_kernel'))
728 _import_names('autotest_lib.server.subcommand',
729 ('parallel', 'parallel_simple', 'subcommand'))
730 _import_names('autotest_lib.server.utils',
731 ('run', 'get_tmp_dir', 'sh_escape', 'parse_machine'))
732 _import_names('autotest_lib.client.common_lib.error')
733 _import_names('autotest_lib.client.common_lib.barrier', ('barrier',))
734
735 # Inject ourself as the job object into other classes within the API.
736 # (Yuck, this injection is a gross thing be part of a public API. -gps)
737 #
738 # XXX Base & SiteAutotest do not appear to use .job. Who does?
739 namespace['autotest'].Autotest.job = self
740 # server.hosts.base_classes.Host uses .job.
741 namespace['hosts'].Host.job = self
742
743
744 def _execute_code(self, code_file, namespace, protect=True):
mbligh2b92b862008-11-22 13:25:32 +0000745 """
746 Execute code using a copy of namespace as a server control script.
mbligh084bc172008-10-18 14:02:45 +0000747
748 Unless protect_namespace is explicitly set to False, the dict will not
749 be modified.
750
751 Args:
752 code_file: The filename of the control file to execute.
753 namespace: A dict containing names to make available during execution.
754 protect: Boolean. If True (the default) a copy of the namespace dict
755 is used during execution to prevent the code from modifying its
756 contents outside of this function. If False the raw dict is
757 passed in and modifications will be allowed.
758 """
759 if protect:
760 namespace = namespace.copy()
761 self._fill_server_control_namespace(namespace, protect=protect)
762 # TODO: Simplify and get rid of the special cases for only 1 machine.
showard3e66e8c2008-10-27 19:20:51 +0000763 if len(self.machines) > 1:
mbligh084bc172008-10-18 14:02:45 +0000764 machines_text = '\n'.join(self.machines) + '\n'
765 # Only rewrite the file if it does not match our machine list.
766 try:
767 machines_f = open(MACHINES_FILENAME, 'r')
768 existing_machines_text = machines_f.read()
769 machines_f.close()
770 except EnvironmentError:
771 existing_machines_text = None
772 if machines_text != existing_machines_text:
773 utils.open_write_close(MACHINES_FILENAME, machines_text)
774 execfile(code_file, namespace, namespace)
jadmanski10646442008-08-13 14:05:21 +0000775
776
777 def _record(self, status_code, subdir, operation, status='',
778 epoch_time=None, optional_fields=None):
779 """
780 Actual function for recording a single line into the status
781 logs. Should never be called directly, only by job.record as
782 this would bypass the console monitor logging.
783 """
784
mbligh2b92b862008-11-22 13:25:32 +0000785 msg = self._render_record(status_code, subdir, operation, status,
786 epoch_time, optional_fields=optional_fields)
jadmanski10646442008-08-13 14:05:21 +0000787
788
789 status_file = os.path.join(self.resultdir, 'status.log')
790 sys.stdout.write(msg)
791 open(status_file, "a").write(msg)
792 if subdir:
793 test_dir = os.path.join(self.resultdir, subdir)
jadmanski5ff55352008-09-18 19:43:46 +0000794 status_file = os.path.join(test_dir, 'status.log')
jadmanski10646442008-08-13 14:05:21 +0000795 open(status_file, "a").write(msg)
796 self.__parse_status(msg.splitlines())
797
798
799 def __parse_status(self, new_lines):
800 if not self.using_parser:
801 return
802 new_tests = self.parser.process_lines(new_lines)
803 for test in new_tests:
804 self.__insert_test(test)
805
806
807 def __insert_test(self, test):
mbligh2b92b862008-11-22 13:25:32 +0000808 """
809 An internal method to insert a new test result into the
jadmanski10646442008-08-13 14:05:21 +0000810 database. This method will not raise an exception, even if an
811 error occurs during the insert, to avoid failing a test
812 simply because of unexpected database issues."""
showard21baa452008-10-21 00:08:39 +0000813 self.num_tests_run += 1
814 if status_lib.is_worse_than_or_equal_to(test.status, 'FAIL'):
815 self.num_tests_failed += 1
jadmanski10646442008-08-13 14:05:21 +0000816 try:
817 self.results_db.insert_test(self.job_model, test)
818 except Exception:
819 msg = ("WARNING: An unexpected error occured while "
820 "inserting test results into the database. "
821 "Ignoring error.\n" + traceback.format_exc())
822 print >> sys.stderr, msg
823
mblighcaa62c22008-04-07 21:51:17 +0000824
825# site_server_job.py may be non-existant or empty, make sure that an
826# appropriate site_server_job class is created nevertheless
827try:
jadmanski0afbb632008-06-06 21:10:57 +0000828 from autotest_lib.server.site_server_job import site_server_job
mblighcaa62c22008-04-07 21:51:17 +0000829except ImportError:
jadmanski10646442008-08-13 14:05:21 +0000830 class site_server_job(object):
jadmanski0afbb632008-06-06 21:10:57 +0000831 pass
832
jadmanski10646442008-08-13 14:05:21 +0000833class server_job(site_server_job, base_server_job):
jadmanski0afbb632008-06-06 21:10:57 +0000834 pass