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