Home | History | Annotate | Download | only in cros
      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 import collections
      6 import re
      7 import sys
      8 import warnings
      9 
     10 import common
     11 from autotest_lib.server.cros import provision_actionables as actionables
     12 from autotest_lib.utils import labellib
     13 from autotest_lib.utils.labellib import Key
     14 
     15 
     16 ### Constants for label prefixes
     17 CROS_VERSION_PREFIX = Key.CROS_VERSION
     18 CROS_ANDROID_VERSION_PREFIX = Key.CROS_ANDROID_VERSION
     19 ANDROID_BUILD_VERSION_PREFIX = Key.ANDROID_BUILD_VERSION
     20 TESTBED_BUILD_VERSION_PREFIX = Key.TESTBED_VERSION
     21 FW_RW_VERSION_PREFIX = Key.FIRMWARE_RW_VERSION
     22 FW_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.
     31 SKIP_PROVISION = 'skip_provision'
     32 
     33 # Postfix -cheetsth to distinguish ChromeOS build during Cheets provisioning.
     34 CHEETS_SUFFIX = '-cheetsth'
     35 
     36 # Default number of provisions attempts to try if we believe the devserver is
     37 # flaky.
     38 FLAKY_DEVSERVER_ATTEMPTS = 2
     39 
     40 
     41 _Action = collections.namedtuple('_Action', 'name, value')
     42 
     43 
     44 def _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
     62 def 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 
     97 def 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 
    112 def 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 
    125 def 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 
    138 class _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 
    270 class 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 
    293 class 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 
    336 class 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 
    349 class 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.
    366 def 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 
    383 def 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 
    398 class 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 
    407 def 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