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