1# 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
6import re
7import sys
8
9import common
10from autotest_lib.server.cros import provision_actionables as actionables
11
12
13### Constants for label prefixes
14CROS_VERSION_PREFIX = 'cros-version'
15ANDROID_BUILD_VERSION_PREFIX = 'ab-version'
16TESTBED_BUILD_VERSION_PREFIX = 'testbed-version'
17FW_RW_VERSION_PREFIX = 'fwrw-version'
18FW_RO_VERSION_PREFIX = 'fwro-version'
19
20_ANDROID_BUILD_REGEX = r'.+/.+/P?([0-9]+|LATEST)'
21_ANDROID_TESTBED_BUILD_REGEX = _ANDROID_BUILD_REGEX + '(,|(#[0-9]+))'
22
23# Special label to skip provision and run reset instead.
24SKIP_PROVISION = 'skip_provision'
25
26# Default number of provisions attempts to try if we believe the devserver is
27# flaky.
28FLAKY_DEVSERVER_ATTEMPTS = 2
29
30
31### Helpers to convert value to label
32def get_version_label_prefix(image):
33    """
34    Determine a version label prefix from a given image name.
35
36    Parses `image` to determine what kind of image it refers
37    to, and returns the corresponding version label prefix.
38
39    Known version label prefixes are:
40      * `CROS_VERSION_PREFIX` for Chrome OS version strings.
41        These images have names like `cave-release/R57-9030.0.0`.
42      * `ANDROID_BUILD_VERSION_PREFIX` for Android build versions
43        These images have names like
44        `git_mnc-release/shamu-userdebug/2457013`.
45      * `TESTBED_BUILD_VERSION_PREFIX` for Android testbed version
46        specifications.  These are either comma separated lists of
47        Android versions, or an Android version with a suffix like
48        '#2', indicating two devices running the given build.
49
50    @param image: The image name to be parsed.
51    @returns: A string that is the prefix of version labels for the type
52              of image identified by `image`.
53
54    """
55    if re.match(_ANDROID_TESTBED_BUILD_REGEX, image, re.I):
56        return TESTBED_BUILD_VERSION_PREFIX
57    elif re.match(_ANDROID_BUILD_REGEX, image, re.I):
58        return ANDROID_BUILD_VERSION_PREFIX
59    else:
60        return CROS_VERSION_PREFIX
61
62
63def image_version_to_label(image):
64    """
65    Return a version label appropriate to the given image name.
66
67    The type of version label is as determined described for
68    `get_version_label_prefix()`, meaning the label will identify a
69    CrOS, Android, or Testbed version.
70
71    @param image: The image name to be parsed.
72    @returns: A string that is the appropriate label name.
73
74    """
75    return get_version_label_prefix(image) + ':' + image
76
77
78def cros_version_to_label(image):
79    """
80    Returns the proper label name for a ChromeOS build of |image|.
81
82    @param image: A string of the form 'lumpy-release/R28-3993.0.0'
83    @returns: A string that is the appropriate label name.
84
85    """
86    return CROS_VERSION_PREFIX + ':' + image
87
88
89def fwro_version_to_label(image):
90    """
91    Returns the proper label name for a RO firmware build of |image|.
92
93    @param image: A string of the form 'lumpy-release/R28-3993.0.0'
94    @returns: A string that is the appropriate label name.
95
96    """
97    return FW_RO_VERSION_PREFIX + ':' + image
98
99
100def fwrw_version_to_label(image):
101    """
102    Returns the proper label name for a RW firmware build of |image|.
103
104    @param image: A string of the form 'lumpy-release/R28-3993.0.0'
105    @returns: A string that is the appropriate label name.
106
107    """
108    return FW_RW_VERSION_PREFIX + ':' + image
109
110
111class _SpecialTaskAction(object):
112    """
113    Base class to give a template for mapping labels to tests.
114    """
115
116    # A dictionary mapping labels to test names.
117    _actions = {}
118
119    # The name of this special task to be used in output.
120    name = None;
121
122    # Some special tasks require to run before others, e.g., ChromeOS image
123    # needs to be updated before firmware provision. List `_priorities` defines
124    # the order of each label prefix. An element with a smaller index has higher
125    # priority. Not listed ones have the lowest priority.
126    # This property should be overriden in subclass to define its own priorities
127    # across available label prefixes.
128    _priorities = []
129
130
131    @classmethod
132    def acts_on(cls, label):
133        """
134        Returns True if the label is a label that we recognize as something we
135        know how to act on, given our _actions.
136
137        @param label: The label as a string.
138        @returns: True if there exists a test to run for this label.
139
140        """
141        return label.split(':')[0] in cls._actions
142
143
144    @classmethod
145    def test_for(cls, label):
146        """
147        Returns the test associated with the given (string) label name.
148
149        @param label: The label for which the action is being requested.
150        @returns: The string name of the test that should be run.
151        @raises KeyError: If the name was not recognized as one we care about.
152
153        """
154        return cls._actions[label]
155
156
157    @classmethod
158    def partition(cls, labels):
159        """
160        Filter a list of labels into two sets: those labels that we know how to
161        act on and those that we don't know how to act on.
162
163        @param labels: A list of strings of labels.
164        @returns: A tuple where the first element is a set of unactionable
165                  labels, and the second element is a set of the actionable
166                  labels.
167        """
168        capabilities = set()
169        configurations = set()
170
171        for label in labels:
172            if label == SKIP_PROVISION:
173                # skip_provision is neither actionable or a capability label.
174                # It doesn't need any handling.
175                continue
176            elif cls.acts_on(label):
177                configurations.add(label)
178            else:
179                capabilities.add(label)
180
181        return capabilities, configurations
182
183
184    @classmethod
185    def sort_configurations(cls, configurations):
186        """
187        Sort configurations based on the priority defined in cls._priorities.
188
189        @param configurations: A list of actionable labels.
190
191        @return: A sorted list of tuple of (label_prefix, value), the tuples are
192                sorted based on the label_prefix's index in cls._priorities.
193        """
194        # Split a list of labels into a dict mapping name to value.  All labels
195        # must be provisionable labels, or else a ValueError
196        # For example, label 'cros-version:lumpy-release/R28-3993.0.0' is split
197        # to  {'cros-version': 'lumpy-release/R28-3993.0.0'}
198        split_configurations = dict()
199        for label in configurations:
200            name, _, value = label.partition(':')
201            split_configurations[name] = value
202
203        sort_key = (lambda config:
204                (cls._priorities.index(config[0])
205                 if (config[0] in cls._priorities) else sys.maxint))
206        return sorted(split_configurations.items(), key=sort_key)
207
208
209class Verify(_SpecialTaskAction):
210    """
211    Tests to verify that the DUT is in a sane, known good state that we can run
212    tests on.  Failure to verify leads to running Repair.
213    """
214
215    _actions = {
216        'modem_repair': actionables.TestActionable('cellular_StaleModemReboot'),
217        # TODO(crbug.com/404421): set rpm action to power_RPMTest after the RPM
218        # is stable in lab (destiny). The power_RPMTest failure led to reset job
219        # failure and that left dut in Repair Failed. Since the test will fail
220        # anyway due to the destiny lab issue, and test retry will retry the
221        # test in another DUT.
222        # This change temporarily disable the RPM check in reset job.
223        # Another way to do this is to remove rpm dependency from tests' control
224        # file. That will involve changes on multiple control files. This one
225        # line change here is a simple temporary fix.
226        'rpm': actionables.TestActionable('dummy_PassServer'),
227    }
228
229    name = 'verify'
230
231
232class Provision(_SpecialTaskAction):
233    """
234    Provisioning runs to change the configuration of the DUT from one state to
235    another.  It will only be run on verified DUTs.
236    """
237
238    # ChromeOS update must happen before firmware install, so the dut has the
239    # correct ChromeOS version label when firmware install occurs. The ChromeOS
240    # version label is used for firmware update to stage desired ChromeOS image
241    # on to the servo USB stick.
242    _priorities = [CROS_VERSION_PREFIX,
243                   FW_RO_VERSION_PREFIX,
244                   FW_RW_VERSION_PREFIX]
245
246    # TODO(milleral): http://crbug.com/249555
247    # Create some way to discover and register provisioning tests so that we
248    # don't need to hand-maintain a list of all of them.
249    _actions = {
250        CROS_VERSION_PREFIX: actionables.TestActionable(
251                'provision_AutoUpdate',
252                extra_kwargs={'disable_sysinfo': False,
253                              'disable_before_test_sysinfo': False,
254                              'disable_before_iteration_sysinfo': True,
255                              'disable_after_test_sysinfo': True,
256                              'disable_after_iteration_sysinfo': True}),
257        FW_RO_VERSION_PREFIX: actionables.TestActionable(
258                'provision_FirmwareUpdate'),
259        FW_RW_VERSION_PREFIX: actionables.TestActionable(
260                'provision_FirmwareUpdate',
261                extra_kwargs={'rw_only': True,
262                              'tag': 'rw_only'}),
263        ANDROID_BUILD_VERSION_PREFIX : actionables.TestActionable(
264                'provision_AndroidUpdate'),
265        TESTBED_BUILD_VERSION_PREFIX : actionables.TestActionable(
266                'provision_TestbedUpdate'),
267    }
268
269    name = 'provision'
270
271
272class Cleanup(_SpecialTaskAction):
273    """
274    Cleanup runs after a test fails to try and remove artifacts of tests and
275    ensure the DUT will be in a sane state for the next test run.
276    """
277
278    _actions = {
279        'cleanup-reboot': actionables.RebootActionable(),
280    }
281
282    name = 'cleanup'
283
284
285class Repair(_SpecialTaskAction):
286    """
287    Repair runs when one of the other special tasks fails.  It should be able
288    to take a component of the DUT that's in an unknown state and restore it to
289    a good state.
290    """
291
292    _actions = {
293    }
294
295    name = 'repair'
296
297
298# TODO(milleral): crbug.com/364273
299# Label doesn't really mean label in this context.  We're putting things into
300# DEPENDENCIES that really aren't DEPENDENCIES, and we should probably stop
301# doing that.
302def is_for_special_action(label):
303    """
304    If any special task handles the label specially, then we're using the label
305    to communicate that we want an action, and not as an actual dependency that
306    the test has.
307
308    @param label: A string label name.
309    @return True if any special task handles this label specially,
310            False if no special task handles this label.
311    """
312    return (Verify.acts_on(label) or
313            Provision.acts_on(label) or
314            Cleanup.acts_on(label) or
315            Repair.acts_on(label) or
316            label == SKIP_PROVISION)
317
318
319def join(provision_type, provision_value):
320    """
321    Combine the provision type and value into the label name.
322
323    @param provision_type: One of the constants that are the label prefixes.
324    @param provision_value: A string of the value for this provision type.
325    @returns: A string that is the label name for this (type, value) pair.
326
327    >>> join(CROS_VERSION_PREFIX, 'lumpy-release/R27-3773.0.0')
328    'cros-version:lumpy-release/R27-3773.0.0'
329
330    """
331    return '%s:%s' % (provision_type, provision_value)
332
333
334class SpecialTaskActionException(Exception):
335    """
336    Exception raised when a special task fails to successfully run a test that
337    is required.
338
339    This is also a literally meaningless exception.  It's always just discarded.
340    """
341
342
343def run_special_task_actions(job, host, labels, task):
344    """
345    Iterate through all `label`s and run any tests on `host` that `task` has
346    corresponding to the passed in labels.
347
348    Emits status lines for each run test, and INFO lines for each skipped label.
349
350    @param job: A job object from a control file.
351    @param host: The host to run actions on.
352    @param labels: The list of job labels to work on.
353    @param task: An instance of _SpecialTaskAction.
354    @returns: None
355    @raises: SpecialTaskActionException if a test fails.
356
357    """
358    capabilities, configurations = task.partition(labels)
359
360    for label in capabilities:
361        job.record('INFO', None, task.name,
362                   "Can't %s label '%s'." % (task.name, label))
363
364    # Sort the configuration labels based on `task._priorities`.
365    sorted_configurations = task.sort_configurations(configurations)
366    for name, value in sorted_configurations:
367        action_item = task.test_for(name)
368        success = action_item.execute(job=job, host=host, value=value)
369        if not success:
370            raise SpecialTaskActionException()
371