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