blob: 89bbcdf4f6d5c26a83941827ba3d14abbcbd6a0b [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
Allen Liab020912016-09-19 18:07:41 -070014import errno
15import fcntl
16import getpass
17import itertools
18import logging
19import os
20import pickle
21import platform
22import re
23import select
24import shutil
25import sys
26import tempfile
27import time
28import traceback
Prathmesh Prabhu588007d2017-06-15 00:31:31 -070029import uuid
Allen Liab020912016-09-19 18:07:41 -070030import warnings
31
showard75cdfee2009-06-10 17:40:41 +000032from autotest_lib.client.bin import sysinfo
Allen Liab020912016-09-19 18:07:41 -070033from autotest_lib.client.common_lib import base_job
Dan Shif53d1262017-06-19 11:25:25 -070034from autotest_lib.client.common_lib import control_data
Allen Liab020912016-09-19 18:07:41 -070035from autotest_lib.client.common_lib import error
showard75cdfee2009-06-10 17:40:41 +000036from autotest_lib.client.common_lib import logging_manager
Allen Liab020912016-09-19 18:07:41 -070037from autotest_lib.client.common_lib import packages
38from autotest_lib.client.common_lib import utils
39from autotest_lib.server import profilers
Prathmesh Prabhu20ea1062017-07-06 10:23:42 -070040from autotest_lib.server import site_gtest_runner
Allen Liab020912016-09-19 18:07:41 -070041from autotest_lib.server import subcommand
42from autotest_lib.server import test
Simran Basi1bf60eb2015-12-01 16:39:29 -080043from autotest_lib.server import utils as server_utils
44from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
Allen Liab020912016-09-19 18:07:41 -070045from autotest_lib.server.hosts import abstract_ssh
Prathmesh Prabhu07b5d982017-01-11 12:08:33 -080046from autotest_lib.server.hosts import afe_store
Prathmesh Prabhu588007d2017-06-15 00:31:31 -070047from autotest_lib.server.hosts import file_store
48from autotest_lib.server.hosts import shadowing_store
Allen Liab020912016-09-19 18:07:41 -070049from autotest_lib.server.hosts import factory as host_factory
Prathmesh Prabhu07b5d982017-01-11 12:08:33 -080050from autotest_lib.server.hosts import host_info
Hidehiko Abe06893302017-06-24 07:32:38 +090051from autotest_lib.server.hosts import ssh_multiplex
Allen Liab020912016-09-19 18:07:41 -070052from autotest_lib.tko import models as tko_models
Luigi Semenzatoe7064812017-02-03 14:47:59 -080053from autotest_lib.tko import parser_lib
jadmanski10646442008-08-13 14:05:21 +000054
Xixuan Wu60325a62017-11-20 17:21:15 -080055try:
56 from chromite.lib import metrics
57except ImportError:
58 metrics = utils.metrics_mock
59
jadmanski10646442008-08-13 14:05:21 +000060
mbligh084bc172008-10-18 14:02:45 +000061def _control_segment_path(name):
62 """Get the pathname of the named control segment file."""
jadmanski10646442008-08-13 14:05:21 +000063 server_dir = os.path.dirname(os.path.abspath(__file__))
mbligh084bc172008-10-18 14:02:45 +000064 return os.path.join(server_dir, "control_segments", name)
jadmanski10646442008-08-13 14:05:21 +000065
66
mbligh084bc172008-10-18 14:02:45 +000067CLIENT_CONTROL_FILENAME = 'control'
68SERVER_CONTROL_FILENAME = 'control.srv'
69MACHINES_FILENAME = '.machines'
jadmanski10646442008-08-13 14:05:21 +000070
mbligh084bc172008-10-18 14:02:45 +000071CLIENT_WRAPPER_CONTROL_FILE = _control_segment_path('client_wrapper')
Allen Li5913d8c2018-09-25 17:56:31 -070072CLIENT_TRAMPOLINE_CONTROL_FILE = _control_segment_path('client_trampoline')
mbligh084bc172008-10-18 14:02:45 +000073CRASHDUMPS_CONTROL_FILE = _control_segment_path('crashdumps')
74CRASHINFO_CONTROL_FILE = _control_segment_path('crashinfo')
showard45ae8192008-11-05 19:32:53 +000075CLEANUP_CONTROL_FILE = _control_segment_path('cleanup')
mbligh084bc172008-10-18 14:02:45 +000076VERIFY_CONTROL_FILE = _control_segment_path('verify')
mbligh084bc172008-10-18 14:02:45 +000077REPAIR_CONTROL_FILE = _control_segment_path('repair')
Alex Millercb79ba72013-05-29 14:43:00 -070078PROVISION_CONTROL_FILE = _control_segment_path('provision')
beepscb6f1e22013-06-28 19:14:10 -070079VERIFY_JOB_REPO_URL_CONTROL_FILE = _control_segment_path('verify_job_repo_url')
Dan Shi07e09af2013-04-12 09:31:29 -070080RESET_CONTROL_FILE = _control_segment_path('reset')
Luigi Semenzato542d0d52016-06-29 11:30:15 -070081GET_NETWORK_STATS_CONTROL_FILE = _control_segment_path('get_network_stats')
jadmanski10646442008-08-13 14:05:21 +000082
mbligh062ed152009-01-13 00:57:14 +000083
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -070084def get_machine_dicts(machine_names, store_dir, in_lab, use_shadow_store,
85 host_attributes=None):
Prathmesh Prabhu429d1002017-01-10 15:30:53 -080086 """Converts a list of machine names to list of dicts.
87
Prathmesh Prabhu588007d2017-06-15 00:31:31 -070088 TODO(crbug.com/678430): This function temporarily has a side effect of
89 creating files under workdir for backing a FileStore. This side-effect will
90 go away once callers of autoserv start passing in the FileStore.
91
Prathmesh Prabhu429d1002017-01-10 15:30:53 -080092 @param machine_names: A list of machine names.
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -070093 @param store_dir: A directory to contain store backing files.
94 @param use_shadow_store: If True, we should create a ShadowingStore where
95 actual store is backed by the AFE but we create a backing file to
96 shadow the store. If False, backing file should already exist at:
97 ${store_dir}/${hostname}.store
Prathmesh Prabhu429d1002017-01-10 15:30:53 -080098 @param in_lab: A boolean indicating whether we're running in lab.
99 @param host_attributes: Optional list of host attributes to add for each
100 host.
101 @returns: A list of dicts. Each dict has the following keys:
102 'hostname': Name of the machine originally in machine_names (str).
103 'afe_host': A frontend.Host object for the machine, or a stub if
104 in_lab is false.
Prathmesh Prabhu07b5d982017-01-11 12:08:33 -0800105 'host_info_store': A host_info.CachingHostInfoStore object to obtain
106 host information. A stub if in_lab is False.
Hidehiko Abe06893302017-06-24 07:32:38 +0900107 'connection_pool': ssh_multiplex.ConnectionPool instance to share
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700108 master ssh connection across control scripts. This is set to
109 None, and should be overridden for connection sharing.
Prathmesh Prabhu429d1002017-01-10 15:30:53 -0800110 """
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700111 # See autoserv_parser.parse_args. Only one of in_lab or host_attributes can
112 # be provided.
113 if in_lab and host_attributes:
114 raise error.AutoservError(
115 'in_lab and host_attribute are mutually exclusive.')
116
Prathmesh Prabhu429d1002017-01-10 15:30:53 -0800117 machine_dict_list = []
Prathmesh Prabhu429d1002017-01-10 15:30:53 -0800118 for machine in machine_names:
Prathmesh Prabhucbeab122017-05-30 11:23:53 -0700119 if not in_lab:
120 afe_host = server_utils.EmptyAFEHost()
121 host_info_store = host_info.InMemoryHostInfoStore()
122 if host_attributes is not None:
123 afe_host.attributes.update(host_attributes)
124 info = host_info.HostInfo(attributes=host_attributes)
125 host_info_store.commit(info)
Prathmesh Prabhueb566792018-06-12 10:08:04 -0700126 elif use_shadow_store:
Prathmesh Prabhucbeab122017-05-30 11:23:53 -0700127 afe_host = _create_afe_host(machine)
Prathmesh Prabhueb566792018-06-12 10:08:04 -0700128 host_info_store = _create_afe_backed_host_info_store(store_dir,
129 machine)
130 else:
131 afe_host = server_utils.EmptyAFEHost()
132 host_info_store = _create_file_backed_host_info_store(store_dir,
133 machine)
Prathmesh Prabhucbeab122017-05-30 11:23:53 -0700134
Prathmesh Prabhu07b5d982017-01-11 12:08:33 -0800135 machine_dict_list.append({
136 'hostname' : machine,
137 'afe_host' : afe_host,
Prathmesh Prabhucbeab122017-05-30 11:23:53 -0700138 'host_info_store': host_info_store,
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700139 'connection_pool': None,
Prathmesh Prabhu07b5d982017-01-11 12:08:33 -0800140 })
Prathmesh Prabhucbeab122017-05-30 11:23:53 -0700141
Prathmesh Prabhu429d1002017-01-10 15:30:53 -0800142 return machine_dict_list
143
144
jadmanski2a89dac2010-06-11 14:32:58 +0000145class status_indenter(base_job.status_indenter):
146 """Provide a simple integer-backed status indenter."""
147 def __init__(self):
148 self._indent = 0
149
150
151 @property
152 def indent(self):
153 return self._indent
154
155
156 def increment(self):
157 self._indent += 1
158
159
160 def decrement(self):
161 self._indent -= 1
162
163
jadmanski52053632010-06-11 21:08:10 +0000164 def get_context(self):
165 """Returns a context object for use by job.get_record_context."""
166 class context(object):
167 def __init__(self, indenter, indent):
168 self._indenter = indenter
169 self._indent = indent
170 def restore(self):
171 self._indenter._indent = self._indent
172 return context(self, self._indent)
173
174
jadmanski2a89dac2010-06-11 14:32:58 +0000175class server_job_record_hook(object):
176 """The job.record hook for server job. Used to inject WARN messages from
177 the console or vlm whenever new logs are written, and to echo any logs
178 to INFO level logging. Implemented as a class so that it can use state to
179 block recursive calls, so that the hook can call job.record itself to
180 log WARN messages.
181
182 Depends on job._read_warnings and job._logger.
183 """
184 def __init__(self, job):
185 self._job = job
186 self._being_called = False
187
188
189 def __call__(self, entry):
190 """A wrapper around the 'real' record hook, the _hook method, which
191 prevents recursion. This isn't making any effort to be threadsafe,
192 the intent is to outright block infinite recursion via a
193 job.record->_hook->job.record->_hook->job.record... chain."""
194 if self._being_called:
195 return
196 self._being_called = True
197 try:
198 self._hook(self._job, entry)
199 finally:
200 self._being_called = False
201
202
203 @staticmethod
204 def _hook(job, entry):
205 """The core hook, which can safely call job.record."""
206 entries = []
207 # poll all our warning loggers for new warnings
208 for timestamp, msg in job._read_warnings():
209 warning_entry = base_job.status_log_entry(
210 'WARN', None, None, msg, {}, timestamp=timestamp)
211 entries.append(warning_entry)
212 job.record_entry(warning_entry)
213 # echo rendered versions of all the status logs to info
214 entries.append(entry)
215 for entry in entries:
216 rendered_entry = job._logger.render_entry(entry)
217 logging.info(rendered_entry)
218
219
Prathmesh Prabhu20ea1062017-07-06 10:23:42 -0700220class server_job(base_job.base_job):
mbligh0d0f67d2009-11-06 03:15:03 +0000221 """The server-side concrete implementation of base_job.
jadmanski10646442008-08-13 14:05:21 +0000222
mbligh0d0f67d2009-11-06 03:15:03 +0000223 Optional properties provided by this implementation:
224 serverdir
mbligh0d0f67d2009-11-06 03:15:03 +0000225
mbligh0d0f67d2009-11-06 03:15:03 +0000226 warning_manager
227 warning_loggers
jadmanski10646442008-08-13 14:05:21 +0000228 """
229
mbligh0d0f67d2009-11-06 03:15:03 +0000230 _STATUS_VERSION = 1
jadmanski10646442008-08-13 14:05:21 +0000231
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700232 # TODO crbug.com/285395 eliminate ssh_verbosity_flag
jadmanski10646442008-08-13 14:05:21 +0000233 def __init__(self, control, args, resultdir, label, user, machines,
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700234 machine_dict_list,
Prathmesh Prabhue0b70c62018-04-24 15:33:42 -0700235 client=False,
beepsd0672682013-09-16 17:32:16 -0700236 ssh_user=host_factory.DEFAULT_SSH_USER,
237 ssh_port=host_factory.DEFAULT_SSH_PORT,
238 ssh_pass=host_factory.DEFAULT_SSH_PASS,
239 ssh_verbosity_flag=host_factory.DEFAULT_SSH_VERBOSITY,
240 ssh_options=host_factory.DEFAULT_SSH_OPTIONS,
Prathmesh Prabhu7d08ac12018-08-17 17:34:39 -0700241 group_name='',
Fang Dengd1c2b732013-08-20 12:59:46 -0700242 tag='', disable_sysinfo=False,
Dan Shi70647ca2015-07-16 22:52:35 -0700243 control_filename=SERVER_CONTROL_FILENAME,
Allen Li5913d8c2018-09-25 17:56:31 -0700244 parent_job_id=None, in_lab=False,
245 use_client_trampoline=False):
jadmanski10646442008-08-13 14:05:21 +0000246 """
mbligh374f3412009-05-13 21:29:45 +0000247 Create a server side job object.
mblighb5dac432008-11-27 00:38:44 +0000248
mblighe7d9c602009-07-02 19:02:33 +0000249 @param control: The pathname of the control file.
250 @param args: Passed to the control file.
251 @param resultdir: Where to throw the results.
252 @param label: Description of the job.
253 @param user: Username for the job (email address).
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700254 @param machines: A list of hostnames of the machines to use for the job.
255 @param machine_dict_list: A list of dicts for each of the machines above
256 as returned by get_machine_dicts.
mblighe7d9c602009-07-02 19:02:33 +0000257 @param client: True if this is a client-side control file.
mblighe7d9c602009-07-02 19:02:33 +0000258 @param ssh_user: The SSH username. [root]
259 @param ssh_port: The SSH port number. [22]
260 @param ssh_pass: The SSH passphrase, if needed.
Fang Dengd1c2b732013-08-20 12:59:46 -0700261 @param ssh_verbosity_flag: The SSH verbosity flag, '-v', '-vv',
262 '-vvv', or an empty string if not needed.
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700263 @param ssh_options: A string giving additional options that will be
264 included in ssh commands.
mblighe7d9c602009-07-02 19:02:33 +0000265 @param group_name: If supplied, this will be written out as
mbligh374f3412009-05-13 21:29:45 +0000266 host_group_name in the keyvals file for the parser.
mblighe7d9c602009-07-02 19:02:33 +0000267 @param tag: The job execution tag from the scheduler. [optional]
Christopher Wiley8a91f232013-07-09 11:02:27 -0700268 @param disable_sysinfo: Whether we should disable the sysinfo step of
269 tests for a modest shortening of test time. [optional]
mblighe0cbc912010-03-11 18:03:07 +0000270 @param control_filename: The filename where the server control file
271 should be written in the results directory.
Dan Shi70647ca2015-07-16 22:52:35 -0700272 @param parent_job_id: Job ID of the parent job. Default to None if the
273 job does not have a parent job.
Simran Basi1bf60eb2015-12-01 16:39:29 -0800274 @param in_lab: Boolean that indicates if this is running in the lab
275 environment.
Allen Li5913d8c2018-09-25 17:56:31 -0700276 @param use_client_trampoline: Boolean that indicates whether to
277 use the client trampoline flow. If this is True, control
278 is interpreted as the name of the client test to run.
279 The client control file will be client_trampoline. The
280 test name will be passed to client_trampoline, which will
281 install the test package and re-exec the actual test
282 control file.
jadmanski10646442008-08-13 14:05:21 +0000283 """
Prathmesh Prabhu7d08ac12018-08-17 17:34:39 -0700284 super(server_job, self).__init__(resultdir=resultdir)
mbligh0d0f67d2009-11-06 03:15:03 +0000285 self.control = control
286 self._uncollected_log_file = os.path.join(self.resultdir,
287 'uncollected_logs')
288 debugdir = os.path.join(self.resultdir, 'debug')
289 if not os.path.exists(debugdir):
290 os.mkdir(debugdir)
291
292 if user:
293 self.user = user
294 else:
295 self.user = getpass.getuser()
296
jadmanski808f4b12010-04-09 22:30:31 +0000297 self.args = args
Peter Mayo7a875762012-06-13 14:38:15 -0400298 self.label = label
jadmanski10646442008-08-13 14:05:21 +0000299 self.machines = machines
mbligh0d0f67d2009-11-06 03:15:03 +0000300 self._client = client
jadmanski10646442008-08-13 14:05:21 +0000301 self.warning_loggers = set()
jadmanskif37df842009-02-11 00:03:26 +0000302 self.warning_manager = warning_manager()
mbligh0d0f67d2009-11-06 03:15:03 +0000303 self._ssh_user = ssh_user
304 self._ssh_port = ssh_port
305 self._ssh_pass = ssh_pass
Fang Dengd1c2b732013-08-20 12:59:46 -0700306 self._ssh_verbosity_flag = ssh_verbosity_flag
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700307 self._ssh_options = ssh_options
mblighe7d9c602009-07-02 19:02:33 +0000308 self.tag = tag
jadmanski53aaf382008-11-17 16:22:31 +0000309 self.hosts = set()
mbligh0d0f67d2009-11-06 03:15:03 +0000310 self.drop_caches = False
mblighb5dac432008-11-27 00:38:44 +0000311 self.drop_caches_between_iterations = False
mblighe0cbc912010-03-11 18:03:07 +0000312 self._control_filename = control_filename
Christopher Wiley8a91f232013-07-09 11:02:27 -0700313 self._disable_sysinfo = disable_sysinfo
Allen Li5913d8c2018-09-25 17:56:31 -0700314 self._use_client_trampoline = use_client_trampoline
jadmanski10646442008-08-13 14:05:21 +0000315
showard75cdfee2009-06-10 17:40:41 +0000316 self.logging = logging_manager.get_logging_manager(
317 manage_stdout_and_stderr=True, redirect_fds=True)
318 subcommand.logging_manager_object = self.logging
jadmanski10646442008-08-13 14:05:21 +0000319
mbligh0d0f67d2009-11-06 03:15:03 +0000320 self.sysinfo = sysinfo.sysinfo(self.resultdir)
jadmanski043e1132008-11-19 17:10:32 +0000321 self.profilers = profilers.profilers(self)
jadmanskic09fc152008-10-15 17:56:59 +0000322
jadmanski10646442008-08-13 14:05:21 +0000323 job_data = {'label' : label, 'user' : user,
324 'hostname' : ','.join(machines),
Eric Li861b2d52011-02-04 14:50:35 -0800325 'drone' : platform.node(),
mbligh0d0f67d2009-11-06 03:15:03 +0000326 'status_version' : str(self._STATUS_VERSION),
showard170873e2009-01-07 00:22:26 +0000327 'job_started' : str(int(time.time()))}
Dan Shi70647ca2015-07-16 22:52:35 -0700328 # Save parent job id to keyvals, so parser can retrieve the info and
329 # write to tko_jobs record.
330 if parent_job_id:
331 job_data['parent_job_id'] = parent_job_id
mbligh374f3412009-05-13 21:29:45 +0000332 if group_name:
333 job_data['host_group_name'] = group_name
jadmanski10646442008-08-13 14:05:21 +0000334
mbligh0d0f67d2009-11-06 03:15:03 +0000335 # only write these keyvals out on the first job in a resultdir
336 if 'job_started' not in utils.read_keyval(self.resultdir):
Prathmesh Prabhu87fecd12017-07-06 10:13:43 -0700337 job_data.update(self._get_job_data())
mbligh0d0f67d2009-11-06 03:15:03 +0000338 utils.write_keyval(self.resultdir, job_data)
339
mbligh0d0f67d2009-11-06 03:15:03 +0000340 self.pkgmgr = packages.PackageManager(
341 self.autodir, run_function_dargs={'timeout':600})
showard21baa452008-10-21 00:08:39 +0000342
jadmanski550fdc22008-11-20 16:32:08 +0000343 self._register_subcommand_hooks()
344
Prathmesh Prabhu6f1d4782018-04-20 11:21:56 -0700345 # We no longer parse results as part of the server_job. These arguments
346 # can't be dropped yet because clients haven't all be cleaned up yet.
347 self.num_tests_run = -1
348 self.num_tests_failed = -1
349
jadmanski2a89dac2010-06-11 14:32:58 +0000350 # set up the status logger
jadmanski52053632010-06-11 21:08:10 +0000351 self._indenter = status_indenter()
jadmanski2a89dac2010-06-11 14:32:58 +0000352 self._logger = base_job.status_logger(
jadmanski52053632010-06-11 21:08:10 +0000353 self, self._indenter, 'status.log', 'status.log',
jadmanski2a89dac2010-06-11 14:32:58 +0000354 record_hook=server_job_record_hook(self))
355
Dan Shib03ea9d2013-08-15 17:13:27 -0700356 # Initialize a flag to indicate DUT failure during the test, e.g.,
357 # unexpected reboot.
358 self.failed_with_device_error = False
359
Hidehiko Abe06893302017-06-24 07:32:38 +0900360 self._connection_pool = ssh_multiplex.ConnectionPool()
361
Daniel Erat8184ad22018-04-04 18:29:48 -0700362 # List of functions to run after the main job function.
363 self._post_run_hooks = []
364
Dan Shi70647ca2015-07-16 22:52:35 -0700365 self.parent_job_id = parent_job_id
Simran Basi1bf60eb2015-12-01 16:39:29 -0800366 self.in_lab = in_lab
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -0700367 self.machine_dict_list = machine_dict_list
368 for machine_dict in self.machine_dict_list:
369 machine_dict['connection_pool'] = self._connection_pool
Dan Shi70647ca2015-07-16 22:52:35 -0700370
Richard Barnette9aec6932016-06-03 13:31:46 -0700371 # TODO(jrbarnette) The harness attribute is only relevant to
372 # client jobs, but it's required to be present, or we will fail
373 # server job unit tests. Yes, really.
Richard Barnetteab9769f2016-06-01 15:01:44 -0700374 #
Richard Barnette9aec6932016-06-03 13:31:46 -0700375 # TODO(jrbarnette) The utility of the 'harness' attribute even
376 # to client jobs is suspect. Probably, we should remove it.
Richard Barnetteab9769f2016-06-01 15:01:44 -0700377 self.harness = None
Richard Barnetteab9769f2016-06-01 15:01:44 -0700378
Allen Li5913d8c2018-09-25 17:56:31 -0700379 # TODO(ayatane): fast and max_result_size_KB are not set for
380 # client_trampoline jobs.
381 if control and not use_client_trampoline:
Xixuan Wucd36ae02017-11-10 13:51:00 -0800382 parsed_control = control_data.parse_control(
383 control, raise_warnings=False)
384 self.fast = parsed_control.fast
385 self.max_result_size_KB = parsed_control.max_result_size_KB
Dan Shif53d1262017-06-19 11:25:25 -0700386 else:
Xixuan Wucd36ae02017-11-10 13:51:00 -0800387 self.fast = False
Dan Shif53d1262017-06-19 11:25:25 -0700388 # Set the maximum result size to be the default specified in
389 # global config, if the job has no control file associated.
390 self.max_result_size_KB = control_data.DEFAULT_MAX_RESULT_SIZE_KB
391
mbligh0d0f67d2009-11-06 03:15:03 +0000392
393 @classmethod
394 def _find_base_directories(cls):
395 """
396 Determine locations of autodir, clientdir and serverdir. Assumes
397 that this file is located within serverdir and uses __file__ along
398 with relative paths to resolve the location.
399 """
400 serverdir = os.path.abspath(os.path.dirname(__file__))
401 autodir = os.path.normpath(os.path.join(serverdir, '..'))
402 clientdir = os.path.join(autodir, 'client')
403 return autodir, clientdir, serverdir
404
405
Scott Zawalski91493c82013-01-25 16:15:20 -0500406 def _find_resultdir(self, resultdir, *args, **dargs):
mbligh0d0f67d2009-11-06 03:15:03 +0000407 """
408 Determine the location of resultdir. For server jobs we expect one to
409 always be explicitly passed in to __init__, so just return that.
410 """
411 if resultdir:
412 return os.path.normpath(resultdir)
413 else:
414 return None
415
jadmanski550fdc22008-11-20 16:32:08 +0000416
jadmanski2a89dac2010-06-11 14:32:58 +0000417 def _get_status_logger(self):
418 """Return a reference to the status logger."""
419 return self._logger
420
421
jadmanskie432dd22009-01-30 15:04:51 +0000422 @staticmethod
423 def _load_control_file(path):
424 f = open(path)
425 try:
426 control_file = f.read()
427 finally:
428 f.close()
429 return re.sub('\r', '', control_file)
430
431
jadmanski550fdc22008-11-20 16:32:08 +0000432 def _register_subcommand_hooks(self):
mbligh2b92b862008-11-22 13:25:32 +0000433 """
434 Register some hooks into the subcommand modules that allow us
435 to properly clean up self.hosts created in forked subprocesses.
436 """
jadmanski550fdc22008-11-20 16:32:08 +0000437 def on_fork(cmd):
438 self._existing_hosts_on_fork = set(self.hosts)
439 def on_join(cmd):
440 new_hosts = self.hosts - self._existing_hosts_on_fork
441 for host in new_hosts:
442 host.close()
443 subcommand.subcommand.register_fork_hook(on_fork)
444 subcommand.subcommand.register_join_hook(on_join)
445
jadmanski10646442008-08-13 14:05:21 +0000446
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700447 # TODO crbug.com/285395 add a kwargs parameter.
448 def _make_namespace(self):
449 """Create a namespace dictionary to be passed along to control file.
450
451 Creates a namespace argument populated with standard values:
452 machines, job, ssh_user, ssh_port, ssh_pass, ssh_verbosity_flag,
453 and ssh_options.
454 """
Simran Basi1bf60eb2015-12-01 16:39:29 -0800455 namespace = {'machines' : self.machine_dict_list,
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700456 'job' : self,
457 'ssh_user' : self._ssh_user,
458 'ssh_port' : self._ssh_port,
459 'ssh_pass' : self._ssh_pass,
460 'ssh_verbosity_flag' : self._ssh_verbosity_flag,
461 'ssh_options' : self._ssh_options}
462 return namespace
463
jadmanski10646442008-08-13 14:05:21 +0000464
J. Richard Barnettec2d99cf2015-11-18 12:46:15 -0800465 def cleanup(self, labels):
Fang Dengad78aca2014-10-02 18:15:46 -0700466 """Cleanup machines.
467
468 @param labels: Comma separated job labels, will be used to
469 determine special task actions.
470 """
471 if not self.machines:
472 raise error.AutoservError('No machines specified to cleanup')
473 if self.resultdir:
474 os.chdir(self.resultdir)
475
476 namespace = self._make_namespace()
477 namespace.update({'job_labels': labels, 'args': ''})
478 self._execute_code(CLEANUP_CONTROL_FILE, namespace, protect=False)
479
480
J. Richard Barnettec2d99cf2015-11-18 12:46:15 -0800481 def verify(self, labels):
Fang Dengad78aca2014-10-02 18:15:46 -0700482 """Verify machines are all ssh-able.
483
484 @param labels: Comma separated job labels, will be used to
485 determine special task actions.
486 """
jadmanski10646442008-08-13 14:05:21 +0000487 if not self.machines:
mbligh084bc172008-10-18 14:02:45 +0000488 raise error.AutoservError('No machines specified to verify')
mbligh0fce4112008-11-27 00:37:17 +0000489 if self.resultdir:
490 os.chdir(self.resultdir)
Fang Dengad78aca2014-10-02 18:15:46 -0700491
492 namespace = self._make_namespace()
493 namespace.update({'job_labels': labels, 'args': ''})
494 self._execute_code(VERIFY_CONTROL_FILE, namespace, protect=False)
jadmanski10646442008-08-13 14:05:21 +0000495
496
J. Richard Barnettec2d99cf2015-11-18 12:46:15 -0800497 def reset(self, labels):
Fang Dengad78aca2014-10-02 18:15:46 -0700498 """Reset machines by first cleanup then verify each machine.
499
500 @param labels: Comma separated job labels, will be used to
501 determine special task actions.
502 """
Dan Shi07e09af2013-04-12 09:31:29 -0700503 if not self.machines:
504 raise error.AutoservError('No machines specified to reset.')
505 if self.resultdir:
506 os.chdir(self.resultdir)
507
Fang Dengad78aca2014-10-02 18:15:46 -0700508 namespace = self._make_namespace()
509 namespace.update({'job_labels': labels, 'args': ''})
510 self._execute_code(RESET_CONTROL_FILE, namespace, protect=False)
Dan Shi07e09af2013-04-12 09:31:29 -0700511
512
J. Richard Barnettec2d99cf2015-11-18 12:46:15 -0800513 def repair(self, labels):
Fang Dengad78aca2014-10-02 18:15:46 -0700514 """Repair machines.
515
Fang Dengad78aca2014-10-02 18:15:46 -0700516 @param labels: Comma separated job labels, will be used to
517 determine special task actions.
518 """
jadmanski10646442008-08-13 14:05:21 +0000519 if not self.machines:
520 raise error.AutoservError('No machines specified to repair')
mbligh0fce4112008-11-27 00:37:17 +0000521 if self.resultdir:
522 os.chdir(self.resultdir)
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700523
524 namespace = self._make_namespace()
J. Richard Barnettec2d99cf2015-11-18 12:46:15 -0800525 namespace.update({'job_labels': labels, 'args': ''})
mbligh0931b0a2009-04-08 17:44:48 +0000526 self._execute_code(REPAIR_CONTROL_FILE, namespace, protect=False)
jadmanski10646442008-08-13 14:05:21 +0000527
528
Alex Millercb79ba72013-05-29 14:43:00 -0700529 def provision(self, labels):
530 """
531 Provision all hosts to match |labels|.
532
533 @param labels: A comma seperated string of labels to provision the
534 host to.
535
536 """
Alex Millercb79ba72013-05-29 14:43:00 -0700537 control = self._load_control_file(PROVISION_CONTROL_FILE)
Alex Miller686fca82014-04-23 17:21:13 -0700538 self.run(control=control, job_labels=labels)
Alex Millercb79ba72013-05-29 14:43:00 -0700539
540
jadmanski10646442008-08-13 14:05:21 +0000541 def precheck(self):
542 """
543 perform any additional checks in derived classes.
544 """
545 pass
546
547
548 def enable_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000549 """
550 Start or restart external logging mechanism.
jadmanski10646442008-08-13 14:05:21 +0000551 """
552 pass
553
554
555 def disable_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000556 """
557 Pause or stop external logging mechanism.
jadmanski10646442008-08-13 14:05:21 +0000558 """
559 pass
560
561
562 def use_external_logging(self):
mbligh2b92b862008-11-22 13:25:32 +0000563 """
564 Return True if external logging should be used.
jadmanski10646442008-08-13 14:05:21 +0000565 """
566 return False
567
568
mbligh415dc212009-06-15 21:53:34 +0000569 def _make_parallel_wrapper(self, function, machines, log):
570 """Wrap function as appropriate for calling by parallel_simple."""
Dan Shi5adbce82015-12-11 10:21:40 -0800571 # machines could be a list of dictionaries, e.g.,
572 # [{'host_attributes': {}, 'hostname': '100.96.51.226'}]
Prathmesh Prabhu20ea1062017-07-06 10:23:42 -0700573 # The dictionary is generated in server_job.__init__, refer to
Dan Shi5adbce82015-12-11 10:21:40 -0800574 # variable machine_dict_list, then passed in with namespace, see method
Prathmesh Prabhu20ea1062017-07-06 10:23:42 -0700575 # server_job._make_namespace.
Dan Shi5adbce82015-12-11 10:21:40 -0800576 # To compare the machinese to self.machines, which is a list of machine
577 # hostname, we need to convert machines back to a list of hostnames.
Dan Shi5adbce82015-12-11 10:21:40 -0800578 if (machines and isinstance(machines, list)
579 and isinstance(machines[0], dict)):
580 machines = [m['hostname'] for m in machines]
Prathmesh Prabhu6f1d4782018-04-20 11:21:56 -0700581 if len(machines) > 1 and log:
jadmanski10646442008-08-13 14:05:21 +0000582 def wrapper(machine):
Simran Basi1bf60eb2015-12-01 16:39:29 -0800583 hostname = server_utils.get_hostname_from_machine(machine)
584 self.push_execution_context(hostname)
jadmanski609a5f42008-08-26 20:52:42 +0000585 os.chdir(self.resultdir)
Simran Basi1bf60eb2015-12-01 16:39:29 -0800586 machine_data = {'hostname' : hostname,
mbligh0d0f67d2009-11-06 03:15:03 +0000587 'status_version' : str(self._STATUS_VERSION)}
mbligh838d82d2009-03-11 17:14:31 +0000588 utils.write_keyval(self.resultdir, machine_data)
jadmanski10646442008-08-13 14:05:21 +0000589 result = function(machine)
590 return result
591 else:
592 wrapper = function
mbligh415dc212009-06-15 21:53:34 +0000593 return wrapper
594
595
596 def parallel_simple(self, function, machines, log=True, timeout=None,
597 return_results=False):
598 """
599 Run 'function' using parallel_simple, with an extra wrapper to handle
600 the necessary setup for continuous parsing, if possible. If continuous
601 parsing is already properly initialized then this should just work.
602
603 @param function: A callable to run in parallel given each machine.
604 @param machines: A list of machine names to be passed one per subcommand
605 invocation of function.
606 @param log: If True, output will be written to output in a subdirectory
607 named after each machine.
608 @param timeout: Seconds after which the function call should timeout.
609 @param return_results: If True instead of an AutoServError being raised
610 on any error a list of the results|exceptions from the function
611 called on each arg is returned. [default: False]
612
613 @raises error.AutotestError: If any of the functions failed.
614 """
615 wrapper = self._make_parallel_wrapper(function, machines, log)
Prathmesh Prabhud08c86b2017-07-21 16:14:33 -0700616 return subcommand.parallel_simple(
617 wrapper, machines,
618 subdir_name_constructor=server_utils.get_hostname_from_machine,
619 log=log, timeout=timeout, return_results=return_results)
mbligh415dc212009-06-15 21:53:34 +0000620
621
622 def parallel_on_machines(self, function, machines, timeout=None):
623 """
showardcd5fac42009-07-06 20:19:43 +0000624 @param function: Called in parallel with one machine as its argument.
mbligh415dc212009-06-15 21:53:34 +0000625 @param machines: A list of machines to call function(machine) on.
626 @param timeout: Seconds after which the function call should timeout.
627
628 @returns A list of machines on which function(machine) returned
629 without raising an exception.
630 """
showardcd5fac42009-07-06 20:19:43 +0000631 results = self.parallel_simple(function, machines, timeout=timeout,
mbligh415dc212009-06-15 21:53:34 +0000632 return_results=True)
633 success_machines = []
634 for result, machine in itertools.izip(results, machines):
635 if not isinstance(result, Exception):
636 success_machines.append(machine)
637 return success_machines
jadmanski10646442008-08-13 14:05:21 +0000638
639
Prathmesh Prabhu20ea1062017-07-06 10:23:42 -0700640 def record_skipped_test(self, skipped_test, message=None):
641 """Insert a failure record into status.log for this test."""
642 msg = message
643 if msg is None:
644 msg = 'No valid machines found for test %s.' % skipped_test
645 logging.info(msg)
646 self.record('START', None, skipped_test.test_name)
647 self.record('INFO', None, skipped_test.test_name, msg)
648 self.record('END TEST_NA', None, skipped_test.test_name, msg)
649
650
Allen Liab020912016-09-19 18:07:41 -0700651 def _has_failed_tests(self):
652 """Parse status log for failed tests.
653
654 This checks the current working directory and is intended only for use
655 by the run() method.
656
657 @return boolean
658 """
659 path = os.getcwd()
660
661 # TODO(ayatane): Copied from tko/parse.py. Needs extensive refactor to
662 # make code reuse plausible.
663 job_keyval = tko_models.job.read_keyval(path)
664 status_version = job_keyval.get("status_version", 0)
665
666 # parse out the job
Luigi Semenzatoe7064812017-02-03 14:47:59 -0800667 parser = parser_lib.parser(status_version)
Allen Liab020912016-09-19 18:07:41 -0700668 job = parser.make_job(path)
669 status_log = os.path.join(path, "status.log")
670 if not os.path.exists(status_log):
671 status_log = os.path.join(path, "status")
672 if not os.path.exists(status_log):
673 logging.warning("! Unable to parse job, no status file")
674 return True
675
676 # parse the status logs
677 status_lines = open(status_log).readlines()
678 parser.start(job)
679 tests = parser.end(status_lines)
680
681 # parser.end can return the same object multiple times, so filter out
682 # dups
683 job.tests = []
684 already_added = set()
685 for test in tests:
686 if test not in already_added:
687 already_added.add(test)
688 job.tests.append(test)
689
690 failed = False
691 for test in job.tests:
692 # The current job is still running and shouldn't count as failed.
693 # The parser will fail to parse the exit status of the job since it
694 # hasn't exited yet (this running right now is the job).
695 failed = failed or (test.status != 'GOOD'
696 and not _is_current_server_job(test))
697 return failed
698
699
700 def _collect_crashes(self, namespace, collect_crashinfo):
701 """Collect crashes.
702
703 @param namespace: namespace dict.
704 @param collect_crashinfo: whether to collect crashinfo in addition to
705 dumps
706 """
707 if collect_crashinfo:
708 # includes crashdumps
709 crash_control_file = CRASHINFO_CONTROL_FILE
710 else:
711 crash_control_file = CRASHDUMPS_CONTROL_FILE
712 self._execute_code(crash_control_file, namespace)
713
714
mbligh0d0f67d2009-11-06 03:15:03 +0000715 _USE_TEMP_DIR = object()
Richard Barnette71854c72018-03-30 14:22:09 -0700716 def run(self, collect_crashdumps=True, namespace={}, control=None,
beepscb6f1e22013-06-28 19:14:10 -0700717 control_file_dir=None, verify_job_repo_url=False,
Alex Millerca76bcc2014-04-18 18:47:28 -0700718 only_collect_crashinfo=False, skip_crash_collection=False,
Dan Shib669cbd2013-09-13 11:17:17 -0700719 job_labels='', use_packaging=True):
jadmanskifb9c0fa2009-04-29 17:39:16 +0000720 # for a normal job, make sure the uncollected logs file exists
721 # for a crashinfo-only run it should already exist, bail out otherwise
jadmanski648c39f2010-03-19 17:38:01 +0000722 created_uncollected_logs = False
Alex Miller45554f32013-08-13 16:48:29 -0700723 logging.info("I am PID %s", os.getpid())
mbligh0d0f67d2009-11-06 03:15:03 +0000724 if self.resultdir and not os.path.exists(self._uncollected_log_file):
jadmanskifb9c0fa2009-04-29 17:39:16 +0000725 if only_collect_crashinfo:
726 # if this is a crashinfo-only run, and there were no existing
727 # uncollected logs, just bail out early
728 logging.info("No existing uncollected logs, "
729 "skipping crashinfo collection")
730 return
731 else:
mbligh0d0f67d2009-11-06 03:15:03 +0000732 log_file = open(self._uncollected_log_file, "w")
jadmanskifb9c0fa2009-04-29 17:39:16 +0000733 pickle.dump([], log_file)
734 log_file.close()
jadmanski648c39f2010-03-19 17:38:01 +0000735 created_uncollected_logs = True
jadmanskifb9c0fa2009-04-29 17:39:16 +0000736
jadmanski10646442008-08-13 14:05:21 +0000737 # use a copy so changes don't affect the original dictionary
738 namespace = namespace.copy()
739 machines = self.machines
jadmanskie432dd22009-01-30 15:04:51 +0000740 if control is None:
jadmanski02a3ba22009-11-13 20:47:27 +0000741 if self.control is None:
742 control = ''
Allen Li5913d8c2018-09-25 17:56:31 -0700743 elif self._use_client_trampoline:
744 control = self._load_control_file(
745 CLIENT_TRAMPOLINE_CONTROL_FILE)
746 # repr of a string is safe for eval.
747 control = (('trampoline_testname = %r\n' % str(self.control))
748 + control)
jadmanski02a3ba22009-11-13 20:47:27 +0000749 else:
750 control = self._load_control_file(self.control)
jadmanskie432dd22009-01-30 15:04:51 +0000751 if control_file_dir is None:
752 control_file_dir = self.resultdir
jadmanski10646442008-08-13 14:05:21 +0000753
754 self.aborted = False
Aviv Keshetc5947fa2013-09-04 14:06:29 -0700755 namespace.update(self._make_namespace())
Prathmesh Prabhu20ea1062017-07-06 10:23:42 -0700756 namespace.update({
757 'args': self.args,
758 'job_labels': job_labels,
759 'gtest_runner': site_gtest_runner.gtest_runner(),
760 })
jadmanski10646442008-08-13 14:05:21 +0000761 test_start_time = int(time.time())
762
mbligh80e1eba2008-11-19 00:26:18 +0000763 if self.resultdir:
764 os.chdir(self.resultdir)
jadmanski779bd292009-03-19 17:33:33 +0000765 # touch status.log so that the parser knows a job is running here
jadmanski382303a2009-04-21 19:53:39 +0000766 open(self.get_status_log_path(), 'a').close()
mbligh80e1eba2008-11-19 00:26:18 +0000767 self.enable_external_logging()
jadmanskie432dd22009-01-30 15:04:51 +0000768
jadmanskicdd0c402008-09-19 21:21:31 +0000769 collect_crashinfo = True
mblighaebe3b62008-12-22 14:45:40 +0000770 temp_control_file_dir = None
jadmanski10646442008-08-13 14:05:21 +0000771 try:
showardcf8d4922009-10-14 16:08:39 +0000772 try:
Xixuan Wu60325a62017-11-20 17:21:15 -0800773 if not self.fast:
774 with metrics.SecondsTimer(
775 'chromeos/autotest/job/get_network_stats',
776 fields = {'stage': 'start'}):
777 namespace['network_stats_label'] = 'at-start'
778 self._execute_code(GET_NETWORK_STATS_CONTROL_FILE,
779 namespace)
780
showardcf8d4922009-10-14 16:08:39 +0000781 if only_collect_crashinfo:
782 return
783
beepscb6f1e22013-06-28 19:14:10 -0700784 # If the verify_job_repo_url option is set but we're unable
785 # to actually verify that the job_repo_url contains the autotest
786 # package, this job will fail.
787 if verify_job_repo_url:
788 self._execute_code(VERIFY_JOB_REPO_URL_CONTROL_FILE,
Dan Shicf4d2032015-03-12 15:04:21 -0700789 namespace)
beepscb6f1e22013-06-28 19:14:10 -0700790 else:
791 logging.warning('Not checking if job_repo_url contains '
792 'autotest packages on %s', machines)
793
jadmanskidef0c3c2009-03-25 20:07:10 +0000794 # determine the dir to write the control files to
795 cfd_specified = (control_file_dir
mbligh0d0f67d2009-11-06 03:15:03 +0000796 and control_file_dir is not self._USE_TEMP_DIR)
jadmanskidef0c3c2009-03-25 20:07:10 +0000797 if cfd_specified:
798 temp_control_file_dir = None
799 else:
800 temp_control_file_dir = tempfile.mkdtemp(
801 suffix='temp_control_file_dir')
802 control_file_dir = temp_control_file_dir
803 server_control_file = os.path.join(control_file_dir,
mblighe0cbc912010-03-11 18:03:07 +0000804 self._control_filename)
jadmanskidef0c3c2009-03-25 20:07:10 +0000805 client_control_file = os.path.join(control_file_dir,
806 CLIENT_CONTROL_FILENAME)
mbligh0d0f67d2009-11-06 03:15:03 +0000807 if self._client:
jadmanskidef0c3c2009-03-25 20:07:10 +0000808 namespace['control'] = control
809 utils.open_write_close(client_control_file, control)
mblighfeac0102009-04-28 18:31:12 +0000810 shutil.copyfile(CLIENT_WRAPPER_CONTROL_FILE,
811 server_control_file)
jadmanskidef0c3c2009-03-25 20:07:10 +0000812 else:
813 utils.open_write_close(server_control_file, control)
Dan Shicf4d2032015-03-12 15:04:21 -0700814
mbligh26f0d882009-06-22 18:30:01 +0000815 logging.info("Processing control file")
Dan Shib669cbd2013-09-13 11:17:17 -0700816 namespace['use_packaging'] = use_packaging
Dan Shicf4d2032015-03-12 15:04:21 -0700817 self._execute_code(server_control_file, namespace)
mbligh26f0d882009-06-22 18:30:01 +0000818 logging.info("Finished processing control file")
jadmanski10646442008-08-13 14:05:21 +0000819
Dan Shib03ea9d2013-08-15 17:13:27 -0700820 # If no device error occured, no need to collect crashinfo.
821 collect_crashinfo = self.failed_with_device_error
Prathmesh Prabhu0afec422017-02-02 15:32:27 -0800822 except Exception as e:
showardcf8d4922009-10-14 16:08:39 +0000823 try:
824 logging.exception(
825 'Exception escaped control file, job aborting:')
Fang Dengc439b6a2015-12-10 16:43:59 -0800826 reason = re.sub(base_job.status_log_entry.BAD_CHAR_REGEX,
827 ' ', str(e))
Eric Li6f27d4f2010-09-29 10:55:17 -0700828 self.record('INFO', None, None, str(e),
Fang Dengc439b6a2015-12-10 16:43:59 -0800829 {'job_abort_reason': reason})
showardcf8d4922009-10-14 16:08:39 +0000830 except:
831 pass # don't let logging exceptions here interfere
832 raise
jadmanski10646442008-08-13 14:05:21 +0000833 finally:
mblighaebe3b62008-12-22 14:45:40 +0000834 if temp_control_file_dir:
jadmanskie432dd22009-01-30 15:04:51 +0000835 # Clean up temp directory used for copies of the control files
mblighaebe3b62008-12-22 14:45:40 +0000836 try:
837 shutil.rmtree(temp_control_file_dir)
Prathmesh Prabhu0afec422017-02-02 15:32:27 -0800838 except Exception as e:
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700839 logging.warning('Could not remove temp directory %s: %s',
mblighe7d9c602009-07-02 19:02:33 +0000840 temp_control_file_dir, e)
jadmanskie432dd22009-01-30 15:04:51 +0000841
jadmanskicdd0c402008-09-19 21:21:31 +0000842 if machines and (collect_crashdumps or collect_crashinfo):
Xixuan Wuf4e857a2017-11-21 15:59:02 -0800843 if skip_crash_collection or self.fast:
Christopher Wileyf594c5e2013-07-03 18:25:30 -0700844 logging.info('Skipping crash dump/info collection '
845 'as requested.')
jadmanskicdd0c402008-09-19 21:21:31 +0000846 else:
Xixuan Wuf4e857a2017-11-21 15:59:02 -0800847 with metrics.SecondsTimer(
848 'chromeos/autotest/job/collect_crashinfo'):
849 namespace['test_start_time'] = test_start_time
850 # Remove crash files for passing tests.
851 # TODO(ayatane): Tests that create crash files should be
852 # reported.
853 namespace['has_failed_tests'] = self._has_failed_tests()
854 self._collect_crashes(namespace, collect_crashinfo)
jadmanski10646442008-08-13 14:05:21 +0000855 self.disable_external_logging()
Chris Sosaf4d43ff2012-10-30 11:21:05 -0700856 if self._uncollected_log_file and created_uncollected_logs:
857 os.remove(self._uncollected_log_file)
Xixuan Wu60325a62017-11-20 17:21:15 -0800858
859 if not self.fast:
860 with metrics.SecondsTimer(
861 'chromeos/autotest/job/get_network_stats',
862 fields = {'stage': 'end'}):
863 namespace['network_stats_label'] = 'at-end'
864 self._execute_code(GET_NETWORK_STATS_CONTROL_FILE,
865 namespace)
jadmanski10646442008-08-13 14:05:21 +0000866
867
868 def run_test(self, url, *args, **dargs):
mbligh2b92b862008-11-22 13:25:32 +0000869 """
870 Summon a test object and run it.
jadmanski10646442008-08-13 14:05:21 +0000871
872 tag
873 tag to add to testname
874 url
875 url of the test to run
876 """
Christopher Wiley8a91f232013-07-09 11:02:27 -0700877 if self._disable_sysinfo:
878 dargs['disable_sysinfo'] = True
879
mblighfc3da5b2010-01-06 18:37:22 +0000880 group, testname = self.pkgmgr.get_package_name(url, 'test')
881 testname, subdir, tag = self._build_tagged_test_name(testname, dargs)
882 outputdir = self._make_test_outputdir(subdir)
jadmanski10646442008-08-13 14:05:21 +0000883
884 def group_func():
885 try:
886 test.runtest(self, url, tag, args, dargs)
Prathmesh Prabhu0afec422017-02-02 15:32:27 -0800887 except error.TestBaseException as e:
jadmanski10646442008-08-13 14:05:21 +0000888 self.record(e.exit_status, subdir, testname, str(e))
889 raise
Prathmesh Prabhu0afec422017-02-02 15:32:27 -0800890 except Exception as e:
jadmanski10646442008-08-13 14:05:21 +0000891 info = str(e) + "\n" + traceback.format_exc()
892 self.record('FAIL', subdir, testname, info)
893 raise
894 else:
mbligh2b92b862008-11-22 13:25:32 +0000895 self.record('GOOD', subdir, testname, 'completed successfully')
jadmanskide292df2008-08-26 20:51:14 +0000896
Prathmesh Prabhu271962f2017-02-02 17:22:37 -0800897 try:
898 result = self._run_group(testname, subdir, group_func)
899 except error.TestBaseException as e:
jadmanskide292df2008-08-26 20:51:14 +0000900 return False
jadmanskide292df2008-08-26 20:51:14 +0000901 else:
902 return True
jadmanski10646442008-08-13 14:05:21 +0000903
904
905 def _run_group(self, name, subdir, function, *args, **dargs):
Prathmesh Prabhu271962f2017-02-02 17:22:37 -0800906 """Underlying method for running something inside of a group."""
jadmanskide292df2008-08-26 20:51:14 +0000907 result, exc_info = None, None
jadmanski10646442008-08-13 14:05:21 +0000908 try:
909 self.record('START', subdir, name)
jadmanski52053632010-06-11 21:08:10 +0000910 result = function(*args, **dargs)
Prathmesh Prabhu0afec422017-02-02 15:32:27 -0800911 except error.TestBaseException as e:
jadmanskib88d6dc2009-01-10 00:33:18 +0000912 self.record("END %s" % e.exit_status, subdir, name)
Prathmesh Prabhu271962f2017-02-02 17:22:37 -0800913 raise
Prathmesh Prabhu0afec422017-02-02 15:32:27 -0800914 except Exception as e:
jadmanski10646442008-08-13 14:05:21 +0000915 err_msg = str(e) + '\n'
916 err_msg += traceback.format_exc()
917 self.record('END ABORT', subdir, name, err_msg)
918 raise error.JobError(name + ' failed\n' + traceback.format_exc())
919 else:
920 self.record('END GOOD', subdir, name)
Daniel Erat8184ad22018-04-04 18:29:48 -0700921 finally:
922 for hook in self._post_run_hooks:
923 hook()
jadmanski10646442008-08-13 14:05:21 +0000924
Prathmesh Prabhu271962f2017-02-02 17:22:37 -0800925 return result
jadmanski10646442008-08-13 14:05:21 +0000926
927
928 def run_group(self, function, *args, **dargs):
929 """\
Prathmesh Prabhu271962f2017-02-02 17:22:37 -0800930 @param function: subroutine to run
931 @returns: (result, exc_info). When the call succeeds, result contains
932 the return value of |function| and exc_info is None. If
933 |function| raises an exception, exc_info contains the tuple
934 returned by sys.exc_info(), and result is None.
jadmanski10646442008-08-13 14:05:21 +0000935 """
936
937 name = function.__name__
jadmanski10646442008-08-13 14:05:21 +0000938 # Allow the tag for the group to be specified.
939 tag = dargs.pop('tag', None)
940 if tag:
941 name = tag
942
Prathmesh Prabhu271962f2017-02-02 17:22:37 -0800943 try:
944 result = self._run_group(name, None, function, *args, **dargs)[0]
945 except error.TestBaseException:
946 return None, sys.exc_info()
947 return result, None
jadmanski10646442008-08-13 14:05:21 +0000948
949
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700950 def run_op(self, op, op_func, get_kernel_func):
jadmanski10646442008-08-13 14:05:21 +0000951 """\
952 A specialization of run_group meant specifically for handling
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700953 management operation. Includes support for capturing the kernel version
954 after the operation.
jadmanski10646442008-08-13 14:05:21 +0000955
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700956 Args:
957 op: name of the operation.
958 op_func: a function that carries out the operation (reboot, suspend)
959 get_kernel_func: a function that returns a string
960 representing the kernel version.
jadmanski10646442008-08-13 14:05:21 +0000961 """
jadmanski10646442008-08-13 14:05:21 +0000962 try:
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700963 self.record('START', None, op)
964 op_func()
Prathmesh Prabhu0afec422017-02-02 15:32:27 -0800965 except Exception as e:
jadmanski10646442008-08-13 14:05:21 +0000966 err_msg = str(e) + '\n' + traceback.format_exc()
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700967 self.record('END FAIL', None, op, err_msg)
jadmanski4b51d542009-04-08 14:17:16 +0000968 raise
jadmanski10646442008-08-13 14:05:21 +0000969 else:
970 kernel = get_kernel_func()
Gwendal Grignou7a61d2f2014-05-23 11:05:51 -0700971 self.record('END GOOD', None, op,
Dale Curtis74a314b2011-06-23 14:55:46 -0700972 optional_fields={"kernel": kernel})
jadmanski10646442008-08-13 14:05:21 +0000973
974
jadmanskie432dd22009-01-30 15:04:51 +0000975 def run_control(self, path):
976 """Execute a control file found at path (relative to the autotest
977 path). Intended for executing a control file within a control file,
978 not for running the top-level job control file."""
979 path = os.path.join(self.autodir, path)
980 control_file = self._load_control_file(path)
mbligh0d0f67d2009-11-06 03:15:03 +0000981 self.run(control=control_file, control_file_dir=self._USE_TEMP_DIR)
jadmanskie432dd22009-01-30 15:04:51 +0000982
983
jadmanskic09fc152008-10-15 17:56:59 +0000984 def add_sysinfo_command(self, command, logfile=None, on_every_test=False):
mbligh4395bbd2009-03-25 19:34:17 +0000985 self._add_sysinfo_loggable(sysinfo.command(command, logf=logfile),
jadmanskic09fc152008-10-15 17:56:59 +0000986 on_every_test)
987
988
989 def add_sysinfo_logfile(self, file, on_every_test=False):
990 self._add_sysinfo_loggable(sysinfo.logfile(file), on_every_test)
991
992
993 def _add_sysinfo_loggable(self, loggable, on_every_test):
994 if on_every_test:
995 self.sysinfo.test_loggables.add(loggable)
996 else:
997 self.sysinfo.boot_loggables.add(loggable)
998
999
jadmanski10646442008-08-13 14:05:21 +00001000 def _read_warnings(self):
jadmanskif37df842009-02-11 00:03:26 +00001001 """Poll all the warning loggers and extract any new warnings that have
1002 been logged. If the warnings belong to a category that is currently
1003 disabled, this method will discard them and they will no longer be
1004 retrievable.
1005
1006 Returns a list of (timestamp, message) tuples, where timestamp is an
1007 integer epoch timestamp."""
jadmanski10646442008-08-13 14:05:21 +00001008 warnings = []
1009 while True:
1010 # pull in a line of output from every logger that has
1011 # output ready to be read
mbligh2b92b862008-11-22 13:25:32 +00001012 loggers, _, _ = select.select(self.warning_loggers, [], [], 0)
jadmanski10646442008-08-13 14:05:21 +00001013 closed_loggers = set()
1014 for logger in loggers:
1015 line = logger.readline()
1016 # record any broken pipes (aka line == empty)
1017 if len(line) == 0:
1018 closed_loggers.add(logger)
1019 continue
jadmanskif37df842009-02-11 00:03:26 +00001020 # parse out the warning
1021 timestamp, msgtype, msg = line.split('\t', 2)
1022 timestamp = int(timestamp)
1023 # if the warning is valid, add it to the results
1024 if self.warning_manager.is_valid(timestamp, msgtype):
1025 warnings.append((timestamp, msg.strip()))
jadmanski10646442008-08-13 14:05:21 +00001026
1027 # stop listening to loggers that are closed
1028 self.warning_loggers -= closed_loggers
1029
1030 # stop if none of the loggers have any output left
1031 if not loggers:
1032 break
1033
1034 # sort into timestamp order
1035 warnings.sort()
1036 return warnings
1037
1038
showardcc929362010-01-25 21:20:41 +00001039 def _unique_subdirectory(self, base_subdirectory_name):
1040 """Compute a unique results subdirectory based on the given name.
1041
1042 Appends base_subdirectory_name with a number as necessary to find a
1043 directory name that doesn't already exist.
1044 """
1045 subdirectory = base_subdirectory_name
1046 counter = 1
1047 while os.path.exists(os.path.join(self.resultdir, subdirectory)):
1048 subdirectory = base_subdirectory_name + '.' + str(counter)
1049 counter += 1
1050 return subdirectory
1051
1052
jadmanski52053632010-06-11 21:08:10 +00001053 def get_record_context(self):
1054 """Returns an object representing the current job.record context.
1055
1056 The object returned is an opaque object with a 0-arg restore method
1057 which can be called to restore the job.record context (i.e. indentation)
1058 to the current level. The intention is that it should be used when
1059 something external which generate job.record calls (e.g. an autotest
1060 client) can fail catastrophically and the server job record state
1061 needs to be reset to its original "known good" state.
1062
1063 @return: A context object with a 0-arg restore() method."""
1064 return self._indenter.get_context()
1065
1066
showardcc929362010-01-25 21:20:41 +00001067 def record_summary(self, status_code, test_name, reason='', attributes=None,
1068 distinguishing_attributes=(), child_test_ids=None):
1069 """Record a summary test result.
1070
1071 @param status_code: status code string, see
1072 common_lib.log.is_valid_status()
1073 @param test_name: name of the test
1074 @param reason: (optional) string providing detailed reason for test
1075 outcome
1076 @param attributes: (optional) dict of string keyvals to associate with
1077 this result
1078 @param distinguishing_attributes: (optional) list of attribute names
1079 that should be used to distinguish identically-named test
1080 results. These attributes should be present in the attributes
1081 parameter. This is used to generate user-friendly subdirectory
1082 names.
1083 @param child_test_ids: (optional) list of test indices for test results
1084 used in generating this result.
1085 """
1086 subdirectory_name_parts = [test_name]
1087 for attribute in distinguishing_attributes:
1088 assert attributes
1089 assert attribute in attributes, '%s not in %s' % (attribute,
1090 attributes)
1091 subdirectory_name_parts.append(attributes[attribute])
1092 base_subdirectory_name = '.'.join(subdirectory_name_parts)
1093
1094 subdirectory = self._unique_subdirectory(base_subdirectory_name)
1095 subdirectory_path = os.path.join(self.resultdir, subdirectory)
1096 os.mkdir(subdirectory_path)
1097
1098 self.record(status_code, subdirectory, test_name,
1099 status=reason, optional_fields={'is_summary': True})
1100
1101 if attributes:
1102 utils.write_keyval(subdirectory_path, attributes)
1103
1104 if child_test_ids:
1105 ids_string = ','.join(str(test_id) for test_id in child_test_ids)
1106 summary_data = {'child_test_ids': ids_string}
1107 utils.write_keyval(os.path.join(subdirectory_path, 'summary_data'),
1108 summary_data)
1109
1110
Daniel Erat8184ad22018-04-04 18:29:48 -07001111 def add_post_run_hook(self, hook):
1112 """
1113 Registers a hook to run after the main job function.
1114
1115 This provides a mechanism by which tests that perform multiple tests of
1116 their own can write additional top-level results to the TKO status.log
1117 file.
1118
1119 @param hook: Function to invoke (without any args) after the main job
1120 function completes and the job status is logged.
1121 """
1122 self._post_run_hooks.append(hook)
1123
1124
jadmanski16a7ff72009-04-01 18:19:53 +00001125 def disable_warnings(self, warning_type):
jadmanskif37df842009-02-11 00:03:26 +00001126 self.warning_manager.disable_warnings(warning_type)
jadmanski16a7ff72009-04-01 18:19:53 +00001127 self.record("INFO", None, None,
1128 "disabling %s warnings" % warning_type,
1129 {"warnings.disable": warning_type})
jadmanskif37df842009-02-11 00:03:26 +00001130
1131
jadmanski16a7ff72009-04-01 18:19:53 +00001132 def enable_warnings(self, warning_type):
jadmanskif37df842009-02-11 00:03:26 +00001133 self.warning_manager.enable_warnings(warning_type)
jadmanski16a7ff72009-04-01 18:19:53 +00001134 self.record("INFO", None, None,
1135 "enabling %s warnings" % warning_type,
1136 {"warnings.enable": warning_type})
jadmanskif37df842009-02-11 00:03:26 +00001137
1138
jadmanski779bd292009-03-19 17:33:33 +00001139 def get_status_log_path(self, subdir=None):
1140 """Return the path to the job status log.
1141
1142 @param subdir - Optional paramter indicating that you want the path
1143 to a subdirectory status log.
1144
1145 @returns The path where the status log should be.
1146 """
mbligh210bae62009-04-01 18:33:13 +00001147 if self.resultdir:
1148 if subdir:
1149 return os.path.join(self.resultdir, subdir, "status.log")
1150 else:
1151 return os.path.join(self.resultdir, "status.log")
jadmanski779bd292009-03-19 17:33:33 +00001152 else:
mbligh210bae62009-04-01 18:33:13 +00001153 return None
jadmanski779bd292009-03-19 17:33:33 +00001154
1155
jadmanski6bb32d72009-03-19 20:25:24 +00001156 def _update_uncollected_logs_list(self, update_func):
1157 """Updates the uncollected logs list in a multi-process safe manner.
1158
1159 @param update_func - a function that updates the list of uncollected
1160 logs. Should take one parameter, the list to be updated.
1161 """
Dan Shi07e09af2013-04-12 09:31:29 -07001162 # Skip log collection if file _uncollected_log_file does not exist.
1163 if not (self._uncollected_log_file and
1164 os.path.exists(self._uncollected_log_file)):
1165 return
mbligh0d0f67d2009-11-06 03:15:03 +00001166 if self._uncollected_log_file:
1167 log_file = open(self._uncollected_log_file, "r+")
mbligha788dc42009-03-26 21:10:16 +00001168 fcntl.flock(log_file, fcntl.LOCK_EX)
jadmanski6bb32d72009-03-19 20:25:24 +00001169 try:
1170 uncollected_logs = pickle.load(log_file)
1171 update_func(uncollected_logs)
1172 log_file.seek(0)
1173 log_file.truncate()
1174 pickle.dump(uncollected_logs, log_file)
jadmanski3bff9092009-04-22 18:09:47 +00001175 log_file.flush()
jadmanski6bb32d72009-03-19 20:25:24 +00001176 finally:
1177 fcntl.flock(log_file, fcntl.LOCK_UN)
1178 log_file.close()
1179
1180
1181 def add_client_log(self, hostname, remote_path, local_path):
1182 """Adds a new set of client logs to the list of uncollected logs,
1183 to allow for future log recovery.
1184
1185 @param host - the hostname of the machine holding the logs
1186 @param remote_path - the directory on the remote machine holding logs
1187 @param local_path - the local directory to copy the logs into
1188 """
1189 def update_func(logs_list):
1190 logs_list.append((hostname, remote_path, local_path))
1191 self._update_uncollected_logs_list(update_func)
1192
1193
1194 def remove_client_log(self, hostname, remote_path, local_path):
1195 """Removes a set of client logs from the list of uncollected logs,
1196 to allow for future log recovery.
1197
1198 @param host - the hostname of the machine holding the logs
1199 @param remote_path - the directory on the remote machine holding logs
1200 @param local_path - the local directory to copy the logs into
1201 """
1202 def update_func(logs_list):
1203 logs_list.remove((hostname, remote_path, local_path))
1204 self._update_uncollected_logs_list(update_func)
1205
1206
mbligh0d0f67d2009-11-06 03:15:03 +00001207 def get_client_logs(self):
1208 """Retrieves the list of uncollected logs, if it exists.
1209
1210 @returns A list of (host, remote_path, local_path) tuples. Returns
1211 an empty list if no uncollected logs file exists.
1212 """
1213 log_exists = (self._uncollected_log_file and
1214 os.path.exists(self._uncollected_log_file))
1215 if log_exists:
1216 return pickle.load(open(self._uncollected_log_file))
1217 else:
1218 return []
1219
1220
mbligh084bc172008-10-18 14:02:45 +00001221 def _fill_server_control_namespace(self, namespace, protect=True):
mbligh2b92b862008-11-22 13:25:32 +00001222 """
1223 Prepare a namespace to be used when executing server control files.
mbligh084bc172008-10-18 14:02:45 +00001224
1225 This sets up the control file API by importing modules and making them
1226 available under the appropriate names within namespace.
1227
1228 For use by _execute_code().
1229
1230 Args:
1231 namespace: The namespace dictionary to fill in.
1232 protect: Boolean. If True (the default) any operation that would
1233 clobber an existing entry in namespace will cause an error.
1234 Raises:
1235 error.AutoservError: When a name would be clobbered by import.
1236 """
1237 def _import_names(module_name, names=()):
mbligh2b92b862008-11-22 13:25:32 +00001238 """
1239 Import a module and assign named attributes into namespace.
mbligh084bc172008-10-18 14:02:45 +00001240
1241 Args:
1242 module_name: The string module name.
1243 names: A limiting list of names to import from module_name. If
1244 empty (the default), all names are imported from the module
1245 similar to a "from foo.bar import *" statement.
1246 Raises:
1247 error.AutoservError: When a name being imported would clobber
1248 a name already in namespace.
1249 """
1250 module = __import__(module_name, {}, {}, names)
1251
1252 # No names supplied? Import * from the lowest level module.
1253 # (Ugh, why do I have to implement this part myself?)
1254 if not names:
1255 for submodule_name in module_name.split('.')[1:]:
1256 module = getattr(module, submodule_name)
1257 if hasattr(module, '__all__'):
1258 names = getattr(module, '__all__')
1259 else:
1260 names = dir(module)
1261
1262 # Install each name into namespace, checking to make sure it
1263 # doesn't override anything that already exists.
1264 for name in names:
1265 # Check for conflicts to help prevent future problems.
1266 if name in namespace and protect:
1267 if namespace[name] is not getattr(module, name):
1268 raise error.AutoservError('importing name '
1269 '%s from %s %r would override %r' %
1270 (name, module_name, getattr(module, name),
1271 namespace[name]))
1272 else:
1273 # Encourage cleanliness and the use of __all__ for a
1274 # more concrete API with less surprises on '*' imports.
1275 warnings.warn('%s (%r) being imported from %s for use '
1276 'in server control files is not the '
Richard Barnetteab9769f2016-06-01 15:01:44 -07001277 'first occurrence of that import.' %
mbligh084bc172008-10-18 14:02:45 +00001278 (name, namespace[name], module_name))
1279
1280 namespace[name] = getattr(module, name)
1281
1282
1283 # This is the equivalent of prepending a bunch of import statements to
1284 # the front of the control script.
mbligha2b07dd2009-06-22 18:26:13 +00001285 namespace.update(os=os, sys=sys, logging=logging)
mbligh084bc172008-10-18 14:02:45 +00001286 _import_names('autotest_lib.server',
Richard Barnetteab9769f2016-06-01 15:01:44 -07001287 ('hosts', 'autotest', 'standalone_profiler'))
mbligh084bc172008-10-18 14:02:45 +00001288 _import_names('autotest_lib.server.subcommand',
1289 ('parallel', 'parallel_simple', 'subcommand'))
1290 _import_names('autotest_lib.server.utils',
1291 ('run', 'get_tmp_dir', 'sh_escape', 'parse_machine'))
1292 _import_names('autotest_lib.client.common_lib.error')
1293 _import_names('autotest_lib.client.common_lib.barrier', ('barrier',))
1294
1295 # Inject ourself as the job object into other classes within the API.
1296 # (Yuck, this injection is a gross thing be part of a public API. -gps)
1297 #
Allen Lid5abdab2017-02-07 16:03:43 -08001298 # XXX Autotest does not appear to use .job. Who does?
mbligh084bc172008-10-18 14:02:45 +00001299 namespace['autotest'].Autotest.job = self
1300 # server.hosts.base_classes.Host uses .job.
1301 namespace['hosts'].Host.job = self
Eric Li10222b82010-11-24 09:33:15 -08001302 namespace['hosts'].factory.ssh_user = self._ssh_user
1303 namespace['hosts'].factory.ssh_port = self._ssh_port
1304 namespace['hosts'].factory.ssh_pass = self._ssh_pass
Fang Dengd1c2b732013-08-20 12:59:46 -07001305 namespace['hosts'].factory.ssh_verbosity_flag = (
1306 self._ssh_verbosity_flag)
Aviv Keshetc5947fa2013-09-04 14:06:29 -07001307 namespace['hosts'].factory.ssh_options = self._ssh_options
mbligh084bc172008-10-18 14:02:45 +00001308
1309
1310 def _execute_code(self, code_file, namespace, protect=True):
mbligh2b92b862008-11-22 13:25:32 +00001311 """
1312 Execute code using a copy of namespace as a server control script.
mbligh084bc172008-10-18 14:02:45 +00001313
1314 Unless protect_namespace is explicitly set to False, the dict will not
1315 be modified.
1316
1317 Args:
1318 code_file: The filename of the control file to execute.
1319 namespace: A dict containing names to make available during execution.
1320 protect: Boolean. If True (the default) a copy of the namespace dict
1321 is used during execution to prevent the code from modifying its
1322 contents outside of this function. If False the raw dict is
1323 passed in and modifications will be allowed.
1324 """
1325 if protect:
1326 namespace = namespace.copy()
1327 self._fill_server_control_namespace(namespace, protect=protect)
1328 # TODO: Simplify and get rid of the special cases for only 1 machine.
showard3e66e8c2008-10-27 19:20:51 +00001329 if len(self.machines) > 1:
mbligh084bc172008-10-18 14:02:45 +00001330 machines_text = '\n'.join(self.machines) + '\n'
1331 # Only rewrite the file if it does not match our machine list.
1332 try:
1333 machines_f = open(MACHINES_FILENAME, 'r')
1334 existing_machines_text = machines_f.read()
1335 machines_f.close()
1336 except EnvironmentError:
1337 existing_machines_text = None
1338 if machines_text != existing_machines_text:
1339 utils.open_write_close(MACHINES_FILENAME, machines_text)
1340 execfile(code_file, namespace, namespace)
jadmanski10646442008-08-13 14:05:21 +00001341
1342
mblighfc3da5b2010-01-06 18:37:22 +00001343 def preprocess_client_state(self):
1344 """
1345 Produce a state file for initializing the state of a client job.
1346
1347 Creates a new client state file with all the current server state, as
1348 well as some pre-set client state.
1349
1350 @returns The path of the file the state was written into.
1351 """
1352 # initialize the sysinfo state
1353 self._state.set('client', 'sysinfo', self.sysinfo.serialize())
1354
1355 # dump the state out to a tempfile
1356 fd, file_path = tempfile.mkstemp(dir=self.tmpdir)
1357 os.close(fd)
mbligha2c99492010-01-27 22:59:50 +00001358
1359 # write_to_file doesn't need locking, we exclusively own file_path
mblighfc3da5b2010-01-06 18:37:22 +00001360 self._state.write_to_file(file_path)
1361 return file_path
1362
1363
1364 def postprocess_client_state(self, state_path):
1365 """
1366 Update the state of this job with the state from a client job.
1367
1368 Updates the state of the server side of a job with the final state
1369 of a client job that was run. Updates the non-client-specific state,
1370 pulls in some specific bits from the client-specific state, and then
1371 discards the rest. Removes the state file afterwards
1372
1373 @param state_file A path to the state file from the client.
1374 """
1375 # update the on-disk state
mblighfc3da5b2010-01-06 18:37:22 +00001376 try:
jadmanskib6e7bdb2010-04-13 16:00:39 +00001377 self._state.read_from_file(state_path)
mblighfc3da5b2010-01-06 18:37:22 +00001378 os.remove(state_path)
mbligha2c99492010-01-27 22:59:50 +00001379 except OSError, e:
mblighfc3da5b2010-01-06 18:37:22 +00001380 # ignore file-not-found errors
1381 if e.errno != errno.ENOENT:
1382 raise
jadmanskib6e7bdb2010-04-13 16:00:39 +00001383 else:
1384 logging.debug('Client state file %s not found', state_path)
mblighfc3da5b2010-01-06 18:37:22 +00001385
1386 # update the sysinfo state
1387 if self._state.has('client', 'sysinfo'):
1388 self.sysinfo.deserialize(self._state.get('client', 'sysinfo'))
1389
1390 # drop all the client-specific state
1391 self._state.discard_namespace('client')
1392
1393
mbligh0a883702010-04-21 01:58:34 +00001394 def clear_all_known_hosts(self):
1395 """Clears known hosts files for all AbstractSSHHosts."""
1396 for host in self.hosts:
1397 if isinstance(host, abstract_ssh.AbstractSSHHost):
1398 host.clear_known_hosts()
1399
1400
Hidehiko Abe06893302017-06-24 07:32:38 +09001401 def close(self):
1402 """Closes this job's operation."""
1403
1404 # Use shallow copy, because host.close() internally discards itself.
1405 for host in list(self.hosts):
1406 host.close()
1407 assert not self.hosts
1408 self._connection_pool.shutdown()
1409
1410
Prathmesh Prabhu87fecd12017-07-06 10:13:43 -07001411 def _get_job_data(self):
1412 """Add custom data to the job keyval info.
1413
1414 When multiple machines are used in a job, change the hostname to
1415 the platform of the first machine instead of machine1,machine2,... This
1416 makes the job reports easier to read and keeps the tko_machines table from
1417 growing too large.
1418
1419 Returns:
1420 keyval dictionary with new hostname value, or empty dictionary.
1421 """
1422 job_data = {}
1423 # Only modify hostname on multimachine jobs. Assume all host have the same
1424 # platform.
1425 if len(self.machines) > 1:
1426 # Search through machines for first machine with a platform.
1427 for host in self.machines:
1428 keyval_path = os.path.join(self.resultdir, 'host_keyvals', host)
1429 keyvals = utils.read_keyval(keyval_path)
1430 host_plat = keyvals.get('platform', None)
1431 if not host_plat:
1432 continue
1433 job_data['hostname'] = host_plat
1434 break
1435 return job_data
1436
1437
jadmanskif37df842009-02-11 00:03:26 +00001438class warning_manager(object):
1439 """Class for controlling warning logs. Manages the enabling and disabling
1440 of warnings."""
1441 def __init__(self):
1442 # a map of warning types to a list of disabled time intervals
1443 self.disabled_warnings = {}
1444
1445
1446 def is_valid(self, timestamp, warning_type):
1447 """Indicates if a warning (based on the time it occured and its type)
1448 is a valid warning. A warning is considered "invalid" if this type of
1449 warning was marked as "disabled" at the time the warning occured."""
1450 disabled_intervals = self.disabled_warnings.get(warning_type, [])
1451 for start, end in disabled_intervals:
1452 if timestamp >= start and (end is None or timestamp < end):
1453 return False
1454 return True
1455
1456
1457 def disable_warnings(self, warning_type, current_time_func=time.time):
1458 """As of now, disables all further warnings of this type."""
1459 intervals = self.disabled_warnings.setdefault(warning_type, [])
1460 if not intervals or intervals[-1][1] is not None:
jadmanski16a7ff72009-04-01 18:19:53 +00001461 intervals.append((int(current_time_func()), None))
jadmanskif37df842009-02-11 00:03:26 +00001462
1463
1464 def enable_warnings(self, warning_type, current_time_func=time.time):
1465 """As of now, enables all further warnings of this type."""
1466 intervals = self.disabled_warnings.get(warning_type, [])
1467 if intervals and intervals[-1][1] is None:
jadmanski16a7ff72009-04-01 18:19:53 +00001468 intervals[-1] = (intervals[-1][0], int(current_time_func()))
Paul Pendlebury57593562011-06-15 10:45:49 -07001469
1470
Allen Liab020912016-09-19 18:07:41 -07001471def _is_current_server_job(test):
1472 """Return True if parsed test is the currently running job.
1473
1474 @param test: test instance from tko parser.
1475 """
1476 return test.testname == 'SERVER_JOB'
1477
1478
Prathmesh Prabhucbeab122017-05-30 11:23:53 -07001479def _create_afe_host(hostname):
1480 """Create an afe_host object backed by the AFE.
Prathmesh Prabhu28616da2017-01-11 12:58:25 -08001481
1482 @param hostname: Name of the host for which we want the Host object.
Prathmesh Prabhu28616da2017-01-11 12:58:25 -08001483 @returns: An object of type frontend.AFE
1484 """
Prathmesh Prabhu28616da2017-01-11 12:58:25 -08001485 afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
1486 hosts = afe.get_hosts(hostname=hostname)
1487 if not hosts:
1488 raise error.AutoservError('No hosts named %s found' % hostname)
1489
1490 return hosts[0]
1491
1492
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -07001493def _create_file_backed_host_info_store(store_dir, hostname):
1494 """Create a CachingHostInfoStore backed by an existing file.
Prathmesh Prabhu07b5d982017-01-11 12:08:33 -08001495
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -07001496 @param store_dir: A directory to contain store backing files.
Prathmesh Prabhu07b5d982017-01-11 12:08:33 -08001497 @param hostname: Name of the host for which we want the store.
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -07001498
1499 @returns: An object of type CachingHostInfoStore.
1500 """
1501 backing_file_path = os.path.join(store_dir, '%s.store' % hostname)
1502 if not os.path.isfile(backing_file_path):
1503 raise error.AutoservError(
1504 'Requested FileStore but no backing file at %s'
1505 % backing_file_path
1506 )
1507 return file_store.FileStore(backing_file_path)
1508
1509
1510def _create_afe_backed_host_info_store(store_dir, hostname):
1511 """Create a CachingHostInfoStore backed by the AFE.
1512
1513 @param store_dir: A directory to contain store backing files.
1514 @param hostname: Name of the host for which we want the store.
1515
1516 @returns: An object of type CachingHostInfoStore.
Prathmesh Prabhu07b5d982017-01-11 12:08:33 -08001517 """
Prathmesh Prabhu588007d2017-06-15 00:31:31 -07001518 primary_store = afe_store.AfeStore(hostname)
Prathmesh Prabhu07b5d982017-01-11 12:08:33 -08001519 try:
Prathmesh Prabhu588007d2017-06-15 00:31:31 -07001520 primary_store.get(force_refresh=True)
Prathmesh Prabhu07b5d982017-01-11 12:08:33 -08001521 except host_info.StoreError:
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -07001522 raise error.AutoservError(
1523 'Could not obtain HostInfo for hostname %s' % hostname)
1524 # Since the store wasn't initialized external to autoserv, we must
1525 # ensure that the store we create is unique within store_dir.
1526 backing_file_path = os.path.join(
1527 _make_unique_subdir(store_dir),
1528 '%s.store' % hostname,
1529 )
Prathmesh Prabhu588007d2017-06-15 00:31:31 -07001530 logging.info('Shadowing AFE store with a FileStore at %s',
1531 backing_file_path)
1532 shadow_store = file_store.FileStore(backing_file_path)
1533 return shadowing_store.ShadowingStore(primary_store, shadow_store)
1534
1535
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -07001536def _make_unique_subdir(workdir):
1537 """Creates a new subdir within workdir and returns the path to it."""
1538 store_dir = os.path.join(workdir, 'dir_%s' % uuid.uuid4())
Prathmesh Prabhu588007d2017-06-15 00:31:31 -07001539 _make_dirs_if_needed(store_dir)
Prathmesh Prabhu7fc39c52018-03-21 14:08:30 -07001540 return store_dir
Prathmesh Prabhu588007d2017-06-15 00:31:31 -07001541
1542
1543def _make_dirs_if_needed(path):
1544 """os.makedirs, but ignores failure because the leaf directory exists"""
1545 try:
1546 os.makedirs(path)
1547 except OSError as e:
1548 if e.errno != errno.EEXIST:
1549 raise