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 
      6 import re
      7 import sys
      8 
      9 import common
     10 from autotest_lib.server.cros import provision_actionables as actionables
     11 
     12 
     13 ### Constants for label prefixes
     14 CROS_VERSION_PREFIX = 'cros-version'
     15 ANDROID_BUILD_VERSION_PREFIX = 'ab-version'
     16 TESTBED_BUILD_VERSION_PREFIX = 'testbed-version'
     17 FW_RW_VERSION_PREFIX = 'fwrw-version'
     18 FW_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.
     24 SKIP_PROVISION = 'skip_provision'
     25 
     26 # Default number of provisions attempts to try if we believe the devserver is
     27 # flaky.
     28 FLAKY_DEVSERVER_ATTEMPTS = 2
     29 
     30 
     31 ### Helpers to convert value to label
     32 def 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 
     63 def 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 
     78 def 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 
     89 def 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 
    100 def 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 
    111 class _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 
    209 class 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 
    232 class 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 
    272 class 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 
    285 class 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.
    302 def 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 
    319 def 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 
    334 class 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 
    343 def 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