blob: e254903dc7f9f08515c55710317ae07bbcd5046d [file] [log] [blame]
Alex Miller0516e4c2013-06-03 18:07:48 -07001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5
Alex Miller1968edf2014-02-27 18:11:36 -08006import abc
Alex Miller0516e4c2013-06-03 18:07:48 -07007import logging
8
9import common
10from autotest_lib.frontend.afe.json_rpc import proxy
11from autotest_lib.server import frontend
Fang Dengaf30e7c2014-11-15 13:57:03 -080012from autotest_lib.server.cros import provision_actionables as actionables
Alex Miller0516e4c2013-06-03 18:07:48 -070013
14
15### Constants for label prefixes
16CROS_VERSION_PREFIX = 'cros-version'
Fang Dengdbc86322013-08-09 16:17:30 -070017FW_VERSION_PREFIX = 'fw-version'
Alex Miller0516e4c2013-06-03 18:07:48 -070018
Chris Sosae92399e2015-04-24 11:32:59 -070019# Default number of provisions attempts to try if we believe the devserver is
20# flaky.
21FLAKY_DEVSERVER_ATTEMPTS = 2
22
Alex Miller0516e4c2013-06-03 18:07:48 -070023
24### Helpers to convert value to label
25def cros_version_to_label(image):
26 """
27 Returns the proper label name for a ChromeOS build of |image|.
28
29 @param image: A string of the form 'lumpy-release/R28-3993.0.0'
30 @returns: A string that is the appropriate label name.
31
32 """
33 return CROS_VERSION_PREFIX + ':' + image
34
35
Dan Shi9cb0eec2014-06-03 09:04:50 -070036def fw_version_to_label(image):
37 """
38 Returns the proper label name for a firmware build of |image|.
39
40 @param image: A string of the form 'lumpy-release/R28-3993.0.0'
41 @returns: A string that is the appropriate label name.
42
43 """
44 return FW_VERSION_PREFIX + ':' + image
45
46
Alex Miller1968edf2014-02-27 18:11:36 -080047class _SpecialTaskAction(object):
Alex Miller0516e4c2013-06-03 18:07:48 -070048 """
Alex Miller1968edf2014-02-27 18:11:36 -080049 Base class to give a template for mapping labels to tests.
Alex Miller0516e4c2013-06-03 18:07:48 -070050 """
Alex Miller1968edf2014-02-27 18:11:36 -080051
52 __metaclass__ = abc.ABCMeta
Alex Miller0516e4c2013-06-03 18:07:48 -070053
54
Alex Miller1968edf2014-02-27 18:11:36 -080055 # One cannot do
56 # @abc.abstractproperty
57 # _actions = {}
58 # so this is the next best thing
59 @abc.abstractproperty
60 def _actions(self):
61 """A dictionary mapping labels to test names."""
62 pass
63
64
65 @abc.abstractproperty
66 def name(self):
67 """The name of this special task to be used in output."""
68 pass
69
70
71 @classmethod
72 def acts_on(cls, label):
73 """
74 Returns True if the label is a label that we recognize as something we
75 know how to act on, given our _actions.
76
77 @param label: The label as a string.
78 @returns: True if there exists a test to run for this label.
79
80 """
81 return label.split(':')[0] in cls._actions
82
83
84 @classmethod
85 def test_for(cls, label):
86 """
87 Returns the test associated with the given (string) label name.
88
89 @param label: The label for which the action is being requested.
90 @returns: The string name of the test that should be run.
91 @raises KeyError: If the name was not recognized as one we care about.
92
93 """
94 return cls._actions[label]
95
96
Alex Milleraa772002014-04-10 17:51:21 -070097 @classmethod
98 def partition(cls, labels):
99 """
100 Filter a list of labels into two sets: those labels that we know how to
101 act on and those that we don't know how to act on.
102
103 @param labels: A list of strings of labels.
104 @returns: A tuple where the first element is a set of unactionable
105 labels, and the second element is a set of the actionable
106 labels.
107 """
108 capabilities = set()
109 configurations = set()
110
111 for label in labels:
112 if cls.acts_on(label):
113 configurations.add(label)
114 else:
115 capabilities.add(label)
116
117 return capabilities, configurations
118
119
Alex Miller1968edf2014-02-27 18:11:36 -0800120class Verify(_SpecialTaskAction):
Alex Miller0516e4c2013-06-03 18:07:48 -0700121 """
Alex Miller1968edf2014-02-27 18:11:36 -0800122 Tests to verify that the DUT is in a sane, known good state that we can run
123 tests on. Failure to verify leads to running Repair.
Alex Miller0516e4c2013-06-03 18:07:48 -0700124 """
Alex Miller1968edf2014-02-27 18:11:36 -0800125
126 _actions = {
Fang Dengaf30e7c2014-11-15 13:57:03 -0800127 'modem_repair': actionables.TestActionable('cellular_StaleModemReboot'),
Dan Shid2b82f32014-10-24 13:39:19 -0700128 # TODO(crbug.com/404421): set rpm action to power_RPMTest after the RPM
129 # is stable in lab (destiny). The power_RPMTest failure led to reset job
130 # failure and that left dut in Repair Failed. Since the test will fail
131 # anyway due to the destiny lab issue, and test retry will retry the
132 # test in another DUT.
133 # This change temporarily disable the RPM check in reset job.
134 # Another way to do this is to remove rpm dependency from tests' control
135 # file. That will involve changes on multiple control files. This one
136 # line change here is a simple temporary fix.
Fang Dengaf30e7c2014-11-15 13:57:03 -0800137 'rpm': actionables.TestActionable('dummy_PassServer'),
Alex Miller1968edf2014-02-27 18:11:36 -0800138 }
139
140 name = 'verify'
141
142
143class Provision(_SpecialTaskAction):
144 """
145 Provisioning runs to change the configuration of the DUT from one state to
146 another. It will only be run on verified DUTs.
147 """
148
149 # TODO(milleral): http://crbug.com/249555
150 # Create some way to discover and register provisioning tests so that we
151 # don't need to hand-maintain a list of all of them.
152 _actions = {
Fang Deng9d548742015-02-03 11:35:02 -0800153 CROS_VERSION_PREFIX: actionables.TestActionable(
154 'provision_AutoUpdate',
155 extra_kwargs={'disable_sysinfo': False,
156 'disable_before_test_sysinfo': False,
157 'disable_before_iteration_sysinfo': True,
158 'disable_after_test_sysinfo': True,
159 'disable_after_iteration_sysinfo': True}),
Fang Dengaf30e7c2014-11-15 13:57:03 -0800160 FW_VERSION_PREFIX: actionables.TestActionable(
161 'provision_FirmwareUpdate'),
Alex Miller1968edf2014-02-27 18:11:36 -0800162 }
163
164 name = 'provision'
165
166
167class Cleanup(_SpecialTaskAction):
168 """
169 Cleanup runs after a test fails to try and remove artifacts of tests and
170 ensure the DUT will be in a sane state for the next test run.
171 """
172
173 _actions = {
Fang Dengaf30e7c2014-11-15 13:57:03 -0800174 'cleanup-reboot': actionables.RebootActionable(),
Alex Miller1968edf2014-02-27 18:11:36 -0800175 }
176
177 name = 'cleanup'
178
179
180class Repair(_SpecialTaskAction):
181 """
182 Repair runs when one of the other special tasks fails. It should be able
183 to take a component of the DUT that's in an unknown state and restore it to
184 a good state.
185 """
186
187 _actions = {
188 }
189
190 name = 'repair'
191
192
Alex Miller1968edf2014-02-27 18:11:36 -0800193
Alex Milleraa772002014-04-10 17:51:21 -0700194# TODO(milleral): crbug.com/364273
195# Label doesn't really mean label in this context. We're putting things into
196# DEPENDENCIES that really aren't DEPENDENCIES, and we should probably stop
197# doing that.
198def is_for_special_action(label):
199 """
200 If any special task handles the label specially, then we're using the label
201 to communicate that we want an action, and not as an actual dependency that
202 the test has.
203
204 @param label: A string label name.
205 @return True if any special task handles this label specially,
206 False if no special task handles this label.
207 """
208 return (Verify.acts_on(label) or
209 Provision.acts_on(label) or
210 Cleanup.acts_on(label) or
211 Repair.acts_on(label))
Alex Miller0516e4c2013-06-03 18:07:48 -0700212
213
214def filter_labels(labels):
215 """
216 Filter a list of labels into two sets: those labels that we know how to
217 change and those that we don't. For the ones we know how to change, split
218 them apart into the name of configuration type and its value.
219
220 @param labels: A list of strings of labels.
221 @returns: A tuple where the first element is a set of unprovisionable
222 labels, and the second element is a set of the provisionable
223 labels.
224
225 >>> filter_labels(['bluetooth', 'cros-version:lumpy-release/R28-3993.0.0'])
226 (set(['bluetooth']), set(['cros-version:lumpy-release/R28-3993.0.0']))
227
228 """
Alex Milleraa772002014-04-10 17:51:21 -0700229 return Provision.partition(labels)
Alex Miller0516e4c2013-06-03 18:07:48 -0700230
231
232def split_labels(labels):
233 """
234 Split a list of labels into a dict mapping name to value. All labels must
235 be provisionable labels, or else a ValueError
236
237 @param labels: list of strings of label names
238 @returns: A dict of where the key is the configuration name, and the value
239 is the configuration value.
240 @raises: ValueError if a label is not a provisionable label.
241
242 >>> split_labels(['cros-version:lumpy-release/R28-3993.0.0'])
243 {'cros-version': 'lumpy-release/R28-3993.0.0'}
244 >>> split_labels(['bluetooth'])
245 Traceback (most recent call last):
246 ...
247 ValueError: Unprovisionable label bluetooth
248
249 """
250 configurations = dict()
251
252 for label in labels:
Alex Milleraa772002014-04-10 17:51:21 -0700253 if Provision.acts_on(label):
Alex Miller0516e4c2013-06-03 18:07:48 -0700254 name, value = label.split(':', 1)
255 configurations[name] = value
256 else:
257 raise ValueError('Unprovisionable label %s' % label)
258
259 return configurations
260
261
Alex Miller2229c9c2013-08-27 15:20:39 -0700262def join(provision_type, provision_value):
263 """
264 Combine the provision type and value into the label name.
265
266 @param provision_type: One of the constants that are the label prefixes.
267 @param provision_value: A string of the value for this provision type.
268 @returns: A string that is the label name for this (type, value) pair.
269
270 >>> join(CROS_VERSION_PREFIX, 'lumpy-release/R27-3773.0.0')
271 'cros-version:lumpy-release/R27-3773.0.0'
272
273 """
274 return '%s:%s' % (provision_type, provision_value)
275
276
Alex Miller667b5f22014-02-28 15:33:39 -0800277class SpecialTaskActionException(Exception):
278 """
279 Exception raised when a special task fails to successfully run a test that
280 is required.
281
282 This is also a literally meaningless exception. It's always just discarded.
283 """
284
285
286def run_special_task_actions(job, host, labels, task):
287 """
288 Iterate through all `label`s and run any tests on `host` that `task` has
289 corresponding to the passed in labels.
290
291 Emits status lines for each run test, and INFO lines for each skipped label.
292
293 @param job: A job object from a control file.
294 @param host: The host to run actions on.
295 @param labels: The list of job labels to work on.
296 @param task: An instance of _SpecialTaskAction.
297 @returns: None
298 @raises: SpecialTaskActionException if a test fails.
299
300 """
301 capabilities, configuration = filter_labels(labels)
302
303 for label in capabilities:
304 if task.acts_on(label):
Fang Dengaf30e7c2014-11-15 13:57:03 -0800305 action_item = task.test_for(label)
306 success = action_item.execute(job=job, host=host)
Alex Miller667b5f22014-02-28 15:33:39 -0800307 if not success:
308 raise SpecialTaskActionException()
309 else:
310 job.record('INFO', None, task.name,
311 "Can't %s label '%s'." % (task.name, label))
312
313 for name, value in split_labels(configuration).items():
314 if task.acts_on(name):
Fang Dengaf30e7c2014-11-15 13:57:03 -0800315 action_item = task.test_for(name)
316 success = action_item.execute(job=job, host=host, value=value)
Alex Miller667b5f22014-02-28 15:33:39 -0800317 if not success:
318 raise SpecialTaskActionException()
319 else:
320 job.record('INFO', None, task.name,
321 "Can't %s label '%s:%s'." % (task.name, name, value))
322
323
Alex Miller2229c9c2013-08-27 15:20:39 -0700324# This has been copied out of dynamic_suite's reimager.py, which no longer
325# exists. I'd prefer if this would go away by doing http://crbug.com/249424,
326# so that labels are just automatically made when we try to add them to a host.
Alex Miller0516e4c2013-06-03 18:07:48 -0700327def ensure_label_exists(name):
328 """
329 Ensure that a label called |name| exists in the autotest DB.
330
331 @param name: the label to check for/create.
332 @raises ValidationError: There was an error in the response that was
333 not because the label already existed.
334
335 """
336 afe = frontend.AFE()
337 try:
338 afe.create_label(name=name)
339 except proxy.ValidationError as ve:
340 if ('name' in ve.problem_keys and
341 'This value must be unique' in ve.problem_keys['name']):
342 logging.debug('Version label %s already exists', name)
343 else:
344 raise ve