• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""
6Repair actions and verifiers relating to CrOS firmware.
7
8This contains the repair actions and verifiers need to find problems
9with the firmware installed on Chrome OS DUTs, and when necessary, to
10fix problems by updating or re-installing the firmware.
11
12The 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
22For purposes of the operations in the module, we distinguish three kinds
23of 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
37import logging
38import re
39
40import common
41from autotest_lib.client.common_lib import global_config
42from autotest_lib.client.common_lib import hosts
43from autotest_lib.server import afe_utils
44from 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
63def _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
79def _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
98def _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
121def _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
146class 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
186class 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
211class 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