blob: 9ddf7b9027678ed2455ca21a0cd02b52d40eed08 [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):
49 """The actual job against which we do everything.
50
51 Properties:
52 autodir
53 The top level autotest directory (/usr/local/autotest).
54 serverdir
55 <autodir>/server/
56 clientdir
57 <autodir>/client/
58 conmuxdir
59 <autodir>/conmux/
60 testdir
61 <autodir>/server/tests/
62 site_testdir
63 <autodir>/server/site_tests/
64 control
65 the control file for this job
66 """
67
68 STATUS_VERSION = 1
69
70
71 def __init__(self, control, args, resultdir, label, user, machines,
72 client=False, parse_job='',
73 ssh_user='root', ssh_port=22, ssh_pass=''):
74 """
75 control
76 The control file (pathname of)
77 args
78 args to pass to the control file
79 resultdir
80 where to throw the results
81 label
82 label for the job
83 user
84 Username for the job (email address)
85 client
86 True if a client-side control file
87 """
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:
98 self.control = open(control, 'r').read()
99 self.control = re.sub('\r', '', self.control)
100 else:
showard45ae8192008-11-05 19:32:53 +0000101 self.control = ''
jadmanski10646442008-08-13 14:05:21 +0000102 self.resultdir = resultdir
mbligh80e1eba2008-11-19 00:26:18 +0000103 if resultdir:
104 if not os.path.exists(resultdir):
105 os.mkdir(resultdir)
106 self.debugdir = os.path.join(resultdir, 'debug')
107 if not os.path.exists(self.debugdir):
108 os.mkdir(self.debugdir)
109 self.status = os.path.join(resultdir, 'status')
110 else:
111 self.status = None
jadmanski10646442008-08-13 14:05:21 +0000112 self.label = label
113 self.user = user
114 self.args = args
115 self.machines = machines
116 self.client = client
117 self.record_prefix = ''
118 self.warning_loggers = set()
119 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()
jadmanski10646442008-08-13 14:05:21 +0000125
126 self.stdout = fd_stack.fd_stack(1, sys.stdout)
127 self.stderr = fd_stack.fd_stack(2, sys.stderr)
128
mbligh80e1eba2008-11-19 00:26:18 +0000129 if resultdir:
130 self.sysinfo = sysinfo.sysinfo(self.resultdir)
jadmanski043e1132008-11-19 17:10:32 +0000131 self.profilers = profilers.profilers(self)
jadmanskic09fc152008-10-15 17:56:59 +0000132
jadmanski025099d2008-09-23 14:13:48 +0000133 if not os.access(self.tmpdir, os.W_OK):
134 try:
135 os.makedirs(self.tmpdir, 0700)
136 except os.error, e:
137 # Thrown if the directory already exists, which it may.
138 pass
139
140 if (not os.access(self.tmpdir, os.W_OK) or
141 not os.path.isdir(self.tmpdir)):
142 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
168 self.pkgmgr = packages.PackageManager(
169 self.autodir, run_function_dargs={'timeout':600})
170 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
jadmanski10646442008-08-13 14:05:21 +0000175
176 def init_parser(self, resultdir):
177 """Start the continuous parsing of resultdir. This sets up
178 the database connection and inserts the basic job object into
179 the database if necessary."""
180 # redirect parser debugging to .parse.log
181 parse_log = os.path.join(resultdir, '.parse.log')
182 parse_log = open(parse_log, 'w', 0)
183 tko_utils.redirect_parser_debugging(parse_log)
184 # create a job model object and set up the db
185 self.results_db = tko_db.db(autocommit=True)
186 self.parser = status_lib.parser(self.STATUS_VERSION)
187 self.job_model = self.parser.make_job(resultdir)
188 self.parser.start(self.job_model)
189 # check if a job already exists in the db and insert it if
190 # it does not
191 job_idx = self.results_db.find_job(self.parse_job)
192 if job_idx is None:
193 self.results_db.insert_job(self.parse_job,
194 self.job_model)
195 else:
196 machine_idx = self.results_db.lookup_machine(
197 self.job_model.machine)
198 self.job_model.index = job_idx
199 self.job_model.machine_idx = machine_idx
200
201
202 def cleanup_parser(self):
203 """This should be called after the server job is finished
204 to carry out any remaining cleanup (e.g. flushing any
205 remaining test results to the results db)"""
206 if not self.using_parser:
207 return
208 final_tests = self.parser.end()
209 for test in final_tests:
210 self.__insert_test(test)
211 self.using_parser = False
212
213
214 def verify(self):
215 if not self.machines:
mbligh084bc172008-10-18 14:02:45 +0000216 raise error.AutoservError('No machines specified to verify')
jadmanski10646442008-08-13 14:05:21 +0000217 try:
jadmanskicdd0c402008-09-19 21:21:31 +0000218 namespace = {'machines' : self.machines, 'job' : self,
219 'ssh_user' : self.ssh_user,
220 'ssh_port' : self.ssh_port,
221 'ssh_pass' : self.ssh_pass}
mbligh084bc172008-10-18 14:02:45 +0000222 self._execute_code(VERIFY_CONTROL_FILE, namespace, protect=False)
jadmanski10646442008-08-13 14:05:21 +0000223 except Exception, e:
224 msg = ('Verify failed\n' + str(e) + '\n'
225 + traceback.format_exc())
226 self.record('ABORT', None, None, msg)
227 raise
228
229
230 def repair(self, host_protection):
231 if not self.machines:
232 raise error.AutoservError('No machines specified to repair')
233 namespace = {'machines': self.machines, 'job': self,
234 'ssh_user': self.ssh_user, 'ssh_port': self.ssh_port,
235 'ssh_pass': self.ssh_pass,
236 'protection_level': host_protection}
237 # no matter what happens during repair, go on to try to reverify
238 try:
mbligh084bc172008-10-18 14:02:45 +0000239 self._execute_code(REPAIR_CONTROL_FILE, namespace,
240 protect=False)
jadmanski10646442008-08-13 14:05:21 +0000241 except Exception, exc:
242 print 'Exception occured during repair'
243 traceback.print_exc()
244 self.verify()
245
246
247 def precheck(self):
248 """
249 perform any additional checks in derived classes.
250 """
251 pass
252
253
254 def enable_external_logging(self):
255 """Start or restart external logging mechanism.
256 """
257 pass
258
259
260 def disable_external_logging(self):
261 """ Pause or stop external logging mechanism.
262 """
263 pass
264
265
jadmanski23afbec2008-09-17 18:12:07 +0000266 def enable_test_cleanup(self):
267 """ By default tests run test.cleanup """
268 self.run_test_cleanup = True
269
270
271 def disable_test_cleanup(self):
272 """ By default tests do not run test.cleanup """
273 self.run_test_cleanup = False
274
275
jadmanski10646442008-08-13 14:05:21 +0000276 def use_external_logging(self):
277 """Return True if external logging should be used.
278 """
279 return False
280
281
282 def parallel_simple(self, function, machines, log=True, timeout=None):
283 """Run 'function' using parallel_simple, with an extra
284 wrapper to handle the necessary setup for continuous parsing,
285 if possible. If continuous parsing is already properly
286 initialized then this should just work."""
287 is_forking = not (len(machines) == 1 and
288 self.machines == machines)
jadmanski4dd1a002008-09-05 20:27:30 +0000289 if self.parse_job and is_forking and log:
jadmanski10646442008-08-13 14:05:21 +0000290 def wrapper(machine):
291 self.parse_job += "/" + machine
292 self.using_parser = True
293 self.machines = [machine]
294 self.resultdir = os.path.join(self.resultdir,
295 machine)
jadmanski609a5f42008-08-26 20:52:42 +0000296 os.chdir(self.resultdir)
showard2bab8f42008-11-12 18:15:22 +0000297 utils.write_keyval(self.resultdir, {"hostname": machine})
jadmanski10646442008-08-13 14:05:21 +0000298 self.init_parser(self.resultdir)
299 result = function(machine)
300 self.cleanup_parser()
301 return result
jadmanski4dd1a002008-09-05 20:27:30 +0000302 elif len(machines) > 1 and log:
jadmanski10646442008-08-13 14:05:21 +0000303 def wrapper(machine):
304 self.resultdir = os.path.join(self.resultdir, machine)
jadmanski609a5f42008-08-26 20:52:42 +0000305 os.chdir(self.resultdir)
jadmanski10646442008-08-13 14:05:21 +0000306 result = function(machine)
307 return result
308 else:
309 wrapper = function
310 subcommand.parallel_simple(wrapper, machines, log, timeout)
311
312
showard45ae8192008-11-05 19:32:53 +0000313 def run(self, cleanup = False, install_before = False,
jadmanski10646442008-08-13 14:05:21 +0000314 install_after = False, collect_crashdumps = True,
315 namespace = {}):
316 # use a copy so changes don't affect the original dictionary
317 namespace = namespace.copy()
318 machines = self.machines
319
320 self.aborted = False
321 namespace['machines'] = machines
322 namespace['args'] = self.args
323 namespace['job'] = self
324 namespace['ssh_user'] = self.ssh_user
325 namespace['ssh_port'] = self.ssh_port
326 namespace['ssh_pass'] = self.ssh_pass
327 test_start_time = int(time.time())
328
mbligh80e1eba2008-11-19 00:26:18 +0000329 if self.resultdir:
330 os.chdir(self.resultdir)
jadmanski10646442008-08-13 14:05:21 +0000331
mbligh80e1eba2008-11-19 00:26:18 +0000332 self.enable_external_logging()
333 status_log = os.path.join(self.resultdir, 'status.log')
jadmanskicdd0c402008-09-19 21:21:31 +0000334 collect_crashinfo = True
jadmanski10646442008-08-13 14:05:21 +0000335 try:
336 if install_before and machines:
mbligh084bc172008-10-18 14:02:45 +0000337 self._execute_code(INSTALL_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000338 if self.client:
339 namespace['control'] = self.control
mbligh084bc172008-10-18 14:02:45 +0000340 utils.open_write_close(CLIENT_CONTROL_FILENAME, self.control)
341 shutil.copy(CLIENT_WRAPPER_CONTROL_FILE,
342 SERVER_CONTROL_FILENAME)
jadmanski10646442008-08-13 14:05:21 +0000343 else:
mbligh084bc172008-10-18 14:02:45 +0000344 utils.open_write_close(SERVER_CONTROL_FILENAME, self.control)
345 self._execute_code(SERVER_CONTROL_FILENAME, namespace)
jadmanski10646442008-08-13 14:05:21 +0000346
jadmanskicdd0c402008-09-19 21:21:31 +0000347 # disable crashinfo collection if we get this far without error
348 collect_crashinfo = False
jadmanski10646442008-08-13 14:05:21 +0000349 finally:
jadmanskicdd0c402008-09-19 21:21:31 +0000350 if machines and (collect_crashdumps or collect_crashinfo):
jadmanski10646442008-08-13 14:05:21 +0000351 namespace['test_start_time'] = test_start_time
jadmanskicdd0c402008-09-19 21:21:31 +0000352 if collect_crashinfo:
mbligh084bc172008-10-18 14:02:45 +0000353 # includes crashdumps
354 self._execute_code(CRASHINFO_CONTROL_FILE, namespace)
jadmanskicdd0c402008-09-19 21:21:31 +0000355 else:
mbligh084bc172008-10-18 14:02:45 +0000356 self._execute_code(CRASHDUMPS_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000357 self.disable_external_logging()
showard45ae8192008-11-05 19:32:53 +0000358 if cleanup and machines:
359 self._execute_code(CLEANUP_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000360 if install_after and machines:
mbligh084bc172008-10-18 14:02:45 +0000361 self._execute_code(INSTALL_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000362
363
364 def run_test(self, url, *args, **dargs):
365 """Summon a test object and run it.
366
367 tag
368 tag to add to testname
369 url
370 url of the test to run
371 """
372
373 (group, testname) = self.pkgmgr.get_package_name(url, 'test')
jadmanski10646442008-08-13 14:05:21 +0000374
375 tag = dargs.pop('tag', None)
376 if tag:
jadmanskide292df2008-08-26 20:51:14 +0000377 testname += '.' + tag
378 subdir = testname
jadmanski10646442008-08-13 14:05:21 +0000379
380 outputdir = os.path.join(self.resultdir, subdir)
381 if os.path.exists(outputdir):
382 msg = ("%s already exists, test <%s> may have"
383 " already run with tag <%s>"
384 % (outputdir, testname, tag) )
385 raise error.TestError(msg)
386 os.mkdir(outputdir)
387
388 def group_func():
389 try:
390 test.runtest(self, url, tag, args, dargs)
391 except error.TestBaseException, e:
392 self.record(e.exit_status, subdir, testname, str(e))
393 raise
394 except Exception, e:
395 info = str(e) + "\n" + traceback.format_exc()
396 self.record('FAIL', subdir, testname, info)
397 raise
398 else:
399 self.record('GOOD', subdir, testname,
400 'completed successfully')
jadmanskide292df2008-08-26 20:51:14 +0000401
402 result, exc_info = self._run_group(testname, subdir, group_func)
403 if exc_info and isinstance(exc_info[1], error.TestBaseException):
404 return False
405 elif exc_info:
406 raise exc_info[0], exc_info[1], exc_info[2]
407 else:
408 return True
jadmanski10646442008-08-13 14:05:21 +0000409
410
411 def _run_group(self, name, subdir, function, *args, **dargs):
412 """\
413 Underlying method for running something inside of a group.
414 """
jadmanskide292df2008-08-26 20:51:14 +0000415 result, exc_info = None, None
jadmanski10646442008-08-13 14:05:21 +0000416 old_record_prefix = self.record_prefix
417 try:
418 self.record('START', subdir, name)
419 self.record_prefix += '\t'
420 try:
421 result = function(*args, **dargs)
422 finally:
423 self.record_prefix = old_record_prefix
424 except error.TestBaseException, e:
425 self.record("END %s" % e.exit_status, subdir, name, str(e))
jadmanskide292df2008-08-26 20:51:14 +0000426 exc_info = sys.exc_info()
jadmanski10646442008-08-13 14:05:21 +0000427 except Exception, e:
428 err_msg = str(e) + '\n'
429 err_msg += traceback.format_exc()
430 self.record('END ABORT', subdir, name, err_msg)
431 raise error.JobError(name + ' failed\n' + traceback.format_exc())
432 else:
433 self.record('END GOOD', subdir, name)
434
jadmanskide292df2008-08-26 20:51:14 +0000435 return result, exc_info
jadmanski10646442008-08-13 14:05:21 +0000436
437
438 def run_group(self, function, *args, **dargs):
439 """\
440 function:
441 subroutine to run
442 *args:
443 arguments for the function
444 """
445
446 name = function.__name__
447
448 # Allow the tag for the group to be specified.
449 tag = dargs.pop('tag', None)
450 if tag:
451 name = tag
452
jadmanskide292df2008-08-26 20:51:14 +0000453 return self._run_group(name, None, function, *args, **dargs)[0]
jadmanski10646442008-08-13 14:05:21 +0000454
455
456 def run_reboot(self, reboot_func, get_kernel_func):
457 """\
458 A specialization of run_group meant specifically for handling
459 a reboot. Includes support for capturing the kernel version
460 after the reboot.
461
462 reboot_func: a function that carries out the reboot
463
464 get_kernel_func: a function that returns a string
465 representing the kernel version.
466 """
467
468 old_record_prefix = self.record_prefix
469 try:
470 self.record('START', None, 'reboot')
471 self.record_prefix += '\t'
472 reboot_func()
473 except Exception, e:
474 self.record_prefix = old_record_prefix
475 err_msg = str(e) + '\n' + traceback.format_exc()
476 self.record('END FAIL', None, 'reboot', err_msg)
477 else:
478 kernel = get_kernel_func()
479 self.record_prefix = old_record_prefix
480 self.record('END GOOD', None, 'reboot',
481 optional_fields={"kernel": kernel})
482
483
jadmanskic09fc152008-10-15 17:56:59 +0000484 def add_sysinfo_command(self, command, logfile=None, on_every_test=False):
485 self._add_sysinfo_loggable(sysinfo.command(command, logfile),
486 on_every_test)
487
488
489 def add_sysinfo_logfile(self, file, on_every_test=False):
490 self._add_sysinfo_loggable(sysinfo.logfile(file), on_every_test)
491
492
493 def _add_sysinfo_loggable(self, loggable, on_every_test):
494 if on_every_test:
495 self.sysinfo.test_loggables.add(loggable)
496 else:
497 self.sysinfo.boot_loggables.add(loggable)
498
499
jadmanski10646442008-08-13 14:05:21 +0000500 def record(self, status_code, subdir, operation, status='',
501 optional_fields=None):
502 """
503 Record job-level status
504
505 The intent is to make this file both machine parseable and
506 human readable. That involves a little more complexity, but
507 really isn't all that bad ;-)
508
509 Format is <status code>\t<subdir>\t<operation>\t<status>
510
mbligh1b3b3762008-09-25 02:46:34 +0000511 status code: see common_lib.log.is_valid_status()
jadmanski10646442008-08-13 14:05:21 +0000512 for valid status definition
513
514 subdir: MUST be a relevant subdirectory in the results,
515 or None, which will be represented as '----'
516
517 operation: description of what you ran (e.g. "dbench", or
518 "mkfs -t foobar /dev/sda9")
519
520 status: error message or "completed sucessfully"
521
522 ------------------------------------------------------------
523
524 Initial tabs indicate indent levels for grouping, and is
525 governed by self.record_prefix
526
527 multiline messages have secondary lines prefaced by a double
528 space (' ')
529
530 Executing this method will trigger the logging of all new
531 warnings to date from the various console loggers.
532 """
533 # poll all our warning loggers for new warnings
534 warnings = self._read_warnings()
535 for timestamp, msg in warnings:
536 self._record("WARN", None, None, msg, timestamp)
537
538 # write out the actual status log line
539 self._record(status_code, subdir, operation, status,
540 optional_fields=optional_fields)
541
542
543 def _read_warnings(self):
544 warnings = []
545 while True:
546 # pull in a line of output from every logger that has
547 # output ready to be read
548 loggers, _, _ = select.select(self.warning_loggers,
549 [], [], 0)
550 closed_loggers = set()
551 for logger in loggers:
552 line = logger.readline()
553 # record any broken pipes (aka line == empty)
554 if len(line) == 0:
555 closed_loggers.add(logger)
556 continue
557 timestamp, msg = line.split('\t', 1)
558 warnings.append((int(timestamp), msg.strip()))
559
560 # stop listening to loggers that are closed
561 self.warning_loggers -= closed_loggers
562
563 # stop if none of the loggers have any output left
564 if not loggers:
565 break
566
567 # sort into timestamp order
568 warnings.sort()
569 return warnings
570
571
572 def _render_record(self, status_code, subdir, operation, status='',
573 epoch_time=None, record_prefix=None,
574 optional_fields=None):
575 """
576 Internal Function to generate a record to be written into a
577 status log. For use by server_job.* classes only.
578 """
579 if subdir:
580 if re.match(r'[\n\t]', subdir):
581 raise ValueError(
582 'Invalid character in subdir string')
583 substr = subdir
584 else:
585 substr = '----'
586
mbligh1b3b3762008-09-25 02:46:34 +0000587 if not log.is_valid_status(status_code):
jadmanski10646442008-08-13 14:05:21 +0000588 raise ValueError('Invalid status code supplied: %s' %
589 status_code)
590 if not operation:
591 operation = '----'
592 if re.match(r'[\n\t]', operation):
593 raise ValueError(
594 'Invalid character in operation string')
595 operation = operation.rstrip()
596 status = status.rstrip()
597 status = re.sub(r"\t", " ", status)
598 # Ensure any continuation lines are marked so we can
599 # detect them in the status file to ensure it is parsable.
600 status = re.sub(r"\n", "\n" + self.record_prefix + " ", status)
601
602 if not optional_fields:
603 optional_fields = {}
604
605 # Generate timestamps for inclusion in the logs
606 if epoch_time is None:
607 epoch_time = int(time.time())
608 local_time = time.localtime(epoch_time)
609 optional_fields["timestamp"] = str(epoch_time)
610 optional_fields["localtime"] = time.strftime("%b %d %H:%M:%S",
611 local_time)
612
613 fields = [status_code, substr, operation]
614 fields += ["%s=%s" % x for x in optional_fields.iteritems()]
615 fields.append(status)
616
617 if record_prefix is None:
618 record_prefix = self.record_prefix
619
620 msg = '\t'.join(str(x) for x in fields)
621
622 return record_prefix + msg + '\n'
623
624
625 def _record_prerendered(self, msg):
626 """
627 Record a pre-rendered msg into the status logs. The only
628 change this makes to the message is to add on the local
629 indentation. Should not be called outside of server_job.*
630 classes. Unlike _record, this does not write the message
631 to standard output.
632 """
633 lines = []
634 status_file = os.path.join(self.resultdir, 'status.log')
635 status_log = open(status_file, 'a')
636 for line in msg.splitlines():
637 line = self.record_prefix + line + '\n'
638 lines.append(line)
639 status_log.write(line)
640 status_log.close()
641 self.__parse_status(lines)
642
643
mbligh084bc172008-10-18 14:02:45 +0000644 def _fill_server_control_namespace(self, namespace, protect=True):
645 """Prepare a namespace to be used when executing server control files.
646
647 This sets up the control file API by importing modules and making them
648 available under the appropriate names within namespace.
649
650 For use by _execute_code().
651
652 Args:
653 namespace: The namespace dictionary to fill in.
654 protect: Boolean. If True (the default) any operation that would
655 clobber an existing entry in namespace will cause an error.
656 Raises:
657 error.AutoservError: When a name would be clobbered by import.
658 """
659 def _import_names(module_name, names=()):
660 """Import a module and assign named attributes into namespace.
661
662 Args:
663 module_name: The string module name.
664 names: A limiting list of names to import from module_name. If
665 empty (the default), all names are imported from the module
666 similar to a "from foo.bar import *" statement.
667 Raises:
668 error.AutoservError: When a name being imported would clobber
669 a name already in namespace.
670 """
671 module = __import__(module_name, {}, {}, names)
672
673 # No names supplied? Import * from the lowest level module.
674 # (Ugh, why do I have to implement this part myself?)
675 if not names:
676 for submodule_name in module_name.split('.')[1:]:
677 module = getattr(module, submodule_name)
678 if hasattr(module, '__all__'):
679 names = getattr(module, '__all__')
680 else:
681 names = dir(module)
682
683 # Install each name into namespace, checking to make sure it
684 # doesn't override anything that already exists.
685 for name in names:
686 # Check for conflicts to help prevent future problems.
687 if name in namespace and protect:
688 if namespace[name] is not getattr(module, name):
689 raise error.AutoservError('importing name '
690 '%s from %s %r would override %r' %
691 (name, module_name, getattr(module, name),
692 namespace[name]))
693 else:
694 # Encourage cleanliness and the use of __all__ for a
695 # more concrete API with less surprises on '*' imports.
696 warnings.warn('%s (%r) being imported from %s for use '
697 'in server control files is not the '
698 'first occurrance of that import.' %
699 (name, namespace[name], module_name))
700
701 namespace[name] = getattr(module, name)
702
703
704 # This is the equivalent of prepending a bunch of import statements to
705 # the front of the control script.
706 namespace.update(os=os, sys=sys)
707 _import_names('autotest_lib.server',
708 ('hosts', 'autotest', 'kvm', 'git', 'standalone_profiler',
709 'source_kernel', 'rpm_kernel', 'deb_kernel', 'git_kernel'))
710 _import_names('autotest_lib.server.subcommand',
711 ('parallel', 'parallel_simple', 'subcommand'))
712 _import_names('autotest_lib.server.utils',
713 ('run', 'get_tmp_dir', 'sh_escape', 'parse_machine'))
714 _import_names('autotest_lib.client.common_lib.error')
715 _import_names('autotest_lib.client.common_lib.barrier', ('barrier',))
716
717 # Inject ourself as the job object into other classes within the API.
718 # (Yuck, this injection is a gross thing be part of a public API. -gps)
719 #
720 # XXX Base & SiteAutotest do not appear to use .job. Who does?
721 namespace['autotest'].Autotest.job = self
722 # server.hosts.base_classes.Host uses .job.
723 namespace['hosts'].Host.job = self
724
725
726 def _execute_code(self, code_file, namespace, protect=True):
727 """Execute code using a copy of namespace as a server control script.
728
729 Unless protect_namespace is explicitly set to False, the dict will not
730 be modified.
731
732 Args:
733 code_file: The filename of the control file to execute.
734 namespace: A dict containing names to make available during execution.
735 protect: Boolean. If True (the default) a copy of the namespace dict
736 is used during execution to prevent the code from modifying its
737 contents outside of this function. If False the raw dict is
738 passed in and modifications will be allowed.
739 """
740 if protect:
741 namespace = namespace.copy()
742 self._fill_server_control_namespace(namespace, protect=protect)
743 # TODO: Simplify and get rid of the special cases for only 1 machine.
showard3e66e8c2008-10-27 19:20:51 +0000744 if len(self.machines) > 1:
mbligh084bc172008-10-18 14:02:45 +0000745 machines_text = '\n'.join(self.machines) + '\n'
746 # Only rewrite the file if it does not match our machine list.
747 try:
748 machines_f = open(MACHINES_FILENAME, 'r')
749 existing_machines_text = machines_f.read()
750 machines_f.close()
751 except EnvironmentError:
752 existing_machines_text = None
753 if machines_text != existing_machines_text:
754 utils.open_write_close(MACHINES_FILENAME, machines_text)
755 execfile(code_file, namespace, namespace)
jadmanski10646442008-08-13 14:05:21 +0000756
757
758 def _record(self, status_code, subdir, operation, status='',
759 epoch_time=None, optional_fields=None):
760 """
761 Actual function for recording a single line into the status
762 logs. Should never be called directly, only by job.record as
763 this would bypass the console monitor logging.
764 """
765
766 msg = self._render_record(status_code, subdir, operation,
767 status, epoch_time,
768 optional_fields=optional_fields)
769
770
771 status_file = os.path.join(self.resultdir, 'status.log')
772 sys.stdout.write(msg)
773 open(status_file, "a").write(msg)
774 if subdir:
775 test_dir = os.path.join(self.resultdir, subdir)
jadmanski5ff55352008-09-18 19:43:46 +0000776 status_file = os.path.join(test_dir, 'status.log')
jadmanski10646442008-08-13 14:05:21 +0000777 open(status_file, "a").write(msg)
778 self.__parse_status(msg.splitlines())
779
780
781 def __parse_status(self, new_lines):
782 if not self.using_parser:
783 return
784 new_tests = self.parser.process_lines(new_lines)
785 for test in new_tests:
786 self.__insert_test(test)
787
788
789 def __insert_test(self, test):
790 """ An internal method to insert a new test result into the
791 database. This method will not raise an exception, even if an
792 error occurs during the insert, to avoid failing a test
793 simply because of unexpected database issues."""
showard21baa452008-10-21 00:08:39 +0000794 self.num_tests_run += 1
795 if status_lib.is_worse_than_or_equal_to(test.status, 'FAIL'):
796 self.num_tests_failed += 1
jadmanski10646442008-08-13 14:05:21 +0000797 try:
798 self.results_db.insert_test(self.job_model, test)
799 except Exception:
800 msg = ("WARNING: An unexpected error occured while "
801 "inserting test results into the database. "
802 "Ignoring error.\n" + traceback.format_exc())
803 print >> sys.stderr, msg
804
mblighcaa62c22008-04-07 21:51:17 +0000805
806# site_server_job.py may be non-existant or empty, make sure that an
807# appropriate site_server_job class is created nevertheless
808try:
jadmanski0afbb632008-06-06 21:10:57 +0000809 from autotest_lib.server.site_server_job import site_server_job
mblighcaa62c22008-04-07 21:51:17 +0000810except ImportError:
jadmanski10646442008-08-13 14:05:21 +0000811 class site_server_job(object):
jadmanski0afbb632008-06-06 21:10:57 +0000812 pass
813
jadmanski10646442008-08-13 14:05:21 +0000814class server_job(site_server_job, base_server_job):
jadmanski0afbb632008-06-06 21:10:57 +0000815 pass