blob: 5dc20ba5c1686dc99a427a3141a323dcd55690b2 [file] [log] [blame]
Dan Shi07e09af2013-04-12 09:31:29 -07001# pylint: disable-msg=C0111
2
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -07003# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
mbligh57e78662008-06-17 19:53:49 +00006"""
7The main job wrapper for the server side.
8
9This is the core infrastructure. Derived from the client side job.py
10
11Copyright Martin J. Bligh, Andy Whitcroft 2007
12"""
13
Scott Zawalski91493c82013-01-25 16:15:20 -050014import getpass, os, sys, re, tempfile, time, select, platform
mblighfc3da5b2010-01-06 18:37:22 +000015import traceback, shutil, warnings, fcntl, pickle, logging, itertools, errno
showard75cdfee2009-06-10 17:40:41 +000016from autotest_lib.client.bin import sysinfo
Alex Miller44ae9232014-06-20 17:24:25 -070017from autotest_lib.client.common_lib import base_job, global_config
Scott Zawalski91493c82013-01-25 16:15:20 -050018from autotest_lib.client.common_lib import error, utils, packages
showard75cdfee2009-06-10 17:40:41 +000019from autotest_lib.client.common_lib import logging_manager
Paul Pendlebury57593562011-06-15 10:45:49 -070020from autotest_lib.server import test, subcommand, profilers
beepsd0672682013-09-16 17:32:16 -070021from autotest_lib.server.hosts import abstract_ssh, factory as host_factory
jadmanski10646442008-08-13 14:05:21 +000022from autotest_lib.tko import db as tko_db, status_lib, utils as tko_utils
jadmanski10646442008-08-13 14:05:21 +000023
24
Alex Miller44ae9232014-06-20 17:24:25 -070025INCREMENTAL_TKO_PARSING = global_config.global_config.get_config_value(
26 'autoserv', 'incremental_tko_parsing', type=bool, default=False)
27
mbligh084bc172008-10-18 14:02:45 +000028def _control_segment_path(name):
29 """Get the pathname of the named control segment file."""
jadmanski10646442008-08-13 14:05:21 +000030 server_dir = os.path.dirname(os.path.abspath(__file__))
mbligh084bc172008-10-18 14:02:45 +000031 return os.path.join(server_dir, "control_segments", name)
jadmanski10646442008-08-13 14:05:21 +000032
33
mbligh084bc172008-10-18 14:02:45 +000034CLIENT_CONTROL_FILENAME = 'control'
35SERVER_CONTROL_FILENAME = 'control.srv'
36MACHINES_FILENAME = '.machines'
jadmanski10646442008-08-13 14:05:21 +000037
mbligh084bc172008-10-18 14:02:45 +000038CLIENT_WRAPPER_CONTROL_FILE = _control_segment_path('client_wrapper')
39CRASHDUMPS_CONTROL_FILE = _control_segment_path('crashdumps')
40CRASHINFO_CONTROL_FILE = _control_segment_path('crashinfo')
mbligh084bc172008-10-18 14:02:45 +000041INSTALL_CONTROL_FILE = _control_segment_path('install')
showard45ae8192008-11-05 19:32:53 +000042CLEANUP_CONTROL_FILE = _control_segment_path('cleanup')
mbligh084bc172008-10-18 14:02:45 +000043VERIFY_CONTROL_FILE = _control_segment_path('verify')
mbligh084bc172008-10-18 14:02:45 +000044REPAIR_CONTROL_FILE = _control_segment_path('repair')
Alex Millercb79ba72013-05-29 14:43:00 -070045PROVISION_CONTROL_FILE = _control_segment_path('provision')
beepscb6f1e22013-06-28 19:14:10 -070046VERIFY_JOB_REPO_URL_CONTROL_FILE = _control_segment_path('verify_job_repo_url')
Dan Shi07e09af2013-04-12 09:31:29 -070047RESET_CONTROL_FILE = _control_segment_path('reset')
jadmanski10646442008-08-13 14:05:21 +000048
mbligh062ed152009-01-13 00:57:14 +000049# by default provide a stub that generates no site data
50def _get_site_job_data_dummy(job):
51 return {}
52
53
jadmanski2a89dac2010-06-11 14:32:58 +000054class status_indenter(base_job.status_indenter):
55 """Provide a simple integer-backed status indenter."""
56 def __init__(self):
57 self._indent = 0
58
59
60 @property
61 def indent(self):
62 return self._indent
63
64
65 def increment(self):
66 self._indent += 1
67
68
69 def decrement(self):
70 self._indent -= 1
71
72
jadmanski52053632010-06-11 21:08:10 +000073 def get_context(self):
74 """Returns a context object for use by job.get_record_context."""
75 class context(object):
76 def __init__(self, indenter, indent):
77 self._indenter = indenter
78 self._indent = indent
79 def restore(self):
80 self._indenter._indent = self._indent
81 return context(self, self._indent)
82
83
jadmanski2a89dac2010-06-11 14:32:58 +000084class server_job_record_hook(object):
85 """The job.record hook for server job. Used to inject WARN messages from
86 the console or vlm whenever new logs are written, and to echo any logs
87 to INFO level logging. Implemented as a class so that it can use state to
88 block recursive calls, so that the hook can call job.record itself to
89 log WARN messages.
90
91 Depends on job._read_warnings and job._logger.
92 """
93 def __init__(self, job):
94 self._job = job
95 self._being_called = False
96
97
98 def __call__(self, entry):
99 """A wrapper around the 'real' record hook, the _hook method, which
100 prevents recursion. This isn't making any effort to be threadsafe,
101 the intent is to outright block infinite recursion via a
102 job.record->_hook->job.record->_hook->job.record... chain."""
103 if self._being_called:
104 return
105 self._being_called = True
106 try:
107 self._hook(self._job, entry)
108 finally:
109 self._being_called = False
110
111
112 @staticmethod
113 def _hook(job, entry):
114 """The core hook, which can safely call job.record."""
115 entries = []
116 # poll all our warning loggers for new warnings
117 for timestamp, msg in job._read_warnings():
118 warning_entry = base_job.status_log_entry(
119 'WARN', None, None, msg, {}, timestamp=timestamp)
120 entries.append(warning_entry)
121 job.record_entry(warning_entry)
122 # echo rendered versions of all the status logs to info
123 entries.append(entry)
124 for entry in entries:
125 rendered_entry = job._logger.render_entry(entry)
126 logging.info(rendered_entry)
jadmanskie29d0e42010-06-17 16:06:52 +0000127 job._parse_status(rendered_entry)
jadmanski2a89dac2010-06-11 14:32:58 +0000128
129
mbligh0d0f67d2009-11-06 03:15:03 +0000130class base_server_job(base_job.base_job):
131 """The server-side concrete implementation of base_job.
jadmanski10646442008-08-13 14:05:21 +0000132
mbligh0d0f67d2009-11-06 03:15:03 +0000133 Optional properties provided by this implementation:
134 serverdir
135 conmuxdir
136
137 num_tests_run
138 num_tests_failed
139
140 warning_manager
141 warning_loggers
jadmanski10646442008-08-13 14:05:21 +0000142 """
143
mbligh0d0f67d2009-11-06 03:15:03 +0000144 _STATUS_VERSION = 1
jadmanski10646442008-08-13 14:05:21 +0000145
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700146 # TODO crbug.com/285395 eliminate ssh_verbosity_flag
jadmanski10646442008-08-13 14:05:21 +0000147 def __init__(self, control, args, resultdir, label, user, machines,
148 client=False, parse_job='',
beepsd0672682013-09-16 17:32:16 -0700149 ssh_user=host_factory.DEFAULT_SSH_USER,
150 ssh_port=host_factory.DEFAULT_SSH_PORT,
151 ssh_pass=host_factory.DEFAULT_SSH_PASS,
152 ssh_verbosity_flag=host_factory.DEFAULT_SSH_VERBOSITY,
153 ssh_options=host_factory.DEFAULT_SSH_OPTIONS,
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700154 test_retry=0, group_name='',
Fang Dengd1c2b732013-08-20 12:59:46 -0700155 tag='', disable_sysinfo=False,
Dan Shi70647ca2015-07-16 22:52:35 -0700156 control_filename=SERVER_CONTROL_FILENAME,
157 parent_job_id=None):
jadmanski10646442008-08-13 14:05:21 +0000158 """
mbligh374f3412009-05-13 21:29:45 +0000159 Create a server side job object.
mblighb5dac432008-11-27 00:38:44 +0000160
mblighe7d9c602009-07-02 19:02:33 +0000161 @param control: The pathname of the control file.
162 @param args: Passed to the control file.
163 @param resultdir: Where to throw the results.
164 @param label: Description of the job.
165 @param user: Username for the job (email address).
166 @param client: True if this is a client-side control file.
167 @param parse_job: string, if supplied it is the job execution tag that
168 the results will be passed through to the TKO parser with.
169 @param ssh_user: The SSH username. [root]
170 @param ssh_port: The SSH port number. [22]
171 @param ssh_pass: The SSH passphrase, if needed.
Fang Dengd1c2b732013-08-20 12:59:46 -0700172 @param ssh_verbosity_flag: The SSH verbosity flag, '-v', '-vv',
173 '-vvv', or an empty string if not needed.
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700174 @param ssh_options: A string giving additional options that will be
175 included in ssh commands.
Scott Zawalski91493c82013-01-25 16:15:20 -0500176 @param test_retry: The number of times to retry a test if the test did
177 not complete successfully.
mblighe7d9c602009-07-02 19:02:33 +0000178 @param group_name: If supplied, this will be written out as
mbligh374f3412009-05-13 21:29:45 +0000179 host_group_name in the keyvals file for the parser.
mblighe7d9c602009-07-02 19:02:33 +0000180 @param tag: The job execution tag from the scheduler. [optional]
Christopher Wiley8a91f232013-07-09 11:02:27 -0700181 @param disable_sysinfo: Whether we should disable the sysinfo step of
182 tests for a modest shortening of test time. [optional]
mblighe0cbc912010-03-11 18:03:07 +0000183 @param control_filename: The filename where the server control file
184 should be written in the results directory.
Dan Shi70647ca2015-07-16 22:52:35 -0700185 @param parent_job_id: Job ID of the parent job. Default to None if the
186 job does not have a parent job.
jadmanski10646442008-08-13 14:05:21 +0000187 """
Scott Zawalski91493c82013-01-25 16:15:20 -0500188 super(base_server_job, self).__init__(resultdir=resultdir,
189 test_retry=test_retry)
mbligh0d0f67d2009-11-06 03:15:03 +0000190 path = os.path.dirname(__file__)
Scott Zawalski91493c82013-01-25 16:15:20 -0500191 self.test_retry = test_retry
mbligh0d0f67d2009-11-06 03:15:03 +0000192 self.control = control
193 self._uncollected_log_file = os.path.join(self.resultdir,
194 'uncollected_logs')
195 debugdir = os.path.join(self.resultdir, 'debug')
196 if not os.path.exists(debugdir):
197 os.mkdir(debugdir)
198
199 if user:
200 self.user = user
201 else:
202 self.user = getpass.getuser()
203
jadmanski808f4b12010-04-09 22:30:31 +0000204 self.args = args
Peter Mayo7a875762012-06-13 14:38:15 -0400205 self.label = label
jadmanski10646442008-08-13 14:05:21 +0000206 self.machines = machines
mbligh0d0f67d2009-11-06 03:15:03 +0000207 self._client = client
jadmanski10646442008-08-13 14:05:21 +0000208 self.warning_loggers = set()
jadmanskif37df842009-02-11 00:03:26 +0000209 self.warning_manager = warning_manager()
mbligh0d0f67d2009-11-06 03:15:03 +0000210 self._ssh_user = ssh_user
211 self._ssh_port = ssh_port
212 self._ssh_pass = ssh_pass
Fang Dengd1c2b732013-08-20 12:59:46 -0700213 self._ssh_verbosity_flag = ssh_verbosity_flag
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700214 self._ssh_options = ssh_options
mblighe7d9c602009-07-02 19:02:33 +0000215 self.tag = tag
mbligh09108442008-10-15 16:27:38 +0000216 self.last_boot_tag = None
jadmanski53aaf382008-11-17 16:22:31 +0000217 self.hosts = set()
mbligh0d0f67d2009-11-06 03:15:03 +0000218 self.drop_caches = False
mblighb5dac432008-11-27 00:38:44 +0000219 self.drop_caches_between_iterations = False
mblighe0cbc912010-03-11 18:03:07 +0000220 self._control_filename = control_filename
Christopher Wiley8a91f232013-07-09 11:02:27 -0700221 self._disable_sysinfo = disable_sysinfo
jadmanski10646442008-08-13 14:05:21 +0000222
showard75cdfee2009-06-10 17:40:41 +0000223 self.logging = logging_manager.get_logging_manager(
224 manage_stdout_and_stderr=True, redirect_fds=True)
225 subcommand.logging_manager_object = self.logging
jadmanski10646442008-08-13 14:05:21 +0000226
mbligh0d0f67d2009-11-06 03:15:03 +0000227 self.sysinfo = sysinfo.sysinfo(self.resultdir)
jadmanski043e1132008-11-19 17:10:32 +0000228 self.profilers = profilers.profilers(self)
jadmanskic09fc152008-10-15 17:56:59 +0000229
jadmanski10646442008-08-13 14:05:21 +0000230 job_data = {'label' : label, 'user' : user,
231 'hostname' : ','.join(machines),
Eric Li861b2d52011-02-04 14:50:35 -0800232 'drone' : platform.node(),
mbligh0d0f67d2009-11-06 03:15:03 +0000233 'status_version' : str(self._STATUS_VERSION),
showard170873e2009-01-07 00:22:26 +0000234 'job_started' : str(int(time.time()))}
Dan Shi70647ca2015-07-16 22:52:35 -0700235 # Save parent job id to keyvals, so parser can retrieve the info and
236 # write to tko_jobs record.
237 if parent_job_id:
238 job_data['parent_job_id'] = parent_job_id
mbligh374f3412009-05-13 21:29:45 +0000239 if group_name:
240 job_data['host_group_name'] = group_name
jadmanski10646442008-08-13 14:05:21 +0000241
mbligh0d0f67d2009-11-06 03:15:03 +0000242 # only write these keyvals out on the first job in a resultdir
243 if 'job_started' not in utils.read_keyval(self.resultdir):
244 job_data.update(get_site_job_data(self))
245 utils.write_keyval(self.resultdir, job_data)
246
247 self._parse_job = parse_job
Alex Miller44ae9232014-06-20 17:24:25 -0700248 self._using_parser = (INCREMENTAL_TKO_PARSING and self._parse_job
249 and len(machines) <= 1)
mbligh0d0f67d2009-11-06 03:15:03 +0000250 self.pkgmgr = packages.PackageManager(
251 self.autodir, run_function_dargs={'timeout':600})
showard21baa452008-10-21 00:08:39 +0000252 self.num_tests_run = 0
253 self.num_tests_failed = 0
254
jadmanski550fdc22008-11-20 16:32:08 +0000255 self._register_subcommand_hooks()
256
mbligh0d0f67d2009-11-06 03:15:03 +0000257 # these components aren't usable on the server
258 self.bootloader = None
259 self.harness = None
260
jadmanski2a89dac2010-06-11 14:32:58 +0000261 # set up the status logger
jadmanski52053632010-06-11 21:08:10 +0000262 self._indenter = status_indenter()
jadmanski2a89dac2010-06-11 14:32:58 +0000263 self._logger = base_job.status_logger(
jadmanski52053632010-06-11 21:08:10 +0000264 self, self._indenter, 'status.log', 'status.log',
jadmanski2a89dac2010-06-11 14:32:58 +0000265 record_hook=server_job_record_hook(self))
266
Dan Shib03ea9d2013-08-15 17:13:27 -0700267 # Initialize a flag to indicate DUT failure during the test, e.g.,
268 # unexpected reboot.
269 self.failed_with_device_error = False
270
Dan Shi70647ca2015-07-16 22:52:35 -0700271 self.parent_job_id = parent_job_id
272
mbligh0d0f67d2009-11-06 03:15:03 +0000273
274 @classmethod
275 def _find_base_directories(cls):
276 """
277 Determine locations of autodir, clientdir and serverdir. Assumes
278 that this file is located within serverdir and uses __file__ along
279 with relative paths to resolve the location.
280 """
281 serverdir = os.path.abspath(os.path.dirname(__file__))
282 autodir = os.path.normpath(os.path.join(serverdir, '..'))
283 clientdir = os.path.join(autodir, 'client')
284 return autodir, clientdir, serverdir
285
286
Scott Zawalski91493c82013-01-25 16:15:20 -0500287 def _find_resultdir(self, resultdir, *args, **dargs):
mbligh0d0f67d2009-11-06 03:15:03 +0000288 """
289 Determine the location of resultdir. For server jobs we expect one to
290 always be explicitly passed in to __init__, so just return that.
291 """
292 if resultdir:
293 return os.path.normpath(resultdir)
294 else:
295 return None
296
jadmanski550fdc22008-11-20 16:32:08 +0000297
jadmanski2a89dac2010-06-11 14:32:58 +0000298 def _get_status_logger(self):
299 """Return a reference to the status logger."""
300 return self._logger
301
302
jadmanskie432dd22009-01-30 15:04:51 +0000303 @staticmethod
304 def _load_control_file(path):
305 f = open(path)
306 try:
307 control_file = f.read()
308 finally:
309 f.close()
310 return re.sub('\r', '', control_file)
311
312
jadmanski550fdc22008-11-20 16:32:08 +0000313 def _register_subcommand_hooks(self):
mbligh2b92b862008-11-22 13:25:32 +0000314 """
315 Register some hooks into the subcommand modules that allow us
316 to properly clean up self.hosts created in forked subprocesses.
317 """
jadmanski550fdc22008-11-20 16:32:08 +0000318 def on_fork(cmd):
319 self._existing_hosts_on_fork = set(self.hosts)
320 def on_join(cmd):
321 new_hosts = self.hosts - self._existing_hosts_on_fork
322 for host in new_hosts:
323 host.close()
324 subcommand.subcommand.register_fork_hook(on_fork)
325 subcommand.subcommand.register_join_hook(on_join)
326
jadmanski10646442008-08-13 14:05:21 +0000327
mbligh4608b002010-01-05 18:22:35 +0000328 def init_parser(self):
mbligh2b92b862008-11-22 13:25:32 +0000329 """
mbligh4608b002010-01-05 18:22:35 +0000330 Start the continuous parsing of self.resultdir. This sets up
jadmanski10646442008-08-13 14:05:21 +0000331 the database connection and inserts the basic job object into
mbligh2b92b862008-11-22 13:25:32 +0000332 the database if necessary.
333 """
mbligh4608b002010-01-05 18:22:35 +0000334 if not self._using_parser:
335 return
jadmanski10646442008-08-13 14:05:21 +0000336 # redirect parser debugging to .parse.log
mbligh4608b002010-01-05 18:22:35 +0000337 parse_log = os.path.join(self.resultdir, '.parse.log')
jadmanski10646442008-08-13 14:05:21 +0000338 parse_log = open(parse_log, 'w', 0)
339 tko_utils.redirect_parser_debugging(parse_log)
340 # create a job model object and set up the db
341 self.results_db = tko_db.db(autocommit=True)
mbligh0d0f67d2009-11-06 03:15:03 +0000342 self.parser = status_lib.parser(self._STATUS_VERSION)
mbligh4608b002010-01-05 18:22:35 +0000343 self.job_model = self.parser.make_job(self.resultdir)
jadmanski10646442008-08-13 14:05:21 +0000344 self.parser.start(self.job_model)
345 # check if a job already exists in the db and insert it if
346 # it does not
mbligh0d0f67d2009-11-06 03:15:03 +0000347 job_idx = self.results_db.find_job(self._parse_job)
jadmanski10646442008-08-13 14:05:21 +0000348 if job_idx is None:
Dan Shi70647ca2015-07-16 22:52:35 -0700349 self.results_db.insert_job(self._parse_job, self.job_model,
350 self.parent_job_id)
jadmanski10646442008-08-13 14:05:21 +0000351 else:
mbligh2b92b862008-11-22 13:25:32 +0000352 machine_idx = self.results_db.lookup_machine(self.job_model.machine)
jadmanski10646442008-08-13 14:05:21 +0000353 self.job_model.index = job_idx
354 self.job_model.machine_idx = machine_idx
355
356
357 def cleanup_parser(self):
mbligh2b92b862008-11-22 13:25:32 +0000358 """
359 This should be called after the server job is finished
jadmanski10646442008-08-13 14:05:21 +0000360 to carry out any remaining cleanup (e.g. flushing any
mbligh2b92b862008-11-22 13:25:32 +0000361 remaining test results to the results db)
362 """
mbligh0d0f67d2009-11-06 03:15:03 +0000363 if not self._using_parser:
jadmanski10646442008-08-13 14:05:21 +0000364 return
365 final_tests = self.parser.end()
366 for test in final_tests:
367 self.__insert_test(test)
mbligh0d0f67d2009-11-06 03:15:03 +0000368 self._using_parser = False
jadmanski10646442008-08-13 14:05:21 +0000369
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700370 # TODO crbug.com/285395 add a kwargs parameter.
371 def _make_namespace(self):
372 """Create a namespace dictionary to be passed along to control file.
373
374 Creates a namespace argument populated with standard values:
375 machines, job, ssh_user, ssh_port, ssh_pass, ssh_verbosity_flag,
376 and ssh_options.
377 """
378 namespace = {'machines' : self.machines,
379 'job' : self,
380 'ssh_user' : self._ssh_user,
381 'ssh_port' : self._ssh_port,
382 'ssh_pass' : self._ssh_pass,
383 'ssh_verbosity_flag' : self._ssh_verbosity_flag,
384 'ssh_options' : self._ssh_options}
385 return namespace
386
jadmanski10646442008-08-13 14:05:21 +0000387
J. Richard Barnettec2d99cf2015-11-18 12:46:15 -0800388 def cleanup(self, labels):
Fang Dengad78aca2014-10-02 18:15:46 -0700389 """Cleanup machines.
390
391 @param labels: Comma separated job labels, will be used to
392 determine special task actions.
393 """
394 if not self.machines:
395 raise error.AutoservError('No machines specified to cleanup')
396 if self.resultdir:
397 os.chdir(self.resultdir)
398
399 namespace = self._make_namespace()
400 namespace.update({'job_labels': labels, 'args': ''})
401 self._execute_code(CLEANUP_CONTROL_FILE, namespace, protect=False)
402
403
J. Richard Barnettec2d99cf2015-11-18 12:46:15 -0800404 def verify(self, labels):
Fang Dengad78aca2014-10-02 18:15:46 -0700405 """Verify machines are all ssh-able.
406
407 @param labels: Comma separated job labels, will be used to
408 determine special task actions.
409 """
jadmanski10646442008-08-13 14:05:21 +0000410 if not self.machines:
mbligh084bc172008-10-18 14:02:45 +0000411 raise error.AutoservError('No machines specified to verify')
mbligh0fce4112008-11-27 00:37:17 +0000412 if self.resultdir:
413 os.chdir(self.resultdir)
Fang Dengad78aca2014-10-02 18:15:46 -0700414
415 namespace = self._make_namespace()
416 namespace.update({'job_labels': labels, 'args': ''})
417 self._execute_code(VERIFY_CONTROL_FILE, namespace, protect=False)
jadmanski10646442008-08-13 14:05:21 +0000418
419
J. Richard Barnettec2d99cf2015-11-18 12:46:15 -0800420 def reset(self, labels):
Fang Dengad78aca2014-10-02 18:15:46 -0700421 """Reset machines by first cleanup then verify each machine.
422
423 @param labels: Comma separated job labels, will be used to
424 determine special task actions.
425 """
Dan Shi07e09af2013-04-12 09:31:29 -0700426 if not self.machines:
427 raise error.AutoservError('No machines specified to reset.')
428 if self.resultdir:
429 os.chdir(self.resultdir)
430
Fang Dengad78aca2014-10-02 18:15:46 -0700431 namespace = self._make_namespace()
432 namespace.update({'job_labels': labels, 'args': ''})
433 self._execute_code(RESET_CONTROL_FILE, namespace, protect=False)
Dan Shi07e09af2013-04-12 09:31:29 -0700434
435
J. Richard Barnettec2d99cf2015-11-18 12:46:15 -0800436 def repair(self, labels):
Fang Dengad78aca2014-10-02 18:15:46 -0700437 """Repair machines.
438
Fang Dengad78aca2014-10-02 18:15:46 -0700439 @param labels: Comma separated job labels, will be used to
440 determine special task actions.
441 """
jadmanski10646442008-08-13 14:05:21 +0000442 if not self.machines:
443 raise error.AutoservError('No machines specified to repair')
mbligh0fce4112008-11-27 00:37:17 +0000444 if self.resultdir:
445 os.chdir(self.resultdir)
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700446
447 namespace = self._make_namespace()
J. Richard Barnettec2d99cf2015-11-18 12:46:15 -0800448 namespace.update({'job_labels': labels, 'args': ''})
mbligh0931b0a2009-04-08 17:44:48 +0000449 self._execute_code(REPAIR_CONTROL_FILE, namespace, protect=False)
jadmanski10646442008-08-13 14:05:21 +0000450
451
Alex Millercb79ba72013-05-29 14:43:00 -0700452 def provision(self, labels):
453 """
454 Provision all hosts to match |labels|.
455
456 @param labels: A comma seperated string of labels to provision the
457 host to.
458
459 """
Alex Millercb79ba72013-05-29 14:43:00 -0700460 control = self._load_control_file(PROVISION_CONTROL_FILE)
Alex Miller686fca82014-04-23 17:21:13 -0700461 self.run(control=control, job_labels=labels)
Alex Millercb79ba72013-05-29 14:43:00 -0700462
463
jadmanski10646442008-08-13 14:05:21 +0000464 def precheck(self):
465 """
466 perform any additional checks in derived classes.
467 """
468 pass
469
470
471 def enable_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000472 """
473 Start or restart external logging mechanism.
jadmanski10646442008-08-13 14:05:21 +0000474 """
475 pass
476
477
478 def disable_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000479 """
480 Pause or stop external logging mechanism.
jadmanski10646442008-08-13 14:05:21 +0000481 """
482 pass
483
484
485 def use_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000486 """
487 Return True if external logging should be used.
jadmanski10646442008-08-13 14:05:21 +0000488 """
489 return False
490
491
mbligh415dc212009-06-15 21:53:34 +0000492 def _make_parallel_wrapper(self, function, machines, log):
493 """Wrap function as appropriate for calling by parallel_simple."""
mbligh2b92b862008-11-22 13:25:32 +0000494 is_forking = not (len(machines) == 1 and self.machines == machines)
mbligh0d0f67d2009-11-06 03:15:03 +0000495 if self._parse_job and is_forking and log:
jadmanski10646442008-08-13 14:05:21 +0000496 def wrapper(machine):
mbligh0d0f67d2009-11-06 03:15:03 +0000497 self._parse_job += "/" + machine
Alex Miller44ae9232014-06-20 17:24:25 -0700498 self._using_parser = INCREMENTAL_TKO_PARSING
jadmanski10646442008-08-13 14:05:21 +0000499 self.machines = [machine]
mbligh0d0f67d2009-11-06 03:15:03 +0000500 self.push_execution_context(machine)
jadmanski609a5f42008-08-26 20:52:42 +0000501 os.chdir(self.resultdir)
showard2bab8f42008-11-12 18:15:22 +0000502 utils.write_keyval(self.resultdir, {"hostname": machine})
mbligh4608b002010-01-05 18:22:35 +0000503 self.init_parser()
jadmanski10646442008-08-13 14:05:21 +0000504 result = function(machine)
505 self.cleanup_parser()
506 return result
jadmanski4dd1a002008-09-05 20:27:30 +0000507 elif len(machines) > 1 and log:
jadmanski10646442008-08-13 14:05:21 +0000508 def wrapper(machine):
mbligh0d0f67d2009-11-06 03:15:03 +0000509 self.push_execution_context(machine)
jadmanski609a5f42008-08-26 20:52:42 +0000510 os.chdir(self.resultdir)
mbligh838d82d2009-03-11 17:14:31 +0000511 machine_data = {'hostname' : machine,
mbligh0d0f67d2009-11-06 03:15:03 +0000512 'status_version' : str(self._STATUS_VERSION)}
mbligh838d82d2009-03-11 17:14:31 +0000513 utils.write_keyval(self.resultdir, machine_data)
jadmanski10646442008-08-13 14:05:21 +0000514 result = function(machine)
515 return result
516 else:
517 wrapper = function
mbligh415dc212009-06-15 21:53:34 +0000518 return wrapper
519
520
521 def parallel_simple(self, function, machines, log=True, timeout=None,
522 return_results=False):
523 """
524 Run 'function' using parallel_simple, with an extra wrapper to handle
525 the necessary setup for continuous parsing, if possible. If continuous
526 parsing is already properly initialized then this should just work.
527
528 @param function: A callable to run in parallel given each machine.
529 @param machines: A list of machine names to be passed one per subcommand
530 invocation of function.
531 @param log: If True, output will be written to output in a subdirectory
532 named after each machine.
533 @param timeout: Seconds after which the function call should timeout.
534 @param return_results: If True instead of an AutoServError being raised
535 on any error a list of the results|exceptions from the function
536 called on each arg is returned. [default: False]
537
538 @raises error.AutotestError: If any of the functions failed.
539 """
540 wrapper = self._make_parallel_wrapper(function, machines, log)
541 return subcommand.parallel_simple(wrapper, machines,
542 log=log, timeout=timeout,
543 return_results=return_results)
544
545
546 def parallel_on_machines(self, function, machines, timeout=None):
547 """
showardcd5fac42009-07-06 20:19:43 +0000548 @param function: Called in parallel with one machine as its argument.
mbligh415dc212009-06-15 21:53:34 +0000549 @param machines: A list of machines to call function(machine) on.
550 @param timeout: Seconds after which the function call should timeout.
551
552 @returns A list of machines on which function(machine) returned
553 without raising an exception.
554 """
showardcd5fac42009-07-06 20:19:43 +0000555 results = self.parallel_simple(function, machines, timeout=timeout,
mbligh415dc212009-06-15 21:53:34 +0000556 return_results=True)
557 success_machines = []
558 for result, machine in itertools.izip(results, machines):
559 if not isinstance(result, Exception):
560 success_machines.append(machine)
561 return success_machines
jadmanski10646442008-08-13 14:05:21 +0000562
563
mbligh0d0f67d2009-11-06 03:15:03 +0000564 _USE_TEMP_DIR = object()
Fang Dengad78aca2014-10-02 18:15:46 -0700565 def run(self, install_before=False, install_after=False,
jadmanskie432dd22009-01-30 15:04:51 +0000566 collect_crashdumps=True, namespace={}, control=None,
beepscb6f1e22013-06-28 19:14:10 -0700567 control_file_dir=None, verify_job_repo_url=False,
Alex Millerca76bcc2014-04-18 18:47:28 -0700568 only_collect_crashinfo=False, skip_crash_collection=False,
Dan Shib669cbd2013-09-13 11:17:17 -0700569 job_labels='', use_packaging=True):
jadmanskifb9c0fa2009-04-29 17:39:16 +0000570 # for a normal job, make sure the uncollected logs file exists
571 # for a crashinfo-only run it should already exist, bail out otherwise
jadmanski648c39f2010-03-19 17:38:01 +0000572 created_uncollected_logs = False
Alex Miller45554f32013-08-13 16:48:29 -0700573 logging.info("I am PID %s", os.getpid())
mbligh0d0f67d2009-11-06 03:15:03 +0000574 if self.resultdir and not os.path.exists(self._uncollected_log_file):
jadmanskifb9c0fa2009-04-29 17:39:16 +0000575 if only_collect_crashinfo:
576 # if this is a crashinfo-only run, and there were no existing
577 # uncollected logs, just bail out early
578 logging.info("No existing uncollected logs, "
579 "skipping crashinfo collection")
580 return
581 else:
mbligh0d0f67d2009-11-06 03:15:03 +0000582 log_file = open(self._uncollected_log_file, "w")
jadmanskifb9c0fa2009-04-29 17:39:16 +0000583 pickle.dump([], log_file)
584 log_file.close()
jadmanski648c39f2010-03-19 17:38:01 +0000585 created_uncollected_logs = True
jadmanskifb9c0fa2009-04-29 17:39:16 +0000586
jadmanski10646442008-08-13 14:05:21 +0000587 # use a copy so changes don't affect the original dictionary
588 namespace = namespace.copy()
589 machines = self.machines
jadmanskie432dd22009-01-30 15:04:51 +0000590 if control is None:
jadmanski02a3ba22009-11-13 20:47:27 +0000591 if self.control is None:
592 control = ''
593 else:
594 control = self._load_control_file(self.control)
jadmanskie432dd22009-01-30 15:04:51 +0000595 if control_file_dir is None:
596 control_file_dir = self.resultdir
jadmanski10646442008-08-13 14:05:21 +0000597
598 self.aborted = False
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700599 namespace.update(self._make_namespace())
Alex Millerca76bcc2014-04-18 18:47:28 -0700600 namespace.update({'args' : self.args,
601 'job_labels' : job_labels})
jadmanski10646442008-08-13 14:05:21 +0000602 test_start_time = int(time.time())
603
mbligh80e1eba2008-11-19 00:26:18 +0000604 if self.resultdir:
605 os.chdir(self.resultdir)
jadmanski779bd292009-03-19 17:33:33 +0000606 # touch status.log so that the parser knows a job is running here
jadmanski382303a2009-04-21 19:53:39 +0000607 open(self.get_status_log_path(), 'a').close()
mbligh80e1eba2008-11-19 00:26:18 +0000608 self.enable_external_logging()
jadmanskie432dd22009-01-30 15:04:51 +0000609
jadmanskicdd0c402008-09-19 21:21:31 +0000610 collect_crashinfo = True
mblighaebe3b62008-12-22 14:45:40 +0000611 temp_control_file_dir = None
jadmanski10646442008-08-13 14:05:21 +0000612 try:
showardcf8d4922009-10-14 16:08:39 +0000613 try:
614 if install_before and machines:
615 self._execute_code(INSTALL_CONTROL_FILE, namespace)
jadmanskie432dd22009-01-30 15:04:51 +0000616
showardcf8d4922009-10-14 16:08:39 +0000617 if only_collect_crashinfo:
618 return
619
beepscb6f1e22013-06-28 19:14:10 -0700620 # If the verify_job_repo_url option is set but we're unable
621 # to actually verify that the job_repo_url contains the autotest
622 # package, this job will fail.
623 if verify_job_repo_url:
624 self._execute_code(VERIFY_JOB_REPO_URL_CONTROL_FILE,
Dan Shicf4d2032015-03-12 15:04:21 -0700625 namespace)
beepscb6f1e22013-06-28 19:14:10 -0700626 else:
627 logging.warning('Not checking if job_repo_url contains '
628 'autotest packages on %s', machines)
629
jadmanskidef0c3c2009-03-25 20:07:10 +0000630 # determine the dir to write the control files to
631 cfd_specified = (control_file_dir
mbligh0d0f67d2009-11-06 03:15:03 +0000632 and control_file_dir is not self._USE_TEMP_DIR)
jadmanskidef0c3c2009-03-25 20:07:10 +0000633 if cfd_specified:
634 temp_control_file_dir = None
635 else:
636 temp_control_file_dir = tempfile.mkdtemp(
637 suffix='temp_control_file_dir')
638 control_file_dir = temp_control_file_dir
639 server_control_file = os.path.join(control_file_dir,
mblighe0cbc912010-03-11 18:03:07 +0000640 self._control_filename)
jadmanskidef0c3c2009-03-25 20:07:10 +0000641 client_control_file = os.path.join(control_file_dir,
642 CLIENT_CONTROL_FILENAME)
mbligh0d0f67d2009-11-06 03:15:03 +0000643 if self._client:
jadmanskidef0c3c2009-03-25 20:07:10 +0000644 namespace['control'] = control
645 utils.open_write_close(client_control_file, control)
mblighfeac0102009-04-28 18:31:12 +0000646 shutil.copyfile(CLIENT_WRAPPER_CONTROL_FILE,
647 server_control_file)
jadmanskidef0c3c2009-03-25 20:07:10 +0000648 else:
649 utils.open_write_close(server_control_file, control)
Dan Shicf4d2032015-03-12 15:04:21 -0700650
mbligh26f0d882009-06-22 18:30:01 +0000651 logging.info("Processing control file")
Dan Shib669cbd2013-09-13 11:17:17 -0700652 namespace['use_packaging'] = use_packaging
Dan Shicf4d2032015-03-12 15:04:21 -0700653 self._execute_code(server_control_file, namespace)
mbligh26f0d882009-06-22 18:30:01 +0000654 logging.info("Finished processing control file")
jadmanski10646442008-08-13 14:05:21 +0000655
Dan Shib03ea9d2013-08-15 17:13:27 -0700656 # If no device error occured, no need to collect crashinfo.
657 collect_crashinfo = self.failed_with_device_error
Eric Li6f27d4f2010-09-29 10:55:17 -0700658 except Exception, e:
showardcf8d4922009-10-14 16:08:39 +0000659 try:
660 logging.exception(
661 'Exception escaped control file, job aborting:')
Eric Li6f27d4f2010-09-29 10:55:17 -0700662 self.record('INFO', None, None, str(e),
663 {'job_abort_reason': str(e)})
showardcf8d4922009-10-14 16:08:39 +0000664 except:
665 pass # don't let logging exceptions here interfere
666 raise
jadmanski10646442008-08-13 14:05:21 +0000667 finally:
mblighaebe3b62008-12-22 14:45:40 +0000668 if temp_control_file_dir:
jadmanskie432dd22009-01-30 15:04:51 +0000669 # Clean up temp directory used for copies of the control files
mblighaebe3b62008-12-22 14:45:40 +0000670 try:
671 shutil.rmtree(temp_control_file_dir)
672 except Exception, e:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700673 logging.warning('Could not remove temp directory %s: %s',
mblighe7d9c602009-07-02 19:02:33 +0000674 temp_control_file_dir, e)
jadmanskie432dd22009-01-30 15:04:51 +0000675
jadmanskicdd0c402008-09-19 21:21:31 +0000676 if machines and (collect_crashdumps or collect_crashinfo):
jadmanski10646442008-08-13 14:05:21 +0000677 namespace['test_start_time'] = test_start_time
Christopher Wileyf594c5e2013-07-03 18:25:30 -0700678 if skip_crash_collection:
679 logging.info('Skipping crash dump/info collection '
680 'as requested.')
681 elif collect_crashinfo:
mbligh084bc172008-10-18 14:02:45 +0000682 # includes crashdumps
683 self._execute_code(CRASHINFO_CONTROL_FILE, namespace)
jadmanskicdd0c402008-09-19 21:21:31 +0000684 else:
mbligh084bc172008-10-18 14:02:45 +0000685 self._execute_code(CRASHDUMPS_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000686 self.disable_external_logging()
Chris Sosaf4d43ff2012-10-30 11:21:05 -0700687 if self._uncollected_log_file and created_uncollected_logs:
688 os.remove(self._uncollected_log_file)
jadmanski10646442008-08-13 14:05:21 +0000689 if install_after and machines:
mbligh084bc172008-10-18 14:02:45 +0000690 self._execute_code(INSTALL_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000691
692
693 def run_test(self, url, *args, **dargs):
mbligh2b92b862008-11-22 13:25:32 +0000694 """
695 Summon a test object and run it.
jadmanski10646442008-08-13 14:05:21 +0000696
697 tag
698 tag to add to testname
699 url
700 url of the test to run
701 """
Christopher Wiley8a91f232013-07-09 11:02:27 -0700702 if self._disable_sysinfo:
703 dargs['disable_sysinfo'] = True
704
mblighfc3da5b2010-01-06 18:37:22 +0000705 group, testname = self.pkgmgr.get_package_name(url, 'test')
706 testname, subdir, tag = self._build_tagged_test_name(testname, dargs)
707 outputdir = self._make_test_outputdir(subdir)
jadmanski10646442008-08-13 14:05:21 +0000708
709 def group_func():
710 try:
711 test.runtest(self, url, tag, args, dargs)
712 except error.TestBaseException, e:
713 self.record(e.exit_status, subdir, testname, str(e))
714 raise
715 except Exception, e:
716 info = str(e) + "\n" + traceback.format_exc()
717 self.record('FAIL', subdir, testname, info)
718 raise
719 else:
mbligh2b92b862008-11-22 13:25:32 +0000720 self.record('GOOD', subdir, testname, 'completed successfully')
jadmanskide292df2008-08-26 20:51:14 +0000721
722 result, exc_info = self._run_group(testname, subdir, group_func)
723 if exc_info and isinstance(exc_info[1], error.TestBaseException):
724 return False
725 elif exc_info:
726 raise exc_info[0], exc_info[1], exc_info[2]
727 else:
728 return True
jadmanski10646442008-08-13 14:05:21 +0000729
730
731 def _run_group(self, name, subdir, function, *args, **dargs):
732 """\
733 Underlying method for running something inside of a group.
734 """
jadmanskide292df2008-08-26 20:51:14 +0000735 result, exc_info = None, None
jadmanski10646442008-08-13 14:05:21 +0000736 try:
737 self.record('START', subdir, name)
jadmanski52053632010-06-11 21:08:10 +0000738 result = function(*args, **dargs)
jadmanski10646442008-08-13 14:05:21 +0000739 except error.TestBaseException, e:
jadmanskib88d6dc2009-01-10 00:33:18 +0000740 self.record("END %s" % e.exit_status, subdir, name)
jadmanskide292df2008-08-26 20:51:14 +0000741 exc_info = sys.exc_info()
jadmanski10646442008-08-13 14:05:21 +0000742 except Exception, e:
743 err_msg = str(e) + '\n'
744 err_msg += traceback.format_exc()
745 self.record('END ABORT', subdir, name, err_msg)
746 raise error.JobError(name + ' failed\n' + traceback.format_exc())
747 else:
748 self.record('END GOOD', subdir, name)
749
jadmanskide292df2008-08-26 20:51:14 +0000750 return result, exc_info
jadmanski10646442008-08-13 14:05:21 +0000751
752
753 def run_group(self, function, *args, **dargs):
754 """\
755 function:
756 subroutine to run
757 *args:
758 arguments for the function
759 """
760
761 name = function.__name__
762
763 # Allow the tag for the group to be specified.
764 tag = dargs.pop('tag', None)
765 if tag:
766 name = tag
767
jadmanskide292df2008-08-26 20:51:14 +0000768 return self._run_group(name, None, function, *args, **dargs)[0]
jadmanski10646442008-08-13 14:05:21 +0000769
770
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700771 def run_op(self, op, op_func, get_kernel_func):
jadmanski10646442008-08-13 14:05:21 +0000772 """\
773 A specialization of run_group meant specifically for handling
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700774 management operation. Includes support for capturing the kernel version
775 after the operation.
jadmanski10646442008-08-13 14:05:21 +0000776
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700777 Args:
778 op: name of the operation.
779 op_func: a function that carries out the operation (reboot, suspend)
780 get_kernel_func: a function that returns a string
781 representing the kernel version.
jadmanski10646442008-08-13 14:05:21 +0000782 """
jadmanski10646442008-08-13 14:05:21 +0000783 try:
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700784 self.record('START', None, op)
785 op_func()
jadmanski10646442008-08-13 14:05:21 +0000786 except Exception, e:
jadmanski10646442008-08-13 14:05:21 +0000787 err_msg = str(e) + '\n' + traceback.format_exc()
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700788 self.record('END FAIL', None, op, err_msg)
jadmanski4b51d542009-04-08 14:17:16 +0000789 raise
jadmanski10646442008-08-13 14:05:21 +0000790 else:
791 kernel = get_kernel_func()
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700792 self.record('END GOOD', None, op,
Dale Curtis74a314b2011-06-23 14:55:46 -0700793 optional_fields={"kernel": kernel})
jadmanski10646442008-08-13 14:05:21 +0000794
795
jadmanskie432dd22009-01-30 15:04:51 +0000796 def run_control(self, path):
797 """Execute a control file found at path (relative to the autotest
798 path). Intended for executing a control file within a control file,
799 not for running the top-level job control file."""
800 path = os.path.join(self.autodir, path)
801 control_file = self._load_control_file(path)
mbligh0d0f67d2009-11-06 03:15:03 +0000802 self.run(control=control_file, control_file_dir=self._USE_TEMP_DIR)
jadmanskie432dd22009-01-30 15:04:51 +0000803
804
jadmanskic09fc152008-10-15 17:56:59 +0000805 def add_sysinfo_command(self, command, logfile=None, on_every_test=False):
mbligh4395bbd2009-03-25 19:34:17 +0000806 self._add_sysinfo_loggable(sysinfo.command(command, logf=logfile),
jadmanskic09fc152008-10-15 17:56:59 +0000807 on_every_test)
808
809
810 def add_sysinfo_logfile(self, file, on_every_test=False):
811 self._add_sysinfo_loggable(sysinfo.logfile(file), on_every_test)
812
813
814 def _add_sysinfo_loggable(self, loggable, on_every_test):
815 if on_every_test:
816 self.sysinfo.test_loggables.add(loggable)
817 else:
818 self.sysinfo.boot_loggables.add(loggable)
819
820
jadmanski10646442008-08-13 14:05:21 +0000821 def _read_warnings(self):
jadmanskif37df842009-02-11 00:03:26 +0000822 """Poll all the warning loggers and extract any new warnings that have
823 been logged. If the warnings belong to a category that is currently
824 disabled, this method will discard them and they will no longer be
825 retrievable.
826
827 Returns a list of (timestamp, message) tuples, where timestamp is an
828 integer epoch timestamp."""
jadmanski10646442008-08-13 14:05:21 +0000829 warnings = []
830 while True:
831 # pull in a line of output from every logger that has
832 # output ready to be read
mbligh2b92b862008-11-22 13:25:32 +0000833 loggers, _, _ = select.select(self.warning_loggers, [], [], 0)
jadmanski10646442008-08-13 14:05:21 +0000834 closed_loggers = set()
835 for logger in loggers:
836 line = logger.readline()
837 # record any broken pipes (aka line == empty)
838 if len(line) == 0:
839 closed_loggers.add(logger)
840 continue
jadmanskif37df842009-02-11 00:03:26 +0000841 # parse out the warning
842 timestamp, msgtype, msg = line.split('\t', 2)
843 timestamp = int(timestamp)
844 # if the warning is valid, add it to the results
845 if self.warning_manager.is_valid(timestamp, msgtype):
846 warnings.append((timestamp, msg.strip()))
jadmanski10646442008-08-13 14:05:21 +0000847
848 # stop listening to loggers that are closed
849 self.warning_loggers -= closed_loggers
850
851 # stop if none of the loggers have any output left
852 if not loggers:
853 break
854
855 # sort into timestamp order
856 warnings.sort()
857 return warnings
858
859
showardcc929362010-01-25 21:20:41 +0000860 def _unique_subdirectory(self, base_subdirectory_name):
861 """Compute a unique results subdirectory based on the given name.
862
863 Appends base_subdirectory_name with a number as necessary to find a
864 directory name that doesn't already exist.
865 """
866 subdirectory = base_subdirectory_name
867 counter = 1
868 while os.path.exists(os.path.join(self.resultdir, subdirectory)):
869 subdirectory = base_subdirectory_name + '.' + str(counter)
870 counter += 1
871 return subdirectory
872
873
jadmanski52053632010-06-11 21:08:10 +0000874 def get_record_context(self):
875 """Returns an object representing the current job.record context.
876
877 The object returned is an opaque object with a 0-arg restore method
878 which can be called to restore the job.record context (i.e. indentation)
879 to the current level. The intention is that it should be used when
880 something external which generate job.record calls (e.g. an autotest
881 client) can fail catastrophically and the server job record state
882 needs to be reset to its original "known good" state.
883
884 @return: A context object with a 0-arg restore() method."""
885 return self._indenter.get_context()
886
887
showardcc929362010-01-25 21:20:41 +0000888 def record_summary(self, status_code, test_name, reason='', attributes=None,
889 distinguishing_attributes=(), child_test_ids=None):
890 """Record a summary test result.
891
892 @param status_code: status code string, see
893 common_lib.log.is_valid_status()
894 @param test_name: name of the test
895 @param reason: (optional) string providing detailed reason for test
896 outcome
897 @param attributes: (optional) dict of string keyvals to associate with
898 this result
899 @param distinguishing_attributes: (optional) list of attribute names
900 that should be used to distinguish identically-named test
901 results. These attributes should be present in the attributes
902 parameter. This is used to generate user-friendly subdirectory
903 names.
904 @param child_test_ids: (optional) list of test indices for test results
905 used in generating this result.
906 """
907 subdirectory_name_parts = [test_name]
908 for attribute in distinguishing_attributes:
909 assert attributes
910 assert attribute in attributes, '%s not in %s' % (attribute,
911 attributes)
912 subdirectory_name_parts.append(attributes[attribute])
913 base_subdirectory_name = '.'.join(subdirectory_name_parts)
914
915 subdirectory = self._unique_subdirectory(base_subdirectory_name)
916 subdirectory_path = os.path.join(self.resultdir, subdirectory)
917 os.mkdir(subdirectory_path)
918
919 self.record(status_code, subdirectory, test_name,
920 status=reason, optional_fields={'is_summary': True})
921
922 if attributes:
923 utils.write_keyval(subdirectory_path, attributes)
924
925 if child_test_ids:
926 ids_string = ','.join(str(test_id) for test_id in child_test_ids)
927 summary_data = {'child_test_ids': ids_string}
928 utils.write_keyval(os.path.join(subdirectory_path, 'summary_data'),
929 summary_data)
930
931
jadmanski16a7ff72009-04-01 18:19:53 +0000932 def disable_warnings(self, warning_type):
jadmanskif37df842009-02-11 00:03:26 +0000933 self.warning_manager.disable_warnings(warning_type)
jadmanski16a7ff72009-04-01 18:19:53 +0000934 self.record("INFO", None, None,
935 "disabling %s warnings" % warning_type,
936 {"warnings.disable": warning_type})
jadmanskif37df842009-02-11 00:03:26 +0000937
938
jadmanski16a7ff72009-04-01 18:19:53 +0000939 def enable_warnings(self, warning_type):
jadmanskif37df842009-02-11 00:03:26 +0000940 self.warning_manager.enable_warnings(warning_type)
jadmanski16a7ff72009-04-01 18:19:53 +0000941 self.record("INFO", None, None,
942 "enabling %s warnings" % warning_type,
943 {"warnings.enable": warning_type})
jadmanskif37df842009-02-11 00:03:26 +0000944
945
jadmanski779bd292009-03-19 17:33:33 +0000946 def get_status_log_path(self, subdir=None):
947 """Return the path to the job status log.
948
949 @param subdir - Optional paramter indicating that you want the path
950 to a subdirectory status log.
951
952 @returns The path where the status log should be.
953 """
mbligh210bae62009-04-01 18:33:13 +0000954 if self.resultdir:
955 if subdir:
956 return os.path.join(self.resultdir, subdir, "status.log")
957 else:
958 return os.path.join(self.resultdir, "status.log")
jadmanski779bd292009-03-19 17:33:33 +0000959 else:
mbligh210bae62009-04-01 18:33:13 +0000960 return None
jadmanski779bd292009-03-19 17:33:33 +0000961
962
jadmanski6bb32d72009-03-19 20:25:24 +0000963 def _update_uncollected_logs_list(self, update_func):
964 """Updates the uncollected logs list in a multi-process safe manner.
965
966 @param update_func - a function that updates the list of uncollected
967 logs. Should take one parameter, the list to be updated.
968 """
Dan Shi07e09af2013-04-12 09:31:29 -0700969 # Skip log collection if file _uncollected_log_file does not exist.
970 if not (self._uncollected_log_file and
971 os.path.exists(self._uncollected_log_file)):
972 return
mbligh0d0f67d2009-11-06 03:15:03 +0000973 if self._uncollected_log_file:
974 log_file = open(self._uncollected_log_file, "r+")
mbligha788dc42009-03-26 21:10:16 +0000975 fcntl.flock(log_file, fcntl.LOCK_EX)
jadmanski6bb32d72009-03-19 20:25:24 +0000976 try:
977 uncollected_logs = pickle.load(log_file)
978 update_func(uncollected_logs)
979 log_file.seek(0)
980 log_file.truncate()
981 pickle.dump(uncollected_logs, log_file)
jadmanski3bff9092009-04-22 18:09:47 +0000982 log_file.flush()
jadmanski6bb32d72009-03-19 20:25:24 +0000983 finally:
984 fcntl.flock(log_file, fcntl.LOCK_UN)
985 log_file.close()
986
987
988 def add_client_log(self, hostname, remote_path, local_path):
989 """Adds a new set of client logs to the list of uncollected logs,
990 to allow for future log recovery.
991
992 @param host - the hostname of the machine holding the logs
993 @param remote_path - the directory on the remote machine holding logs
994 @param local_path - the local directory to copy the logs into
995 """
996 def update_func(logs_list):
997 logs_list.append((hostname, remote_path, local_path))
998 self._update_uncollected_logs_list(update_func)
999
1000
1001 def remove_client_log(self, hostname, remote_path, local_path):
1002 """Removes a set of client logs from the list of uncollected logs,
1003 to allow for future log recovery.
1004
1005 @param host - the hostname of the machine holding the logs
1006 @param remote_path - the directory on the remote machine holding logs
1007 @param local_path - the local directory to copy the logs into
1008 """
1009 def update_func(logs_list):
1010 logs_list.remove((hostname, remote_path, local_path))
1011 self._update_uncollected_logs_list(update_func)
1012
1013
mbligh0d0f67d2009-11-06 03:15:03 +00001014 def get_client_logs(self):
1015 """Retrieves the list of uncollected logs, if it exists.
1016
1017 @returns A list of (host, remote_path, local_path) tuples. Returns
1018 an empty list if no uncollected logs file exists.
1019 """
1020 log_exists = (self._uncollected_log_file and
1021 os.path.exists(self._uncollected_log_file))
1022 if log_exists:
1023 return pickle.load(open(self._uncollected_log_file))
1024 else:
1025 return []
1026
1027
mbligh084bc172008-10-18 14:02:45 +00001028 def _fill_server_control_namespace(self, namespace, protect=True):
mbligh2b92b862008-11-22 13:25:32 +00001029 """
1030 Prepare a namespace to be used when executing server control files.
mbligh084bc172008-10-18 14:02:45 +00001031
1032 This sets up the control file API by importing modules and making them
1033 available under the appropriate names within namespace.
1034
1035 For use by _execute_code().
1036
1037 Args:
1038 namespace: The namespace dictionary to fill in.
1039 protect: Boolean. If True (the default) any operation that would
1040 clobber an existing entry in namespace will cause an error.
1041 Raises:
1042 error.AutoservError: When a name would be clobbered by import.
1043 """
1044 def _import_names(module_name, names=()):
mbligh2b92b862008-11-22 13:25:32 +00001045 """
1046 Import a module and assign named attributes into namespace.
mbligh084bc172008-10-18 14:02:45 +00001047
1048 Args:
1049 module_name: The string module name.
1050 names: A limiting list of names to import from module_name. If
1051 empty (the default), all names are imported from the module
1052 similar to a "from foo.bar import *" statement.
1053 Raises:
1054 error.AutoservError: When a name being imported would clobber
1055 a name already in namespace.
1056 """
1057 module = __import__(module_name, {}, {}, names)
1058
1059 # No names supplied? Import * from the lowest level module.
1060 # (Ugh, why do I have to implement this part myself?)
1061 if not names:
1062 for submodule_name in module_name.split('.')[1:]:
1063 module = getattr(module, submodule_name)
1064 if hasattr(module, '__all__'):
1065 names = getattr(module, '__all__')
1066 else:
1067 names = dir(module)
1068
1069 # Install each name into namespace, checking to make sure it
1070 # doesn't override anything that already exists.
1071 for name in names:
1072 # Check for conflicts to help prevent future problems.
1073 if name in namespace and protect:
1074 if namespace[name] is not getattr(module, name):
1075 raise error.AutoservError('importing name '
1076 '%s from %s %r would override %r' %
1077 (name, module_name, getattr(module, name),
1078 namespace[name]))
1079 else:
1080 # Encourage cleanliness and the use of __all__ for a
1081 # more concrete API with less surprises on '*' imports.
1082 warnings.warn('%s (%r) being imported from %s for use '
1083 'in server control files is not the '
1084 'first occurrance of that import.' %
1085 (name, namespace[name], module_name))
1086
1087 namespace[name] = getattr(module, name)
1088
1089
1090 # This is the equivalent of prepending a bunch of import statements to
1091 # the front of the control script.
mbligha2b07dd2009-06-22 18:26:13 +00001092 namespace.update(os=os, sys=sys, logging=logging)
mbligh084bc172008-10-18 14:02:45 +00001093 _import_names('autotest_lib.server',
1094 ('hosts', 'autotest', 'kvm', 'git', 'standalone_profiler',
1095 'source_kernel', 'rpm_kernel', 'deb_kernel', 'git_kernel'))
1096 _import_names('autotest_lib.server.subcommand',
1097 ('parallel', 'parallel_simple', 'subcommand'))
1098 _import_names('autotest_lib.server.utils',
1099 ('run', 'get_tmp_dir', 'sh_escape', 'parse_machine'))
1100 _import_names('autotest_lib.client.common_lib.error')
1101 _import_names('autotest_lib.client.common_lib.barrier', ('barrier',))
1102
1103 # Inject ourself as the job object into other classes within the API.
1104 # (Yuck, this injection is a gross thing be part of a public API. -gps)
1105 #
1106 # XXX Base & SiteAutotest do not appear to use .job. Who does?
1107 namespace['autotest'].Autotest.job = self
1108 # server.hosts.base_classes.Host uses .job.
1109 namespace['hosts'].Host.job = self
Eric Li10222b82010-11-24 09:33:15 -08001110 namespace['hosts'].factory.ssh_user = self._ssh_user
1111 namespace['hosts'].factory.ssh_port = self._ssh_port
1112 namespace['hosts'].factory.ssh_pass = self._ssh_pass
Fang Dengd1c2b732013-08-20 12:59:46 -07001113 namespace['hosts'].factory.ssh_verbosity_flag = (
1114 self._ssh_verbosity_flag)
Aviv Keshetc5947fa2013-09-04 14:06:29 -07001115 namespace['hosts'].factory.ssh_options = self._ssh_options
mbligh084bc172008-10-18 14:02:45 +00001116
1117
1118 def _execute_code(self, code_file, namespace, protect=True):
mbligh2b92b862008-11-22 13:25:32 +00001119 """
1120 Execute code using a copy of namespace as a server control script.
mbligh084bc172008-10-18 14:02:45 +00001121
1122 Unless protect_namespace is explicitly set to False, the dict will not
1123 be modified.
1124
1125 Args:
1126 code_file: The filename of the control file to execute.
1127 namespace: A dict containing names to make available during execution.
1128 protect: Boolean. If True (the default) a copy of the namespace dict
1129 is used during execution to prevent the code from modifying its
1130 contents outside of this function. If False the raw dict is
1131 passed in and modifications will be allowed.
1132 """
1133 if protect:
1134 namespace = namespace.copy()
1135 self._fill_server_control_namespace(namespace, protect=protect)
1136 # TODO: Simplify and get rid of the special cases for only 1 machine.
showard3e66e8c2008-10-27 19:20:51 +00001137 if len(self.machines) > 1:
mbligh084bc172008-10-18 14:02:45 +00001138 machines_text = '\n'.join(self.machines) + '\n'
1139 # Only rewrite the file if it does not match our machine list.
1140 try:
1141 machines_f = open(MACHINES_FILENAME, 'r')
1142 existing_machines_text = machines_f.read()
1143 machines_f.close()
1144 except EnvironmentError:
1145 existing_machines_text = None
1146 if machines_text != existing_machines_text:
1147 utils.open_write_close(MACHINES_FILENAME, machines_text)
1148 execfile(code_file, namespace, namespace)
jadmanski10646442008-08-13 14:05:21 +00001149
1150
jadmanskie29d0e42010-06-17 16:06:52 +00001151 def _parse_status(self, new_line):
mbligh0d0f67d2009-11-06 03:15:03 +00001152 if not self._using_parser:
jadmanski10646442008-08-13 14:05:21 +00001153 return
jadmanskie29d0e42010-06-17 16:06:52 +00001154 new_tests = self.parser.process_lines([new_line])
jadmanski10646442008-08-13 14:05:21 +00001155 for test in new_tests:
1156 self.__insert_test(test)
1157
1158
1159 def __insert_test(self, test):
mbligh2b92b862008-11-22 13:25:32 +00001160 """
1161 An internal method to insert a new test result into the
jadmanski10646442008-08-13 14:05:21 +00001162 database. This method will not raise an exception, even if an
1163 error occurs during the insert, to avoid failing a test
1164 simply because of unexpected database issues."""
showard21baa452008-10-21 00:08:39 +00001165 self.num_tests_run += 1
1166 if status_lib.is_worse_than_or_equal_to(test.status, 'FAIL'):
1167 self.num_tests_failed += 1
jadmanski10646442008-08-13 14:05:21 +00001168 try:
1169 self.results_db.insert_test(self.job_model, test)
1170 except Exception:
1171 msg = ("WARNING: An unexpected error occured while "
1172 "inserting test results into the database. "
1173 "Ignoring error.\n" + traceback.format_exc())
1174 print >> sys.stderr, msg
1175
mblighcaa62c22008-04-07 21:51:17 +00001176
mblighfc3da5b2010-01-06 18:37:22 +00001177 def preprocess_client_state(self):
1178 """
1179 Produce a state file for initializing the state of a client job.
1180
1181 Creates a new client state file with all the current server state, as
1182 well as some pre-set client state.
1183
1184 @returns The path of the file the state was written into.
1185 """
1186 # initialize the sysinfo state
1187 self._state.set('client', 'sysinfo', self.sysinfo.serialize())
1188
1189 # dump the state out to a tempfile
1190 fd, file_path = tempfile.mkstemp(dir=self.tmpdir)
1191 os.close(fd)
mbligha2c99492010-01-27 22:59:50 +00001192
1193 # write_to_file doesn't need locking, we exclusively own file_path
mblighfc3da5b2010-01-06 18:37:22 +00001194 self._state.write_to_file(file_path)
1195 return file_path
1196
1197
1198 def postprocess_client_state(self, state_path):
1199 """
1200 Update the state of this job with the state from a client job.
1201
1202 Updates the state of the server side of a job with the final state
1203 of a client job that was run. Updates the non-client-specific state,
1204 pulls in some specific bits from the client-specific state, and then
1205 discards the rest. Removes the state file afterwards
1206
1207 @param state_file A path to the state file from the client.
1208 """
1209 # update the on-disk state
mblighfc3da5b2010-01-06 18:37:22 +00001210 try:
jadmanskib6e7bdb2010-04-13 16:00:39 +00001211 self._state.read_from_file(state_path)
mblighfc3da5b2010-01-06 18:37:22 +00001212 os.remove(state_path)
mbligha2c99492010-01-27 22:59:50 +00001213 except OSError, e:
mblighfc3da5b2010-01-06 18:37:22 +00001214 # ignore file-not-found errors
1215 if e.errno != errno.ENOENT:
1216 raise
jadmanskib6e7bdb2010-04-13 16:00:39 +00001217 else:
1218 logging.debug('Client state file %s not found', state_path)
mblighfc3da5b2010-01-06 18:37:22 +00001219
1220 # update the sysinfo state
1221 if self._state.has('client', 'sysinfo'):
1222 self.sysinfo.deserialize(self._state.get('client', 'sysinfo'))
1223
1224 # drop all the client-specific state
1225 self._state.discard_namespace('client')
1226
1227
mbligh0a883702010-04-21 01:58:34 +00001228 def clear_all_known_hosts(self):
1229 """Clears known hosts files for all AbstractSSHHosts."""
1230 for host in self.hosts:
1231 if isinstance(host, abstract_ssh.AbstractSSHHost):
1232 host.clear_known_hosts()
1233
1234
jadmanskif37df842009-02-11 00:03:26 +00001235class warning_manager(object):
1236 """Class for controlling warning logs. Manages the enabling and disabling
1237 of warnings."""
1238 def __init__(self):
1239 # a map of warning types to a list of disabled time intervals
1240 self.disabled_warnings = {}
1241
1242
1243 def is_valid(self, timestamp, warning_type):
1244 """Indicates if a warning (based on the time it occured and its type)
1245 is a valid warning. A warning is considered "invalid" if this type of
1246 warning was marked as "disabled" at the time the warning occured."""
1247 disabled_intervals = self.disabled_warnings.get(warning_type, [])
1248 for start, end in disabled_intervals:
1249 if timestamp >= start and (end is None or timestamp < end):
1250 return False
1251 return True
1252
1253
1254 def disable_warnings(self, warning_type, current_time_func=time.time):
1255 """As of now, disables all further warnings of this type."""
1256 intervals = self.disabled_warnings.setdefault(warning_type, [])
1257 if not intervals or intervals[-1][1] is not None:
jadmanski16a7ff72009-04-01 18:19:53 +00001258 intervals.append((int(current_time_func()), None))
jadmanskif37df842009-02-11 00:03:26 +00001259
1260
1261 def enable_warnings(self, warning_type, current_time_func=time.time):
1262 """As of now, enables all further warnings of this type."""
1263 intervals = self.disabled_warnings.get(warning_type, [])
1264 if intervals and intervals[-1][1] is None:
jadmanski16a7ff72009-04-01 18:19:53 +00001265 intervals[-1] = (intervals[-1][0], int(current_time_func()))
Paul Pendlebury57593562011-06-15 10:45:49 -07001266
1267
1268# load up site-specific code for generating site-specific job data
1269get_site_job_data = utils.import_site_function(__file__,
1270 "autotest_lib.server.site_server_job", "get_site_job_data",
1271 _get_site_job_data_dummy)
1272
1273
1274site_server_job = utils.import_site_class(
1275 __file__, "autotest_lib.server.site_server_job", "site_server_job",
1276 base_server_job)
1277
1278
1279class server_job(site_server_job):
Dale Curtis456d3c12011-07-19 11:42:51 -07001280 pass