blob: a6c3cd9da092f37ac3d465917a80b604be37aa7e [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
Allen Lifda3e232016-10-17 14:54:12 -07005import collections
6import re
Dan Shi7279a5a2016-04-07 11:04:28 -07007import sys
Alex Miller0516e4c2013-06-03 18:07:48 -07008
9import common
Fang Dengaf30e7c2014-11-15 13:57:03 -080010from autotest_lib.server.cros import provision_actionables as actionables
Alex Miller0516e4c2013-06-03 18:07:48 -070011
12
13### Constants for label prefixes
14CROS_VERSION_PREFIX = 'cros-version'
Simran Basi5ace6f22016-01-06 17:30:44 -080015ANDROID_BUILD_VERSION_PREFIX = 'ab-version'
Kevin Cheng84a71ba2016-07-14 11:03:57 -070016TESTBED_BUILD_VERSION_PREFIX = 'testbed-version'
Dan Shi0723bf52015-06-24 10:52:38 -070017FW_RW_VERSION_PREFIX = 'fwrw-version'
Dan Shi36cfd832014-10-10 13:38:51 -070018FW_RO_VERSION_PREFIX = 'fwro-version'
Alex Miller0516e4c2013-06-03 18:07:48 -070019
Dan Shie44f9c02016-02-18 13:25:05 -080020# Special label to skip provision and run reset instead.
21SKIP_PROVISION = 'skip_provision'
22
Chris Sosae92399e2015-04-24 11:32:59 -070023# Default number of provisions attempts to try if we believe the devserver is
24# flaky.
25FLAKY_DEVSERVER_ATTEMPTS = 2
26
Alex Miller0516e4c2013-06-03 18:07:48 -070027
Allen Lifda3e232016-10-17 14:54:12 -070028def label_from_str(label_string):
29 """Return a proper Label instance from a label string.
30
31 This function is for converting an existing label string into a Label
32 instance of the proper type. For constructing a specific type of label,
33 don't use this. Instead, instantiate the specific Label subclass.
34
35 @param label_string: Label string.
36 @returns: An instance of Label or a subclass.
Alex Miller0516e4c2013-06-03 18:07:48 -070037 """
Allen Lifda3e232016-10-17 14:54:12 -070038 if NamespaceLabel.SEP in label_string:
39 label = NamespaceLabel.from_str(label_string)
40 namespaces = _PresetNamespaceLabelMeta.namespaces
41 if label.namespace in namespaces:
42 return namespaces[label.namespace](label.value)
43 else:
44 return label
45 else:
46 return Label(label_string)
Alex Miller0516e4c2013-06-03 18:07:48 -070047
Alex Miller0516e4c2013-06-03 18:07:48 -070048
Allen Lifda3e232016-10-17 14:54:12 -070049class Label(str):
50 """A string that is explicitly typed as a label."""
51
52 def __repr__(self):
53 return '{cls}({label})'.format(
54 cls=type(self).__name__,
55 label=super(Label, self).__repr__())
56
57 @property
58 def action(self):
59 """Return the action represented by the label.
60
61 This is used for determine actions to perform based on labels, for
62 example for provisioning or repair.
63
64 @return: An Action instance.
65 """
66 return Action(self, '')
67
68
69Action = collections.namedtuple('Action', 'name,value')
70
71
72class NamespaceLabel(Label):
73 """Label with namespace and value separated by a colon."""
74
75 SEP = ':'
76
77 def __new__(cls, namespace, value):
78 return super(NamespaceLabel, cls).__new__(
79 cls, cls.SEP.join((namespace, value)))
80
81 @classmethod
82 def from_str(cls, label):
83 """Make NamespaceLabel instance from full string.
84
85 @param label: Label string.
86 @returns: NamespaceLabel instance.
87 """
88 namespace, value = label.split(cls.SEP, 1)
89 return cls(namespace, value)
90
91 @property
92 def namespace(self):
93 """The label's namespace (before colon).
94
95 @returns: string
96 """
97 return self.split(self.SEP, 1)[0]
98
99 @property
100 def value(self):
101 """The label's value (after colon).
102
103 @returns: string
104 """
105 return self.split(self.SEP, 1)[1]
106
107 @property
108 def action(self):
109 """Return the action represented by the label.
110
111 See docstring on overridden method.
112
113 @return: An Action instance.
114 """
115 return Action(self.namespace, self.value)
116
117
118class _PresetNamespaceLabelMeta(type):
119 """Metaclass for PresetNamespaceLabelMeta and subclasses.
120
121 This automatically tracks the NAMESPACE for concrete classes that define
122 it. The namespaces attribute is a dict mapping namespace strings to the
123 corresponding NamespaceLabel subclass.
Alex Miller0516e4c2013-06-03 18:07:48 -0700124 """
Allen Lifda3e232016-10-17 14:54:12 -0700125
126 namespaces = {}
127
128 def __init__(cls, name, bases, dict_):
129 if hasattr(cls, 'NAMESPACE'):
130 type(cls).namespaces[cls.NAMESPACE] = cls
Alex Miller0516e4c2013-06-03 18:07:48 -0700131
132
Allen Lifda3e232016-10-17 14:54:12 -0700133class _PresetNamespaceLabel(NamespaceLabel):
134 """NamespaceLabel with preset namespace.
135
136 This class is abstract. Concrete subclasses must set a NAMESPACE class
137 attribute.
Dan Shi9cb0eec2014-06-03 09:04:50 -0700138 """
Tom Wai-Hong Tam2d00cb22016-01-08 06:40:50 +0800139
Allen Lifda3e232016-10-17 14:54:12 -0700140 __metaclass__ = _PresetNamespaceLabelMeta
Tom Wai-Hong Tam2d00cb22016-01-08 06:40:50 +0800141
Allen Lifda3e232016-10-17 14:54:12 -0700142 def __new__(cls, value):
143 return super(_PresetNamespaceLabel, cls).__new__(cls, cls.NAMESPACE, value)
144
145
146class CrosVersionLabel(_PresetNamespaceLabel):
147 """cros-version label."""
148 NAMESPACE = CROS_VERSION_PREFIX
149
150 @property
151 def value(self):
152 """The label's value (after colon).
153
154 @returns: string
155 """
156 return CrosVersion(super(CrosVersionLabel, self).value)
157
158
159class FWROVersionLabel(_PresetNamespaceLabel):
160 """Read-only firmware version label."""
161 NAMESPACE = FW_RO_VERSION_PREFIX
162
163
164class FWRWVersionLabel(_PresetNamespaceLabel):
165 """Read-write firmware version label."""
166 NAMESPACE = FW_RW_VERSION_PREFIX
167
168
169class CrosVersion(str):
170 """The name of a CrOS image version (e.g. lumpy-release/R27-3773.0.0).
171
172 Parts of the image name are exposed via properties. In case the name is
173 not well-formed, these properties return INVALID_STR, which is not a valid value
174 for any part.
175
176 Class attributes:
177 INVALID_STR -- String returned if the version is not well-formed.
178
179 Properties:
180 group
181 milestone
182 version
183 rc
Tom Wai-Hong Tam2d00cb22016-01-08 06:40:50 +0800184 """
Tom Wai-Hong Tam2d00cb22016-01-08 06:40:50 +0800185
Allen Lifda3e232016-10-17 14:54:12 -0700186 INVALID_STR = 'N/A'
187 _NAME_PATTERN = re.compile(
188 r'^'
189 r'(?P<group>[a-z0-9-]+)'
190 r'/'
191 r'(?P<milestone>R[0-9]+)'
192 r'-'
193 r'(?P<version>[0-9.]+)'
194 r'(-(?P<rc>rc[0-9]+))?'
195 r'$'
196 )
Tom Wai-Hong Tam2d00cb22016-01-08 06:40:50 +0800197
Allen Lifda3e232016-10-17 14:54:12 -0700198 def __repr__(self):
199 return '{cls}({name})'.format(
200 cls=type(self).__name__,
201 name=super(CrosVersion, self).__repr__())
Dan Shi9cb0eec2014-06-03 09:04:50 -0700202
Allen Lifda3e232016-10-17 14:54:12 -0700203 def _get_group(self, group):
204 """Get regex match group, or fall back to N/A.
Dan Shi9cb0eec2014-06-03 09:04:50 -0700205
Allen Lifda3e232016-10-17 14:54:12 -0700206 @param group: Group name string.
207 @returns String.
208 """
209 match = self._NAME_PATTERN.search(self)
210 if match is None:
211 return self.INVALID_STR
212 else:
213 return match.group(group)
214
215 @property
216 def group(self):
217 """Cros image group (e.g. lumpy-release)."""
218 return self._get_group('group')
219
220 @property
221 def milestone(self):
222 """Cros image milestone (e.g. R27)."""
223 return self._get_group('milestone')
224
225 @property
226 def version(self):
227 """Cros image milestone (e.g. 3773.0.0)."""
228 return self._get_group('version')
229
230 @property
231 def rc(self):
232 """Cros image rc (e.g. rc2)."""
233 return self._get_group('rc')
Dan Shi9cb0eec2014-06-03 09:04:50 -0700234
235
Alex Miller1968edf2014-02-27 18:11:36 -0800236class _SpecialTaskAction(object):
Alex Miller0516e4c2013-06-03 18:07:48 -0700237 """
Alex Miller1968edf2014-02-27 18:11:36 -0800238 Base class to give a template for mapping labels to tests.
Alex Miller0516e4c2013-06-03 18:07:48 -0700239 """
Alex Miller1968edf2014-02-27 18:11:36 -0800240
Dan Shi7279a5a2016-04-07 11:04:28 -0700241 # A dictionary mapping labels to test names.
242 _actions = {}
Alex Miller0516e4c2013-06-03 18:07:48 -0700243
Dan Shi7279a5a2016-04-07 11:04:28 -0700244 # The name of this special task to be used in output.
245 name = None;
Alex Miller0516e4c2013-06-03 18:07:48 -0700246
Dan Shi7279a5a2016-04-07 11:04:28 -0700247 # Some special tasks require to run before others, e.g., ChromeOS image
248 # needs to be updated before firmware provision. List `_priorities` defines
249 # the order of each label prefix. An element with a smaller index has higher
250 # priority. Not listed ones have the lowest priority.
251 # This property should be overriden in subclass to define its own priorities
252 # across available label prefixes.
253 _priorities = []
Alex Miller1968edf2014-02-27 18:11:36 -0800254
Alex Miller1968edf2014-02-27 18:11:36 -0800255 @classmethod
Allen Lifda3e232016-10-17 14:54:12 -0700256 def acts_on(cls, label_string):
Alex Miller1968edf2014-02-27 18:11:36 -0800257 """
258 Returns True if the label is a label that we recognize as something we
259 know how to act on, given our _actions.
260
Allen Lifda3e232016-10-17 14:54:12 -0700261 @param label_string: The label as a string.
Alex Miller1968edf2014-02-27 18:11:36 -0800262 @returns: True if there exists a test to run for this label.
Alex Miller1968edf2014-02-27 18:11:36 -0800263 """
Allen Lifda3e232016-10-17 14:54:12 -0700264 label = label_from_str(label_string)
265 return label.action.name in cls._actions
Alex Miller1968edf2014-02-27 18:11:36 -0800266
267 @classmethod
268 def test_for(cls, label):
269 """
270 Returns the test associated with the given (string) label name.
271
272 @param label: The label for which the action is being requested.
273 @returns: The string name of the test that should be run.
274 @raises KeyError: If the name was not recognized as one we care about.
Alex Miller1968edf2014-02-27 18:11:36 -0800275 """
276 return cls._actions[label]
277
Alex Milleraa772002014-04-10 17:51:21 -0700278 @classmethod
279 def partition(cls, labels):
280 """
281 Filter a list of labels into two sets: those labels that we know how to
282 act on and those that we don't know how to act on.
283
284 @param labels: A list of strings of labels.
285 @returns: A tuple where the first element is a set of unactionable
286 labels, and the second element is a set of the actionable
287 labels.
288 """
289 capabilities = set()
290 configurations = set()
291
292 for label in labels:
Dan Shie44f9c02016-02-18 13:25:05 -0800293 if label == SKIP_PROVISION:
294 # skip_provision is neither actionable or a capability label.
295 # It doesn't need any handling.
296 continue
297 elif cls.acts_on(label):
Alex Milleraa772002014-04-10 17:51:21 -0700298 configurations.add(label)
299 else:
300 capabilities.add(label)
301
302 return capabilities, configurations
303
Dan Shi7279a5a2016-04-07 11:04:28 -0700304 @classmethod
Allen Lifda3e232016-10-17 14:54:12 -0700305 def get_sorted_actions(cls, configurations):
Dan Shi7279a5a2016-04-07 11:04:28 -0700306 """
307 Sort configurations based on the priority defined in cls._priorities.
308
309 @param configurations: A list of actionable labels.
Allen Lifda3e232016-10-17 14:54:12 -0700310 @return: A list of Action instances sorted by the action name in
311 cls._priorities.
Dan Shi7279a5a2016-04-07 11:04:28 -0700312 """
Allen Lifda3e232016-10-17 14:54:12 -0700313 actions = (label_from_str(label_string).action
314 for label_string in configurations)
315 return sorted(actions, key=cls._get_action_priority)
Dan Shi7279a5a2016-04-07 11:04:28 -0700316
Allen Lifda3e232016-10-17 14:54:12 -0700317 @classmethod
318 def _get_action_priority(cls, action):
319 """
320 Return the priority of the action string.
321
322 @param action: An Action instance.
323 @return: An int.
324 """
325 if action.name in cls._priorities:
326 return cls._priorities.index(action.name)
327 else:
328 return sys.maxint
Dan Shi7279a5a2016-04-07 11:04:28 -0700329
330
Alex Miller1968edf2014-02-27 18:11:36 -0800331class Verify(_SpecialTaskAction):
Alex Miller0516e4c2013-06-03 18:07:48 -0700332 """
Alex Miller1968edf2014-02-27 18:11:36 -0800333 Tests to verify that the DUT is in a sane, known good state that we can run
334 tests on. Failure to verify leads to running Repair.
Alex Miller0516e4c2013-06-03 18:07:48 -0700335 """
Alex Miller1968edf2014-02-27 18:11:36 -0800336
337 _actions = {
Fang Dengaf30e7c2014-11-15 13:57:03 -0800338 'modem_repair': actionables.TestActionable('cellular_StaleModemReboot'),
Don Garrett6f7e8002015-07-23 22:45:37 +0000339 # TODO(crbug.com/404421): set rpm action to power_RPMTest after the RPM
340 # is stable in lab (destiny). The power_RPMTest failure led to reset job
341 # failure and that left dut in Repair Failed. Since the test will fail
342 # anyway due to the destiny lab issue, and test retry will retry the
343 # test in another DUT.
344 # This change temporarily disable the RPM check in reset job.
345 # Another way to do this is to remove rpm dependency from tests' control
346 # file. That will involve changes on multiple control files. This one
347 # line change here is a simple temporary fix.
348 'rpm': actionables.TestActionable('dummy_PassServer'),
Alex Miller1968edf2014-02-27 18:11:36 -0800349 }
350
351 name = 'verify'
352
353
354class Provision(_SpecialTaskAction):
355 """
356 Provisioning runs to change the configuration of the DUT from one state to
357 another. It will only be run on verified DUTs.
358 """
359
Dan Shi7279a5a2016-04-07 11:04:28 -0700360 # ChromeOS update must happen before firmware install, so the dut has the
361 # correct ChromeOS version label when firmware install occurs. The ChromeOS
362 # version label is used for firmware update to stage desired ChromeOS image
363 # on to the servo USB stick.
364 _priorities = [CROS_VERSION_PREFIX,
365 FW_RO_VERSION_PREFIX,
366 FW_RW_VERSION_PREFIX]
367
Alex Miller1968edf2014-02-27 18:11:36 -0800368 # TODO(milleral): http://crbug.com/249555
369 # Create some way to discover and register provisioning tests so that we
370 # don't need to hand-maintain a list of all of them.
371 _actions = {
Fang Deng9d548742015-02-03 11:35:02 -0800372 CROS_VERSION_PREFIX: actionables.TestActionable(
373 'provision_AutoUpdate',
374 extra_kwargs={'disable_sysinfo': False,
375 'disable_before_test_sysinfo': False,
376 'disable_before_iteration_sysinfo': True,
377 'disable_after_test_sysinfo': True,
378 'disable_after_iteration_sysinfo': True}),
Tom Wai-Hong Tam9a237612016-01-08 03:41:46 +0800379 FW_RO_VERSION_PREFIX: actionables.TestActionable(
Fang Dengaf30e7c2014-11-15 13:57:03 -0800380 'provision_FirmwareUpdate'),
Tom Wai-Hong Tam9a237612016-01-08 03:41:46 +0800381 FW_RW_VERSION_PREFIX: actionables.TestActionable(
382 'provision_FirmwareUpdate',
Dan Shi61e407c2016-04-08 14:21:07 -0700383 extra_kwargs={'rw_only': True,
384 'tag': 'rw_only'}),
Simran Basi5ace6f22016-01-06 17:30:44 -0800385 ANDROID_BUILD_VERSION_PREFIX : actionables.TestActionable(
386 'provision_AndroidUpdate'),
Simran Basiadf31312016-06-28 14:23:05 -0700387 TESTBED_BUILD_VERSION_PREFIX : actionables.TestActionable(
388 'provision_TestbedUpdate'),
Alex Miller1968edf2014-02-27 18:11:36 -0800389 }
390
391 name = 'provision'
392
393
394class Cleanup(_SpecialTaskAction):
395 """
396 Cleanup runs after a test fails to try and remove artifacts of tests and
397 ensure the DUT will be in a sane state for the next test run.
398 """
399
400 _actions = {
Fang Dengaf30e7c2014-11-15 13:57:03 -0800401 'cleanup-reboot': actionables.RebootActionable(),
Alex Miller1968edf2014-02-27 18:11:36 -0800402 }
403
404 name = 'cleanup'
405
406
407class Repair(_SpecialTaskAction):
408 """
409 Repair runs when one of the other special tasks fails. It should be able
410 to take a component of the DUT that's in an unknown state and restore it to
411 a good state.
412 """
413
414 _actions = {
415 }
416
417 name = 'repair'
418
419
Alex Milleraa772002014-04-10 17:51:21 -0700420# TODO(milleral): crbug.com/364273
421# Label doesn't really mean label in this context. We're putting things into
422# DEPENDENCIES that really aren't DEPENDENCIES, and we should probably stop
423# doing that.
424def is_for_special_action(label):
425 """
426 If any special task handles the label specially, then we're using the label
427 to communicate that we want an action, and not as an actual dependency that
428 the test has.
429
430 @param label: A string label name.
431 @return True if any special task handles this label specially,
432 False if no special task handles this label.
433 """
434 return (Verify.acts_on(label) or
435 Provision.acts_on(label) or
436 Cleanup.acts_on(label) or
Dan Shie44f9c02016-02-18 13:25:05 -0800437 Repair.acts_on(label) or
438 label == SKIP_PROVISION)
Alex Miller0516e4c2013-06-03 18:07:48 -0700439
440
Alex Miller667b5f22014-02-28 15:33:39 -0800441class SpecialTaskActionException(Exception):
442 """
443 Exception raised when a special task fails to successfully run a test that
444 is required.
445
446 This is also a literally meaningless exception. It's always just discarded.
447 """
448
449
450def run_special_task_actions(job, host, labels, task):
451 """
452 Iterate through all `label`s and run any tests on `host` that `task` has
453 corresponding to the passed in labels.
454
455 Emits status lines for each run test, and INFO lines for each skipped label.
456
457 @param job: A job object from a control file.
458 @param host: The host to run actions on.
459 @param labels: The list of job labels to work on.
460 @param task: An instance of _SpecialTaskAction.
461 @returns: None
462 @raises: SpecialTaskActionException if a test fails.
463
464 """
Dan Shi7279a5a2016-04-07 11:04:28 -0700465 capabilities, configurations = task.partition(labels)
Alex Miller667b5f22014-02-28 15:33:39 -0800466
467 for label in capabilities:
Dan Shi7279a5a2016-04-07 11:04:28 -0700468 job.record('INFO', None, task.name,
469 "Can't %s label '%s'." % (task.name, label))
Alex Miller667b5f22014-02-28 15:33:39 -0800470
Dan Shi7279a5a2016-04-07 11:04:28 -0700471 # Sort the configuration labels based on `task._priorities`.
Allen Lifda3e232016-10-17 14:54:12 -0700472 actions = task.get_sorted_actions(configurations)
473 for name, value in actions:
Dan Shi7279a5a2016-04-07 11:04:28 -0700474 action_item = task.test_for(name)
475 success = action_item.execute(job=job, host=host, value=value)
476 if not success:
477 raise SpecialTaskActionException()