blob: 45ff8900aff03210f07da35e9c9d9224c925f49d [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
Fang Dengad78aca2014-10-02 18:15:46 -0700388 def cleanup(self, labels=''):
389 """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
Alex Miller667b5f22014-02-28 15:33:39 -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
Alex Miller667b5f22014-02-28 15:33:39 -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
Alex Miller667b5f22014-02-28 15:33:39 -0800436 def repair(self, host_protection, labels=''):
Fang Dengad78aca2014-10-02 18:15:46 -0700437 """Repair machines.
438
439 @param host_protection: level of host protection.
440 @param labels: Comma separated job labels, will be used to
441 determine special task actions.
442 """
jadmanski10646442008-08-13 14:05:21 +0000443 if not self.machines:
444 raise error.AutoservError('No machines specified to repair')
mbligh0fce4112008-11-27 00:37:17 +0000445 if self.resultdir:
446 os.chdir(self.resultdir)
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700447
448 namespace = self._make_namespace()
Alex Miller667b5f22014-02-28 15:33:39 -0800449 namespace.update({'protection_level' : host_protection,
450 'job_labels': labels, 'args': ''})
mbligh0931b0a2009-04-08 17:44:48 +0000451 self._execute_code(REPAIR_CONTROL_FILE, namespace, protect=False)
jadmanski10646442008-08-13 14:05:21 +0000452
453
Alex Millercb79ba72013-05-29 14:43:00 -0700454 def provision(self, labels):
455 """
456 Provision all hosts to match |labels|.
457
458 @param labels: A comma seperated string of labels to provision the
459 host to.
460
461 """
Alex Millercb79ba72013-05-29 14:43:00 -0700462 control = self._load_control_file(PROVISION_CONTROL_FILE)
Alex Miller686fca82014-04-23 17:21:13 -0700463 self.run(control=control, job_labels=labels)
Alex Millercb79ba72013-05-29 14:43:00 -0700464
465
jadmanski10646442008-08-13 14:05:21 +0000466 def precheck(self):
467 """
468 perform any additional checks in derived classes.
469 """
470 pass
471
472
473 def enable_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000474 """
475 Start or restart external logging mechanism.
jadmanski10646442008-08-13 14:05:21 +0000476 """
477 pass
478
479
480 def disable_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000481 """
482 Pause or stop external logging mechanism.
jadmanski10646442008-08-13 14:05:21 +0000483 """
484 pass
485
486
487 def use_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000488 """
489 Return True if external logging should be used.
jadmanski10646442008-08-13 14:05:21 +0000490 """
491 return False
492
493
mbligh415dc212009-06-15 21:53:34 +0000494 def _make_parallel_wrapper(self, function, machines, log):
495 """Wrap function as appropriate for calling by parallel_simple."""
mbligh2b92b862008-11-22 13:25:32 +0000496 is_forking = not (len(machines) == 1 and self.machines == machines)
mbligh0d0f67d2009-11-06 03:15:03 +0000497 if self._parse_job and is_forking and log:
jadmanski10646442008-08-13 14:05:21 +0000498 def wrapper(machine):
mbligh0d0f67d2009-11-06 03:15:03 +0000499 self._parse_job += "/" + machine
Alex Miller44ae9232014-06-20 17:24:25 -0700500 self._using_parser = INCREMENTAL_TKO_PARSING
jadmanski10646442008-08-13 14:05:21 +0000501 self.machines = [machine]
mbligh0d0f67d2009-11-06 03:15:03 +0000502 self.push_execution_context(machine)
jadmanski609a5f42008-08-26 20:52:42 +0000503 os.chdir(self.resultdir)
showard2bab8f42008-11-12 18:15:22 +0000504 utils.write_keyval(self.resultdir, {"hostname": machine})
mbligh4608b002010-01-05 18:22:35 +0000505 self.init_parser()
jadmanski10646442008-08-13 14:05:21 +0000506 result = function(machine)
507 self.cleanup_parser()
508 return result
jadmanski4dd1a002008-09-05 20:27:30 +0000509 elif len(machines) > 1 and log:
jadmanski10646442008-08-13 14:05:21 +0000510 def wrapper(machine):
mbligh0d0f67d2009-11-06 03:15:03 +0000511 self.push_execution_context(machine)
jadmanski609a5f42008-08-26 20:52:42 +0000512 os.chdir(self.resultdir)
mbligh838d82d2009-03-11 17:14:31 +0000513 machine_data = {'hostname' : machine,
mbligh0d0f67d2009-11-06 03:15:03 +0000514 'status_version' : str(self._STATUS_VERSION)}
mbligh838d82d2009-03-11 17:14:31 +0000515 utils.write_keyval(self.resultdir, machine_data)
jadmanski10646442008-08-13 14:05:21 +0000516 result = function(machine)
517 return result
518 else:
519 wrapper = function
mbligh415dc212009-06-15 21:53:34 +0000520 return wrapper
521
522
523 def parallel_simple(self, function, machines, log=True, timeout=None,
524 return_results=False):
525 """
526 Run 'function' using parallel_simple, with an extra wrapper to handle
527 the necessary setup for continuous parsing, if possible. If continuous
528 parsing is already properly initialized then this should just work.
529
530 @param function: A callable to run in parallel given each machine.
531 @param machines: A list of machine names to be passed one per subcommand
532 invocation of function.
533 @param log: If True, output will be written to output in a subdirectory
534 named after each machine.
535 @param timeout: Seconds after which the function call should timeout.
536 @param return_results: If True instead of an AutoServError being raised
537 on any error a list of the results|exceptions from the function
538 called on each arg is returned. [default: False]
539
540 @raises error.AutotestError: If any of the functions failed.
541 """
542 wrapper = self._make_parallel_wrapper(function, machines, log)
543 return subcommand.parallel_simple(wrapper, machines,
544 log=log, timeout=timeout,
545 return_results=return_results)
546
547
548 def parallel_on_machines(self, function, machines, timeout=None):
549 """
showardcd5fac42009-07-06 20:19:43 +0000550 @param function: Called in parallel with one machine as its argument.
mbligh415dc212009-06-15 21:53:34 +0000551 @param machines: A list of machines to call function(machine) on.
552 @param timeout: Seconds after which the function call should timeout.
553
554 @returns A list of machines on which function(machine) returned
555 without raising an exception.
556 """
showardcd5fac42009-07-06 20:19:43 +0000557 results = self.parallel_simple(function, machines, timeout=timeout,
mbligh415dc212009-06-15 21:53:34 +0000558 return_results=True)
559 success_machines = []
560 for result, machine in itertools.izip(results, machines):
561 if not isinstance(result, Exception):
562 success_machines.append(machine)
563 return success_machines
jadmanski10646442008-08-13 14:05:21 +0000564
565
mbligh0d0f67d2009-11-06 03:15:03 +0000566 _USE_TEMP_DIR = object()
Fang Dengad78aca2014-10-02 18:15:46 -0700567 def run(self, install_before=False, install_after=False,
jadmanskie432dd22009-01-30 15:04:51 +0000568 collect_crashdumps=True, namespace={}, control=None,
beepscb6f1e22013-06-28 19:14:10 -0700569 control_file_dir=None, verify_job_repo_url=False,
Alex Millerca76bcc2014-04-18 18:47:28 -0700570 only_collect_crashinfo=False, skip_crash_collection=False,
Dan Shib669cbd2013-09-13 11:17:17 -0700571 job_labels='', use_packaging=True):
jadmanskifb9c0fa2009-04-29 17:39:16 +0000572 # for a normal job, make sure the uncollected logs file exists
573 # for a crashinfo-only run it should already exist, bail out otherwise
jadmanski648c39f2010-03-19 17:38:01 +0000574 created_uncollected_logs = False
Alex Miller45554f32013-08-13 16:48:29 -0700575 logging.info("I am PID %s", os.getpid())
mbligh0d0f67d2009-11-06 03:15:03 +0000576 if self.resultdir and not os.path.exists(self._uncollected_log_file):
jadmanskifb9c0fa2009-04-29 17:39:16 +0000577 if only_collect_crashinfo:
578 # if this is a crashinfo-only run, and there were no existing
579 # uncollected logs, just bail out early
580 logging.info("No existing uncollected logs, "
581 "skipping crashinfo collection")
582 return
583 else:
mbligh0d0f67d2009-11-06 03:15:03 +0000584 log_file = open(self._uncollected_log_file, "w")
jadmanskifb9c0fa2009-04-29 17:39:16 +0000585 pickle.dump([], log_file)
586 log_file.close()
jadmanski648c39f2010-03-19 17:38:01 +0000587 created_uncollected_logs = True
jadmanskifb9c0fa2009-04-29 17:39:16 +0000588
jadmanski10646442008-08-13 14:05:21 +0000589 # use a copy so changes don't affect the original dictionary
590 namespace = namespace.copy()
591 machines = self.machines
jadmanskie432dd22009-01-30 15:04:51 +0000592 if control is None:
jadmanski02a3ba22009-11-13 20:47:27 +0000593 if self.control is None:
594 control = ''
595 else:
596 control = self._load_control_file(self.control)
jadmanskie432dd22009-01-30 15:04:51 +0000597 if control_file_dir is None:
598 control_file_dir = self.resultdir
jadmanski10646442008-08-13 14:05:21 +0000599
600 self.aborted = False
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700601 namespace.update(self._make_namespace())
Alex Millerca76bcc2014-04-18 18:47:28 -0700602 namespace.update({'args' : self.args,
603 'job_labels' : job_labels})
jadmanski10646442008-08-13 14:05:21 +0000604 test_start_time = int(time.time())
605
mbligh80e1eba2008-11-19 00:26:18 +0000606 if self.resultdir:
607 os.chdir(self.resultdir)
jadmanski779bd292009-03-19 17:33:33 +0000608 # touch status.log so that the parser knows a job is running here
jadmanski382303a2009-04-21 19:53:39 +0000609 open(self.get_status_log_path(), 'a').close()
mbligh80e1eba2008-11-19 00:26:18 +0000610 self.enable_external_logging()
jadmanskie432dd22009-01-30 15:04:51 +0000611
jadmanskicdd0c402008-09-19 21:21:31 +0000612 collect_crashinfo = True
mblighaebe3b62008-12-22 14:45:40 +0000613 temp_control_file_dir = None
jadmanski10646442008-08-13 14:05:21 +0000614 try:
showardcf8d4922009-10-14 16:08:39 +0000615 try:
616 if install_before and machines:
617 self._execute_code(INSTALL_CONTROL_FILE, namespace)
jadmanskie432dd22009-01-30 15:04:51 +0000618
showardcf8d4922009-10-14 16:08:39 +0000619 if only_collect_crashinfo:
620 return
621
beepscb6f1e22013-06-28 19:14:10 -0700622 # If the verify_job_repo_url option is set but we're unable
623 # to actually verify that the job_repo_url contains the autotest
624 # package, this job will fail.
625 if verify_job_repo_url:
626 self._execute_code(VERIFY_JOB_REPO_URL_CONTROL_FILE,
Dan Shicf4d2032015-03-12 15:04:21 -0700627 namespace)
beepscb6f1e22013-06-28 19:14:10 -0700628 else:
629 logging.warning('Not checking if job_repo_url contains '
630 'autotest packages on %s', machines)
631
jadmanskidef0c3c2009-03-25 20:07:10 +0000632 # determine the dir to write the control files to
633 cfd_specified = (control_file_dir
mbligh0d0f67d2009-11-06 03:15:03 +0000634 and control_file_dir is not self._USE_TEMP_DIR)
jadmanskidef0c3c2009-03-25 20:07:10 +0000635 if cfd_specified:
636 temp_control_file_dir = None
637 else:
638 temp_control_file_dir = tempfile.mkdtemp(
639 suffix='temp_control_file_dir')
640 control_file_dir = temp_control_file_dir
641 server_control_file = os.path.join(control_file_dir,
mblighe0cbc912010-03-11 18:03:07 +0000642 self._control_filename)
jadmanskidef0c3c2009-03-25 20:07:10 +0000643 client_control_file = os.path.join(control_file_dir,
644 CLIENT_CONTROL_FILENAME)
mbligh0d0f67d2009-11-06 03:15:03 +0000645 if self._client:
jadmanskidef0c3c2009-03-25 20:07:10 +0000646 namespace['control'] = control
647 utils.open_write_close(client_control_file, control)
mblighfeac0102009-04-28 18:31:12 +0000648 shutil.copyfile(CLIENT_WRAPPER_CONTROL_FILE,
649 server_control_file)
jadmanskidef0c3c2009-03-25 20:07:10 +0000650 else:
651 utils.open_write_close(server_control_file, control)
Dan Shicf4d2032015-03-12 15:04:21 -0700652
mbligh26f0d882009-06-22 18:30:01 +0000653 logging.info("Processing control file")
Dan Shib669cbd2013-09-13 11:17:17 -0700654 namespace['use_packaging'] = use_packaging
Dan Shicf4d2032015-03-12 15:04:21 -0700655 self._execute_code(server_control_file, namespace)
mbligh26f0d882009-06-22 18:30:01 +0000656 logging.info("Finished processing control file")
jadmanski10646442008-08-13 14:05:21 +0000657
Dan Shib03ea9d2013-08-15 17:13:27 -0700658 # If no device error occured, no need to collect crashinfo.
659 collect_crashinfo = self.failed_with_device_error
Eric Li6f27d4f2010-09-29 10:55:17 -0700660 except Exception, e:
showardcf8d4922009-10-14 16:08:39 +0000661 try:
662 logging.exception(
663 'Exception escaped control file, job aborting:')
Eric Li6f27d4f2010-09-29 10:55:17 -0700664 self.record('INFO', None, None, str(e),
665 {'job_abort_reason': str(e)})
showardcf8d4922009-10-14 16:08:39 +0000666 except:
667 pass # don't let logging exceptions here interfere
668 raise
jadmanski10646442008-08-13 14:05:21 +0000669 finally:
mblighaebe3b62008-12-22 14:45:40 +0000670 if temp_control_file_dir:
jadmanskie432dd22009-01-30 15:04:51 +0000671 # Clean up temp directory used for copies of the control files
mblighaebe3b62008-12-22 14:45:40 +0000672 try:
673 shutil.rmtree(temp_control_file_dir)
674 except Exception, e:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700675 logging.warning('Could not remove temp directory %s: %s',
mblighe7d9c602009-07-02 19:02:33 +0000676 temp_control_file_dir, e)
jadmanskie432dd22009-01-30 15:04:51 +0000677
jadmanskicdd0c402008-09-19 21:21:31 +0000678 if machines and (collect_crashdumps or collect_crashinfo):
jadmanski10646442008-08-13 14:05:21 +0000679 namespace['test_start_time'] = test_start_time
Christopher Wileyf594c5e2013-07-03 18:25:30 -0700680 if skip_crash_collection:
681 logging.info('Skipping crash dump/info collection '
682 'as requested.')
683 elif collect_crashinfo:
mbligh084bc172008-10-18 14:02:45 +0000684 # includes crashdumps
685 self._execute_code(CRASHINFO_CONTROL_FILE, namespace)
jadmanskicdd0c402008-09-19 21:21:31 +0000686 else:
mbligh084bc172008-10-18 14:02:45 +0000687 self._execute_code(CRASHDUMPS_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000688 self.disable_external_logging()
Chris Sosaf4d43ff2012-10-30 11:21:05 -0700689 if self._uncollected_log_file and created_uncollected_logs:
690 os.remove(self._uncollected_log_file)
jadmanski10646442008-08-13 14:05:21 +0000691 if install_after and machines:
mbligh084bc172008-10-18 14:02:45 +0000692 self._execute_code(INSTALL_CONTROL_FILE, namespace)
jadmanski10646442008-08-13 14:05:21 +0000693
694
695 def run_test(self, url, *args, **dargs):
mbligh2b92b862008-11-22 13:25:32 +0000696 """
697 Summon a test object and run it.
jadmanski10646442008-08-13 14:05:21 +0000698
699 tag
700 tag to add to testname
701 url
702 url of the test to run
703 """
Christopher Wiley8a91f232013-07-09 11:02:27 -0700704 if self._disable_sysinfo:
705 dargs['disable_sysinfo'] = True
706
mblighfc3da5b2010-01-06 18:37:22 +0000707 group, testname = self.pkgmgr.get_package_name(url, 'test')
708 testname, subdir, tag = self._build_tagged_test_name(testname, dargs)
709 outputdir = self._make_test_outputdir(subdir)
jadmanski10646442008-08-13 14:05:21 +0000710
711 def group_func():
712 try:
713 test.runtest(self, url, tag, args, dargs)
714 except error.TestBaseException, e:
715 self.record(e.exit_status, subdir, testname, str(e))
716 raise
717 except Exception, e:
718 info = str(e) + "\n" + traceback.format_exc()
719 self.record('FAIL', subdir, testname, info)
720 raise
721 else:
mbligh2b92b862008-11-22 13:25:32 +0000722 self.record('GOOD', subdir, testname, 'completed successfully')
jadmanskide292df2008-08-26 20:51:14 +0000723
724 result, exc_info = self._run_group(testname, subdir, group_func)
725 if exc_info and isinstance(exc_info[1], error.TestBaseException):
726 return False
727 elif exc_info:
728 raise exc_info[0], exc_info[1], exc_info[2]
729 else:
730 return True
jadmanski10646442008-08-13 14:05:21 +0000731
732
733 def _run_group(self, name, subdir, function, *args, **dargs):
734 """\
735 Underlying method for running something inside of a group.
736 """
jadmanskide292df2008-08-26 20:51:14 +0000737 result, exc_info = None, None
jadmanski10646442008-08-13 14:05:21 +0000738 try:
739 self.record('START', subdir, name)
jadmanski52053632010-06-11 21:08:10 +0000740 result = function(*args, **dargs)
jadmanski10646442008-08-13 14:05:21 +0000741 except error.TestBaseException, e:
jadmanskib88d6dc2009-01-10 00:33:18 +0000742 self.record("END %s" % e.exit_status, subdir, name)
jadmanskide292df2008-08-26 20:51:14 +0000743 exc_info = sys.exc_info()
jadmanski10646442008-08-13 14:05:21 +0000744 except Exception, e:
745 err_msg = str(e) + '\n'
746 err_msg += traceback.format_exc()
747 self.record('END ABORT', subdir, name, err_msg)
748 raise error.JobError(name + ' failed\n' + traceback.format_exc())
749 else:
750 self.record('END GOOD', subdir, name)
751
jadmanskide292df2008-08-26 20:51:14 +0000752 return result, exc_info
jadmanski10646442008-08-13 14:05:21 +0000753
754
755 def run_group(self, function, *args, **dargs):
756 """\
757 function:
758 subroutine to run
759 *args:
760 arguments for the function
761 """
762
763 name = function.__name__
764
765 # Allow the tag for the group to be specified.
766 tag = dargs.pop('tag', None)
767 if tag:
768 name = tag
769
jadmanskide292df2008-08-26 20:51:14 +0000770 return self._run_group(name, None, function, *args, **dargs)[0]
jadmanski10646442008-08-13 14:05:21 +0000771
772
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700773 def run_op(self, op, op_func, get_kernel_func):
jadmanski10646442008-08-13 14:05:21 +0000774 """\
775 A specialization of run_group meant specifically for handling
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700776 management operation. Includes support for capturing the kernel version
777 after the operation.
jadmanski10646442008-08-13 14:05:21 +0000778
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700779 Args:
780 op: name of the operation.
781 op_func: a function that carries out the operation (reboot, suspend)
782 get_kernel_func: a function that returns a string
783 representing the kernel version.
jadmanski10646442008-08-13 14:05:21 +0000784 """
jadmanski10646442008-08-13 14:05:21 +0000785 try:
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700786 self.record('START', None, op)
787 op_func()
jadmanski10646442008-08-13 14:05:21 +0000788 except Exception, e:
jadmanski10646442008-08-13 14:05:21 +0000789 err_msg = str(e) + '\n' + traceback.format_exc()
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700790 self.record('END FAIL', None, op, err_msg)
jadmanski4b51d542009-04-08 14:17:16 +0000791 raise
jadmanski10646442008-08-13 14:05:21 +0000792 else:
793 kernel = get_kernel_func()
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700794 self.record('END GOOD', None, op,
Dale Curtis74a314b2011-06-23 14:55:46 -0700795 optional_fields={"kernel": kernel})
jadmanski10646442008-08-13 14:05:21 +0000796
797
jadmanskie432dd22009-01-30 15:04:51 +0000798 def run_control(self, path):
799 """Execute a control file found at path (relative to the autotest
800 path). Intended for executing a control file within a control file,
801 not for running the top-level job control file."""
802 path = os.path.join(self.autodir, path)
803 control_file = self._load_control_file(path)
mbligh0d0f67d2009-11-06 03:15:03 +0000804 self.run(control=control_file, control_file_dir=self._USE_TEMP_DIR)
jadmanskie432dd22009-01-30 15:04:51 +0000805
806
jadmanskic09fc152008-10-15 17:56:59 +0000807 def add_sysinfo_command(self, command, logfile=None, on_every_test=False):
mbligh4395bbd2009-03-25 19:34:17 +0000808 self._add_sysinfo_loggable(sysinfo.command(command, logf=logfile),
jadmanskic09fc152008-10-15 17:56:59 +0000809 on_every_test)
810
811
812 def add_sysinfo_logfile(self, file, on_every_test=False):
813 self._add_sysinfo_loggable(sysinfo.logfile(file), on_every_test)
814
815
816 def _add_sysinfo_loggable(self, loggable, on_every_test):
817 if on_every_test:
818 self.sysinfo.test_loggables.add(loggable)
819 else:
820 self.sysinfo.boot_loggables.add(loggable)
821
822
jadmanski10646442008-08-13 14:05:21 +0000823 def _read_warnings(self):
jadmanskif37df842009-02-11 00:03:26 +0000824 """Poll all the warning loggers and extract any new warnings that have
825 been logged. If the warnings belong to a category that is currently
826 disabled, this method will discard them and they will no longer be
827 retrievable.
828
829 Returns a list of (timestamp, message) tuples, where timestamp is an
830 integer epoch timestamp."""
jadmanski10646442008-08-13 14:05:21 +0000831 warnings = []
832 while True:
833 # pull in a line of output from every logger that has
834 # output ready to be read
mbligh2b92b862008-11-22 13:25:32 +0000835 loggers, _, _ = select.select(self.warning_loggers, [], [], 0)
jadmanski10646442008-08-13 14:05:21 +0000836 closed_loggers = set()
837 for logger in loggers:
838 line = logger.readline()
839 # record any broken pipes (aka line == empty)
840 if len(line) == 0:
841 closed_loggers.add(logger)
842 continue
jadmanskif37df842009-02-11 00:03:26 +0000843 # parse out the warning
844 timestamp, msgtype, msg = line.split('\t', 2)
845 timestamp = int(timestamp)
846 # if the warning is valid, add it to the results
847 if self.warning_manager.is_valid(timestamp, msgtype):
848 warnings.append((timestamp, msg.strip()))
jadmanski10646442008-08-13 14:05:21 +0000849
850 # stop listening to loggers that are closed
851 self.warning_loggers -= closed_loggers
852
853 # stop if none of the loggers have any output left
854 if not loggers:
855 break
856
857 # sort into timestamp order
858 warnings.sort()
859 return warnings
860
861
showardcc929362010-01-25 21:20:41 +0000862 def _unique_subdirectory(self, base_subdirectory_name):
863 """Compute a unique results subdirectory based on the given name.
864
865 Appends base_subdirectory_name with a number as necessary to find a
866 directory name that doesn't already exist.
867 """
868 subdirectory = base_subdirectory_name
869 counter = 1
870 while os.path.exists(os.path.join(self.resultdir, subdirectory)):
871 subdirectory = base_subdirectory_name + '.' + str(counter)
872 counter += 1
873 return subdirectory
874
875
jadmanski52053632010-06-11 21:08:10 +0000876 def get_record_context(self):
877 """Returns an object representing the current job.record context.
878
879 The object returned is an opaque object with a 0-arg restore method
880 which can be called to restore the job.record context (i.e. indentation)
881 to the current level. The intention is that it should be used when
882 something external which generate job.record calls (e.g. an autotest
883 client) can fail catastrophically and the server job record state
884 needs to be reset to its original "known good" state.
885
886 @return: A context object with a 0-arg restore() method."""
887 return self._indenter.get_context()
888
889
showardcc929362010-01-25 21:20:41 +0000890 def record_summary(self, status_code, test_name, reason='', attributes=None,
891 distinguishing_attributes=(), child_test_ids=None):
892 """Record a summary test result.
893
894 @param status_code: status code string, see
895 common_lib.log.is_valid_status()
896 @param test_name: name of the test
897 @param reason: (optional) string providing detailed reason for test
898 outcome
899 @param attributes: (optional) dict of string keyvals to associate with
900 this result
901 @param distinguishing_attributes: (optional) list of attribute names
902 that should be used to distinguish identically-named test
903 results. These attributes should be present in the attributes
904 parameter. This is used to generate user-friendly subdirectory
905 names.
906 @param child_test_ids: (optional) list of test indices for test results
907 used in generating this result.
908 """
909 subdirectory_name_parts = [test_name]
910 for attribute in distinguishing_attributes:
911 assert attributes
912 assert attribute in attributes, '%s not in %s' % (attribute,
913 attributes)
914 subdirectory_name_parts.append(attributes[attribute])
915 base_subdirectory_name = '.'.join(subdirectory_name_parts)
916
917 subdirectory = self._unique_subdirectory(base_subdirectory_name)
918 subdirectory_path = os.path.join(self.resultdir, subdirectory)
919 os.mkdir(subdirectory_path)
920
921 self.record(status_code, subdirectory, test_name,
922 status=reason, optional_fields={'is_summary': True})
923
924 if attributes:
925 utils.write_keyval(subdirectory_path, attributes)
926
927 if child_test_ids:
928 ids_string = ','.join(str(test_id) for test_id in child_test_ids)
929 summary_data = {'child_test_ids': ids_string}
930 utils.write_keyval(os.path.join(subdirectory_path, 'summary_data'),
931 summary_data)
932
933
jadmanski16a7ff72009-04-01 18:19:53 +0000934 def disable_warnings(self, warning_type):
jadmanskif37df842009-02-11 00:03:26 +0000935 self.warning_manager.disable_warnings(warning_type)
jadmanski16a7ff72009-04-01 18:19:53 +0000936 self.record("INFO", None, None,
937 "disabling %s warnings" % warning_type,
938 {"warnings.disable": warning_type})
jadmanskif37df842009-02-11 00:03:26 +0000939
940
jadmanski16a7ff72009-04-01 18:19:53 +0000941 def enable_warnings(self, warning_type):
jadmanskif37df842009-02-11 00:03:26 +0000942 self.warning_manager.enable_warnings(warning_type)
jadmanski16a7ff72009-04-01 18:19:53 +0000943 self.record("INFO", None, None,
944 "enabling %s warnings" % warning_type,
945 {"warnings.enable": warning_type})
jadmanskif37df842009-02-11 00:03:26 +0000946
947
jadmanski779bd292009-03-19 17:33:33 +0000948 def get_status_log_path(self, subdir=None):
949 """Return the path to the job status log.
950
951 @param subdir - Optional paramter indicating that you want the path
952 to a subdirectory status log.
953
954 @returns The path where the status log should be.
955 """
mbligh210bae62009-04-01 18:33:13 +0000956 if self.resultdir:
957 if subdir:
958 return os.path.join(self.resultdir, subdir, "status.log")
959 else:
960 return os.path.join(self.resultdir, "status.log")
jadmanski779bd292009-03-19 17:33:33 +0000961 else:
mbligh210bae62009-04-01 18:33:13 +0000962 return None
jadmanski779bd292009-03-19 17:33:33 +0000963
964
jadmanski6bb32d72009-03-19 20:25:24 +0000965 def _update_uncollected_logs_list(self, update_func):
966 """Updates the uncollected logs list in a multi-process safe manner.
967
968 @param update_func - a function that updates the list of uncollected
969 logs. Should take one parameter, the list to be updated.
970 """
Dan Shi07e09af2013-04-12 09:31:29 -0700971 # Skip log collection if file _uncollected_log_file does not exist.
972 if not (self._uncollected_log_file and
973 os.path.exists(self._uncollected_log_file)):
974 return
mbligh0d0f67d2009-11-06 03:15:03 +0000975 if self._uncollected_log_file:
976 log_file = open(self._uncollected_log_file, "r+")
mbligha788dc42009-03-26 21:10:16 +0000977 fcntl.flock(log_file, fcntl.LOCK_EX)
jadmanski6bb32d72009-03-19 20:25:24 +0000978 try:
979 uncollected_logs = pickle.load(log_file)
980 update_func(uncollected_logs)
981 log_file.seek(0)
982 log_file.truncate()
983 pickle.dump(uncollected_logs, log_file)
jadmanski3bff9092009-04-22 18:09:47 +0000984 log_file.flush()
jadmanski6bb32d72009-03-19 20:25:24 +0000985 finally:
986 fcntl.flock(log_file, fcntl.LOCK_UN)
987 log_file.close()
988
989
990 def add_client_log(self, hostname, remote_path, local_path):
991 """Adds a new set of client logs to the list of uncollected logs,
992 to allow for future log recovery.
993
994 @param host - the hostname of the machine holding the logs
995 @param remote_path - the directory on the remote machine holding logs
996 @param local_path - the local directory to copy the logs into
997 """
998 def update_func(logs_list):
999 logs_list.append((hostname, remote_path, local_path))
1000 self._update_uncollected_logs_list(update_func)
1001
1002
1003 def remove_client_log(self, hostname, remote_path, local_path):
1004 """Removes a set of client logs from the list of uncollected logs,
1005 to allow for future log recovery.
1006
1007 @param host - the hostname of the machine holding the logs
1008 @param remote_path - the directory on the remote machine holding logs
1009 @param local_path - the local directory to copy the logs into
1010 """
1011 def update_func(logs_list):
1012 logs_list.remove((hostname, remote_path, local_path))
1013 self._update_uncollected_logs_list(update_func)
1014
1015
mbligh0d0f67d2009-11-06 03:15:03 +00001016 def get_client_logs(self):
1017 """Retrieves the list of uncollected logs, if it exists.
1018
1019 @returns A list of (host, remote_path, local_path) tuples. Returns
1020 an empty list if no uncollected logs file exists.
1021 """
1022 log_exists = (self._uncollected_log_file and
1023 os.path.exists(self._uncollected_log_file))
1024 if log_exists:
1025 return pickle.load(open(self._uncollected_log_file))
1026 else:
1027 return []
1028
1029
mbligh084bc172008-10-18 14:02:45 +00001030 def _fill_server_control_namespace(self, namespace, protect=True):
mbligh2b92b862008-11-22 13:25:32 +00001031 """
1032 Prepare a namespace to be used when executing server control files.
mbligh084bc172008-10-18 14:02:45 +00001033
1034 This sets up the control file API by importing modules and making them
1035 available under the appropriate names within namespace.
1036
1037 For use by _execute_code().
1038
1039 Args:
1040 namespace: The namespace dictionary to fill in.
1041 protect: Boolean. If True (the default) any operation that would
1042 clobber an existing entry in namespace will cause an error.
1043 Raises:
1044 error.AutoservError: When a name would be clobbered by import.
1045 """
1046 def _import_names(module_name, names=()):
mbligh2b92b862008-11-22 13:25:32 +00001047 """
1048 Import a module and assign named attributes into namespace.
mbligh084bc172008-10-18 14:02:45 +00001049
1050 Args:
1051 module_name: The string module name.
1052 names: A limiting list of names to import from module_name. If
1053 empty (the default), all names are imported from the module
1054 similar to a "from foo.bar import *" statement.
1055 Raises:
1056 error.AutoservError: When a name being imported would clobber
1057 a name already in namespace.
1058 """
1059 module = __import__(module_name, {}, {}, names)
1060
1061 # No names supplied? Import * from the lowest level module.
1062 # (Ugh, why do I have to implement this part myself?)
1063 if not names:
1064 for submodule_name in module_name.split('.')[1:]:
1065 module = getattr(module, submodule_name)
1066 if hasattr(module, '__all__'):
1067 names = getattr(module, '__all__')
1068 else:
1069 names = dir(module)
1070
1071 # Install each name into namespace, checking to make sure it
1072 # doesn't override anything that already exists.
1073 for name in names:
1074 # Check for conflicts to help prevent future problems.
1075 if name in namespace and protect:
1076 if namespace[name] is not getattr(module, name):
1077 raise error.AutoservError('importing name '
1078 '%s from %s %r would override %r' %
1079 (name, module_name, getattr(module, name),
1080 namespace[name]))
1081 else:
1082 # Encourage cleanliness and the use of __all__ for a
1083 # more concrete API with less surprises on '*' imports.
1084 warnings.warn('%s (%r) being imported from %s for use '
1085 'in server control files is not the '
1086 'first occurrance of that import.' %
1087 (name, namespace[name], module_name))
1088
1089 namespace[name] = getattr(module, name)
1090
1091
1092 # This is the equivalent of prepending a bunch of import statements to
1093 # the front of the control script.
mbligha2b07dd2009-06-22 18:26:13 +00001094 namespace.update(os=os, sys=sys, logging=logging)
mbligh084bc172008-10-18 14:02:45 +00001095 _import_names('autotest_lib.server',
1096 ('hosts', 'autotest', 'kvm', 'git', 'standalone_profiler',
1097 'source_kernel', 'rpm_kernel', 'deb_kernel', 'git_kernel'))
1098 _import_names('autotest_lib.server.subcommand',
1099 ('parallel', 'parallel_simple', 'subcommand'))
1100 _import_names('autotest_lib.server.utils',
1101 ('run', 'get_tmp_dir', 'sh_escape', 'parse_machine'))
1102 _import_names('autotest_lib.client.common_lib.error')
1103 _import_names('autotest_lib.client.common_lib.barrier', ('barrier',))
1104
1105 # Inject ourself as the job object into other classes within the API.
1106 # (Yuck, this injection is a gross thing be part of a public API. -gps)
1107 #
1108 # XXX Base & SiteAutotest do not appear to use .job. Who does?
1109 namespace['autotest'].Autotest.job = self
1110 # server.hosts.base_classes.Host uses .job.
1111 namespace['hosts'].Host.job = self
Eric Li10222b82010-11-24 09:33:15 -08001112 namespace['hosts'].factory.ssh_user = self._ssh_user
1113 namespace['hosts'].factory.ssh_port = self._ssh_port
1114 namespace['hosts'].factory.ssh_pass = self._ssh_pass
Fang Dengd1c2b732013-08-20 12:59:46 -07001115 namespace['hosts'].factory.ssh_verbosity_flag = (
1116 self._ssh_verbosity_flag)
Aviv Keshetc5947fa2013-09-04 14:06:29 -07001117 namespace['hosts'].factory.ssh_options = self._ssh_options
mbligh084bc172008-10-18 14:02:45 +00001118
1119
1120 def _execute_code(self, code_file, namespace, protect=True):
mbligh2b92b862008-11-22 13:25:32 +00001121 """
1122 Execute code using a copy of namespace as a server control script.
mbligh084bc172008-10-18 14:02:45 +00001123
1124 Unless protect_namespace is explicitly set to False, the dict will not
1125 be modified.
1126
1127 Args:
1128 code_file: The filename of the control file to execute.
1129 namespace: A dict containing names to make available during execution.
1130 protect: Boolean. If True (the default) a copy of the namespace dict
1131 is used during execution to prevent the code from modifying its
1132 contents outside of this function. If False the raw dict is
1133 passed in and modifications will be allowed.
1134 """
1135 if protect:
1136 namespace = namespace.copy()
1137 self._fill_server_control_namespace(namespace, protect=protect)
1138 # TODO: Simplify and get rid of the special cases for only 1 machine.
showard3e66e8c2008-10-27 19:20:51 +00001139 if len(self.machines) > 1:
mbligh084bc172008-10-18 14:02:45 +00001140 machines_text = '\n'.join(self.machines) + '\n'
1141 # Only rewrite the file if it does not match our machine list.
1142 try:
1143 machines_f = open(MACHINES_FILENAME, 'r')
1144 existing_machines_text = machines_f.read()
1145 machines_f.close()
1146 except EnvironmentError:
1147 existing_machines_text = None
1148 if machines_text != existing_machines_text:
1149 utils.open_write_close(MACHINES_FILENAME, machines_text)
1150 execfile(code_file, namespace, namespace)
jadmanski10646442008-08-13 14:05:21 +00001151
1152
jadmanskie29d0e42010-06-17 16:06:52 +00001153 def _parse_status(self, new_line):
mbligh0d0f67d2009-11-06 03:15:03 +00001154 if not self._using_parser:
jadmanski10646442008-08-13 14:05:21 +00001155 return
jadmanskie29d0e42010-06-17 16:06:52 +00001156 new_tests = self.parser.process_lines([new_line])
jadmanski10646442008-08-13 14:05:21 +00001157 for test in new_tests:
1158 self.__insert_test(test)
1159
1160
1161 def __insert_test(self, test):
mbligh2b92b862008-11-22 13:25:32 +00001162 """
1163 An internal method to insert a new test result into the
jadmanski10646442008-08-13 14:05:21 +00001164 database. This method will not raise an exception, even if an
1165 error occurs during the insert, to avoid failing a test
1166 simply because of unexpected database issues."""
showard21baa452008-10-21 00:08:39 +00001167 self.num_tests_run += 1
1168 if status_lib.is_worse_than_or_equal_to(test.status, 'FAIL'):
1169 self.num_tests_failed += 1
jadmanski10646442008-08-13 14:05:21 +00001170 try:
1171 self.results_db.insert_test(self.job_model, test)
1172 except Exception:
1173 msg = ("WARNING: An unexpected error occured while "
1174 "inserting test results into the database. "
1175 "Ignoring error.\n" + traceback.format_exc())
1176 print >> sys.stderr, msg
1177
mblighcaa62c22008-04-07 21:51:17 +00001178
mblighfc3da5b2010-01-06 18:37:22 +00001179 def preprocess_client_state(self):
1180 """
1181 Produce a state file for initializing the state of a client job.
1182
1183 Creates a new client state file with all the current server state, as
1184 well as some pre-set client state.
1185
1186 @returns The path of the file the state was written into.
1187 """
1188 # initialize the sysinfo state
1189 self._state.set('client', 'sysinfo', self.sysinfo.serialize())
1190
1191 # dump the state out to a tempfile
1192 fd, file_path = tempfile.mkstemp(dir=self.tmpdir)
1193 os.close(fd)
mbligha2c99492010-01-27 22:59:50 +00001194
1195 # write_to_file doesn't need locking, we exclusively own file_path
mblighfc3da5b2010-01-06 18:37:22 +00001196 self._state.write_to_file(file_path)
1197 return file_path
1198
1199
1200 def postprocess_client_state(self, state_path):
1201 """
1202 Update the state of this job with the state from a client job.
1203
1204 Updates the state of the server side of a job with the final state
1205 of a client job that was run. Updates the non-client-specific state,
1206 pulls in some specific bits from the client-specific state, and then
1207 discards the rest. Removes the state file afterwards
1208
1209 @param state_file A path to the state file from the client.
1210 """
1211 # update the on-disk state
mblighfc3da5b2010-01-06 18:37:22 +00001212 try:
jadmanskib6e7bdb2010-04-13 16:00:39 +00001213 self._state.read_from_file(state_path)
mblighfc3da5b2010-01-06 18:37:22 +00001214 os.remove(state_path)
mbligha2c99492010-01-27 22:59:50 +00001215 except OSError, e:
mblighfc3da5b2010-01-06 18:37:22 +00001216 # ignore file-not-found errors
1217 if e.errno != errno.ENOENT:
1218 raise
jadmanskib6e7bdb2010-04-13 16:00:39 +00001219 else:
1220 logging.debug('Client state file %s not found', state_path)
mblighfc3da5b2010-01-06 18:37:22 +00001221
1222 # update the sysinfo state
1223 if self._state.has('client', 'sysinfo'):
1224 self.sysinfo.deserialize(self._state.get('client', 'sysinfo'))
1225
1226 # drop all the client-specific state
1227 self._state.discard_namespace('client')
1228
1229
mbligh0a883702010-04-21 01:58:34 +00001230 def clear_all_known_hosts(self):
1231 """Clears known hosts files for all AbstractSSHHosts."""
1232 for host in self.hosts:
1233 if isinstance(host, abstract_ssh.AbstractSSHHost):
1234 host.clear_known_hosts()
1235
1236
jadmanskif37df842009-02-11 00:03:26 +00001237class warning_manager(object):
1238 """Class for controlling warning logs. Manages the enabling and disabling
1239 of warnings."""
1240 def __init__(self):
1241 # a map of warning types to a list of disabled time intervals
1242 self.disabled_warnings = {}
1243
1244
1245 def is_valid(self, timestamp, warning_type):
1246 """Indicates if a warning (based on the time it occured and its type)
1247 is a valid warning. A warning is considered "invalid" if this type of
1248 warning was marked as "disabled" at the time the warning occured."""
1249 disabled_intervals = self.disabled_warnings.get(warning_type, [])
1250 for start, end in disabled_intervals:
1251 if timestamp >= start and (end is None or timestamp < end):
1252 return False
1253 return True
1254
1255
1256 def disable_warnings(self, warning_type, current_time_func=time.time):
1257 """As of now, disables all further warnings of this type."""
1258 intervals = self.disabled_warnings.setdefault(warning_type, [])
1259 if not intervals or intervals[-1][1] is not None:
jadmanski16a7ff72009-04-01 18:19:53 +00001260 intervals.append((int(current_time_func()), None))
jadmanskif37df842009-02-11 00:03:26 +00001261
1262
1263 def enable_warnings(self, warning_type, current_time_func=time.time):
1264 """As of now, enables all further warnings of this type."""
1265 intervals = self.disabled_warnings.get(warning_type, [])
1266 if intervals and intervals[-1][1] is None:
jadmanski16a7ff72009-04-01 18:19:53 +00001267 intervals[-1] = (intervals[-1][0], int(current_time_func()))
Paul Pendlebury57593562011-06-15 10:45:49 -07001268
1269
1270# load up site-specific code for generating site-specific job data
1271get_site_job_data = utils.import_site_function(__file__,
1272 "autotest_lib.server.site_server_job", "get_site_job_data",
1273 _get_site_job_data_dummy)
1274
1275
1276site_server_job = utils.import_site_class(
1277 __file__, "autotest_lib.server.site_server_job", "site_server_job",
1278 base_server_job)
1279
1280
1281class server_job(site_server_job):
Dale Curtis456d3c12011-07-19 11:42:51 -07001282 pass