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