Home | History | Annotate | Download | only in hosts
      1 # Copyright 2016 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 Repair actions and verifiers relating to CrOS firmware.
      7 
      8 This contains the repair actions and verifiers need to find problems
      9 with the firmware installed on Chrome OS DUTs, and when necessary, to
     10 fix problems by updating or re-installing the firmware.
     11 
     12 The operations in the module support two distinct use cases:
     13   * DUTs used for FAFT tests can in some cases have problems with
     14     corrupted firmware.  The module supplies `FirmwareStatusVerifier`
     15     to check for corruption, and supplies `FirmwareRepair` to re-install
     16     firmware via servo when needed.
     17   * DUTs used for general testing normally should be running a
     18     designated "stable" firmware version.  This module supplies
     19     `FirmwareVersionVerifier` to detect and automatically update
     20     firmware that is out-of-date from the designated version.
     21 
     22 For purposes of the operations in the module, we distinguish three kinds
     23 of DUT, based on pool assignments:
     24   * DUTs used for general testing.  These DUTs automatically check for
     25     and install the stable firmware using `FirmwareVersionVerifier`.
     26   * DUTs in pools used for FAFT testing.  These check for bad firmware
     27     builds with `FirmwareStatusVerifier`, and will fix problems using
     28     `FirmwareRepair`.  These DUTs don't check for or install the
     29     stable firmware.
     30   * DUTs not in general pools, and not used for FAFT.  These DUTs
     31     are expected to be managed by separate processes and are excluded
     32     from all of the verification and repair code in this module.
     33 """
     34 
     35 # pylint: disable=missing-docstring
     36 
     37 import logging
     38 import re
     39 
     40 import common
     41 from autotest_lib.client.common_lib import global_config
     42 from autotest_lib.client.common_lib import hosts
     43 from autotest_lib.server import afe_utils
     44 from autotest_lib.server import constants
     45 
     46 
     47 # _FIRMWARE_REPAIR_POOLS - The set of pools that should be
     48 # managed by `FirmwareStatusVerifier` and `FirmwareRepair`.
     49 #
     50 _FIRMWARE_REPAIR_POOLS = set(
     51     global_config.global_config.get_config_value(
     52             'CROS',
     53             'pools_support_firmware_repair',
     54             type=str).split(','))
     55 
     56 
     57 # _FIRMWARE_UPDATE_POOLS - The set of pools that should be
     58 # managed by `FirmwareVersionVerifier`.
     59 #
     60 _FIRMWARE_UPDATE_POOLS = set(constants.Pools.MANAGED_POOLS)
     61 
     62 
     63 def _is_firmware_repair_supported(host):
     64     """
     65     Check if a host supports firmware repair.
     66 
     67     When this function returns true, the DUT should be managed by
     68     `FirmwareStatusVerifier` and `FirmwareRepair`, but not
     69     `FirmwareVersionVerifier`.  In general, this applies to DUTs
     70     used for firmware testing.
     71 
     72     @return A true value if the host should use `FirmwareStatusVerifier`
     73             and `FirmwareRepair`; a false value otherwise.
     74     """
     75     info = host.host_info_store.get()
     76     return bool(info.pools & _FIRMWARE_REPAIR_POOLS)
     77 
     78 
     79 def _is_firmware_update_supported(host):
     80     """
     81     Return whether a DUT should be running the standard firmware.
     82 
     83     In the test lab, DUTs used for general testing, (e.g. the `bvt`
     84     pool) need their firmware kept up-to-date with
     85     `FirmwareVersionVerifier`.  However, some pools have alternative
     86     policies for firmware management.  This returns whether a given DUT
     87     should be updated via the standard stable version update, or
     88     managed by some other procedure.
     89 
     90     @param host   The host to be checked for update policy.
     91     @return A true value if the host should use
     92             `FirmwareVersionVerifier`; a false value otherwise.
     93     """
     94     info = host.host_info_store.get()
     95     return bool(info.pools & _FIRMWARE_UPDATE_POOLS)
     96 
     97 
     98 def _get_firmware_version(output):
     99     """Parse the output and get the firmware version.
    100 
    101     @param output   The standard output of chromeos-firmwareupdate script.
    102     @return Firmware version if found, else, None.
    103     """
    104     # At one point, the chromeos-firmwareupdate script was updated to
    105     # add "RW" version fields.  The old string, "BIOS version:" still
    106     # appears in the new output, however it now refers to the RO
    107     # firmware version.  Therefore, we try searching for the new string
    108     # first, "BIOS (RW) version".  If that string isn't found, we then
    109     # fallback to searching for old string.
    110     version = re.search(r'BIOS \(RW\) version:\s*(?P<version>.*)', output)
    111 
    112     if not version:
    113         version = re.search(r'BIOS version:\s*(?P<version>.*)', output)
    114 
    115     if version is not None:
    116         return version.group('version')
    117 
    118     return None
    119 
    120 
    121 def _get_available_firmware(host, model):
    122     """Get the available firmware version given the model.
    123 
    124     @param host     The host to get available firmware for.
    125     @param model    The model name to get corresponding firmware version.
    126     @return The available firmware version if found, else, None.
    127     """
    128     result = host.run('chromeos-firmwareupdate -V', ignore_status=True)
    129 
    130     if result.exit_status == 0:
    131         unibuild = False
    132         paragraphs = result.stdout.split('\n\n')
    133         for p in paragraphs:
    134             match = re.search(r'Model:\s*(?P<model>.*)', p)
    135             if match:
    136                 unibuild = True
    137                 if model == match.group('model'):
    138                     return _get_firmware_version(p)
    139 
    140         if not unibuild:
    141             return _get_firmware_version(result.stdout)
    142 
    143     return None
    144 
    145 
    146 class FirmwareStatusVerifier(hosts.Verifier):
    147     """
    148     Verify that a host's firmware is in a good state.
    149 
    150     For DUTs that run firmware tests, it's possible that the firmware
    151     on the DUT can get corrupted.  This verifier checks whether it
    152     appears that firmware should be re-flashed using servo.
    153     """
    154 
    155     def verify(self, host):
    156         if not _is_firmware_repair_supported(host):
    157             return
    158         try:
    159             # Read the AP firmware and dump the sections that we're
    160             # interested in.
    161             cmd = ('mkdir /tmp/verify_firmware; '
    162                    'cd /tmp/verify_firmware; '
    163                    'for section in VBLOCK_A VBLOCK_B FW_MAIN_A FW_MAIN_B; '
    164                    'do flashrom -r image.bin -i $section:$section; '
    165                    'done')
    166             host.run(cmd)
    167 
    168             # Verify the firmware blocks A and B.
    169             cmd = ('vbutil_firmware --verify /tmp/verify_firmware/VBLOCK_%c'
    170                    ' --signpubkey /usr/share/vboot/devkeys/root_key.vbpubk'
    171                    ' --fv /tmp/verify_firmware/FW_MAIN_%c')
    172             for c in ('A', 'B'):
    173                 rv = host.run(cmd % (c, c), ignore_status=True)
    174                 if rv.exit_status:
    175                     raise hosts.AutoservVerifyError(
    176                             'Firmware %c is in a bad state.' % c)
    177         finally:
    178             # Remove the temporary files.
    179             host.run('rm -rf /tmp/verify_firmware')
    180 
    181     @property
    182     def description(self):
    183         return 'Firmware on this DUT is clean'
    184 
    185 
    186 class FirmwareRepair(hosts.RepairAction):
    187     """
    188     Reinstall the firmware image using servo.
    189 
    190     This repair function attempts to use servo to install the DUT's
    191     designated "stable firmware version".
    192 
    193     This repair method only applies to DUTs used for FAFT.
    194     """
    195 
    196     def repair(self, host):
    197         if not _is_firmware_repair_supported(host):
    198             raise hosts.AutoservRepairError(
    199                     'Firmware repair is not applicable to host %s.' %
    200                     host.hostname)
    201         if not host.servo:
    202             raise hosts.AutoservRepairError(
    203                     '%s has no servo support.' % host.hostname)
    204         host.firmware_install()
    205 
    206     @property
    207     def description(self):
    208         return 'Re-install the stable firmware via servo'
    209 
    210 
    211 class FirmwareVersionVerifier(hosts.Verifier):
    212     """
    213     Check for a firmware update, and apply it if appropriate.
    214 
    215     This verifier checks to ensure that either the firmware on the DUT
    216     is up-to-date, or that the target firmware can be installed from the
    217     currently running build.
    218 
    219     Failure occurs when all of the following apply:
    220      1. The DUT is not excluded from updates.  For example, DUTs used
    221         for FAFT testing use `FirmwareRepair` instead.
    222      2. The DUT's board has an assigned stable firmware version.
    223      3. The DUT is not running the assigned stable firmware.
    224      4. The firmware supplied in the running OS build is not the
    225         assigned stable firmware.
    226 
    227     If the DUT needs an upgrade and the currently running OS build
    228     supplies the necessary firmware, the verifier installs the new
    229     firmware using `chromeos-firmwareupdate`.  Failure to install will
    230     cause the verifier to fail.
    231 
    232     This verifier nominally breaks the rule that "verifiers must succeed
    233     quickly", since it can invoke `reboot()` during the success code
    234     path.  We're doing it anyway for two reasons:
    235       * The time between updates will typically be measured in months,
    236         so the amortized cost is low.
    237       * The reason we distinguish repair from verify is to allow
    238         rescheduling work immediately while the expensive repair happens
    239         out-of-band.  But a firmware update will likely hit all DUTs at
    240         once, so it's pointless to pass the buck to repair.
    241 
    242     N.B. This verifier is a trigger for all repair actions that install
    243     the stable repair image.  If the firmware is out-of-date, but the
    244     stable repair image does *not* contain the proper firmware version,
    245     _the target DUT will fail repair, and will be unable to fix itself_.
    246     """
    247 
    248     @staticmethod
    249     def _get_rw_firmware(host):
    250         result = host.run('crossystem fwid', ignore_status=True)
    251         if result.exit_status == 0:
    252             return result.stdout
    253         else:
    254             return None
    255 
    256     @staticmethod
    257     def _check_hardware_match(version_a, version_b):
    258         """
    259         Check that two firmware versions identify the same hardware.
    260 
    261         Firmware version strings look like this:
    262             Google_Gnawty.5216.239.34
    263         The part before the numbers identifies the hardware for which
    264         the firmware was built.  This function checks that the hardware
    265         identified by `version_a` and `version_b` is the same.
    266 
    267         This is a sanity check to protect us from installing the wrong
    268         firmware on a DUT when a board label has somehow gone astray.
    269 
    270         @param version_a  First firmware version for the comparison.
    271         @param version_b  Second firmware version for the comparison.
    272         """
    273         hardware_a = version_a.split('.')[0]
    274         hardware_b = version_b.split('.')[0]
    275         if hardware_a != hardware_b:
    276             message = 'Hardware/Firmware mismatch updating %s to %s'
    277             raise hosts.AutoservVerifyError(
    278                     message % (version_a, version_b))
    279 
    280     def verify(self, host):
    281         # Test 1 - The DUT is not excluded from updates.
    282         if not _is_firmware_update_supported(host):
    283             return
    284         # Test 2 - The DUT has an assigned stable firmware version.
    285         info = host.host_info_store.get()
    286         if info.model is None:
    287             raise hosts.AutoservVerifyError(
    288                     'Can not verify firmware version. '
    289                     'No model label value found')
    290 
    291         stable_firmware = afe_utils.get_stable_firmware_version(info.model)
    292         if stable_firmware is None:
    293             # This DUT doesn't have a firmware update target
    294             return
    295 
    296         # For tests 3 and 4:  If the output from `crossystem` or
    297         # `chromeos-firmwareupdate` isn't what we expect, we log an
    298         # error, but don't fail:  We don't want DUTs unable to test a
    299         # build merely because of a bug or change in either of those
    300         # commands.
    301 
    302         # Test 3 - The DUT is not running the target stable firmware.
    303         current_firmware = self._get_rw_firmware(host)
    304         if current_firmware is None:
    305             logging.error('DUT firmware version can\'t be determined.')
    306             return
    307         if current_firmware == stable_firmware:
    308             return
    309         # Test 4 - The firmware supplied in the running OS build is not
    310         # the assigned stable firmware.
    311         available_firmware = _get_available_firmware(host, info.model)
    312         if available_firmware is None:
    313             logging.error('Supplied firmware version in OS can\'t be '
    314                           'determined.')
    315             return
    316         if available_firmware != stable_firmware:
    317             raise hosts.AutoservVerifyError(
    318                     'DUT firmware requires update from %s to %s' %
    319                     (current_firmware, stable_firmware))
    320         # Time to update the firmware.
    321         logging.info('Updating firmware from %s to %s',
    322                      current_firmware, stable_firmware)
    323         self._check_hardware_match(current_firmware, stable_firmware)
    324         try:
    325             host.run('chromeos-firmwareupdate --mode=autoupdate')
    326             host.reboot()
    327         except Exception as e:
    328             message = ('chromeos-firmwareupdate failed: from '
    329                        '%s to %s')
    330             logging.exception(message, current_firmware, stable_firmware)
    331             raise hosts.AutoservVerifyError(
    332                     message % (current_firmware, stable_firmware))
    333         final_firmware = self._get_rw_firmware(host)
    334         if final_firmware != stable_firmware:
    335             message = ('chromeos-firmwareupdate failed: tried upgrade '
    336                        'to %s, now running %s instead')
    337             raise hosts.AutoservVerifyError(
    338                     message % (stable_firmware, final_firmware))
    339 
    340     @property
    341     def description(self):
    342         return 'The firmware on this DUT is up-to-date'
    343