1# Copyright 2015 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 5import logging 6import os 7import six 8 9from autotest_lib.client.common_lib import error 10from autotest_lib.client.common_lib import utils 11from autotest_lib.server.cros.faft.firmware_test import FirmwareTest 12 13 14class firmware_FWupdate(FirmwareTest): 15 """RO+RW firmware update using chromeos-firmware with various modes. 16 If restore=false is given, the DUT is left running that firmware, so the 17 test can be used to apply updates. 18 19 Accepted --args names: 20 old_bios= 21 old_bios_ro= 22 old_bios_rw= 23 old_ec= 24 old_pd= 25 new_bios= 26 new_bios_ro= 27 new_bios_rw= 28 new_ec= 29 new_pd= 30 apply the given image(s) 31 restore=false|true (default true) 32 if value is anything but 'false', once the test ends, the firmware will 33 be restored to the backup that was made at the start of the test. 34 """ 35 # Region to use for flashrom wp-region commands 36 WP_REGION = 'WP_RO' 37 38 MODE = 'recovery' 39 40 def initialize(self, host, cmdline_args): 41 42 self.flashed = False 43 self._want_restore = None 44 self._orig_sw_wp = {} 45 self._orig_hw_wp = None 46 47 dict_args = utils.args_to_dict(cmdline_args) 48 49 if dict_args.get('restore', '').lower() == 'false': 50 self._want_restore = False 51 else: 52 self._want_restore = True 53 54 self.images = {} 55 56 for old_or_new in ('old', 'new'): 57 for target in ('bios', 'bios_ro', 'bios_rw', 'ec', 'pd'): 58 arg_name = '%s_%s' % (old_or_new, target) 59 arg_value = dict_args.get(arg_name) 60 if arg_value: 61 logging.info('%s=%s', arg_name, arg_value) 62 image_path = os.path.expanduser(arg_value) 63 if not os.path.isabs(image_path): 64 raise error.TestError( 65 'Specified path must be absolute: %s=%s' 66 % (arg_name, arg_value)) 67 if not os.path.isfile(image_path): 68 raise error.TestError( 69 'Specified file does not exist: %s=%s' 70 % (arg_name, arg_value)) 71 self.images[arg_name] = image_path 72 73 self.old_bios = self.images.get('old_bios') 74 self.old_bios_ro = self.images.get('old_bios_ro') 75 self.old_bios_rw = self.images.get('old_bios_rw') 76 77 self.new_bios = self.images.get('new_bios') 78 self.new_bios_ro = self.images.get('new_bios_ro') 79 self.new_bios_rw = self.images.get('new_bios_rw') 80 81 if not (self.new_bios or self.new_bios_rw): 82 raise error.TestError('Must specify at least new_bios=<path>' 83 ' or new_bios_rw=<path>') 84 85 super(firmware_FWupdate, self).initialize(host, cmdline_args) 86 87 self._orig_sw_wp = self.faft_client.bios.get_write_protect_status() 88 89 self.backup_firmware() 90 91 if self.faft_config.ap_access_ec_flash: 92 self._setup_ec_write_protect(False) 93 94 self._original_hw_wp = 'on' in self.servo.get('fw_wp_state') 95 96 self.set_ap_write_protect_and_reboot(False) 97 self.faft_client.bios.set_write_protect_region(self.WP_REGION, True) 98 self.set_ap_write_protect_and_reboot(True) 99 100 def cleanup(self): 101 """Restore write protection, unless "restore" was false.""" 102 if not hasattr(self, 'run_id'): 103 # Exited very early during initialize, so no cleanup needed 104 return 105 106 self.set_ap_write_protect_and_reboot(False) 107 try: 108 if self.flashed and self._want_restore and self.is_firmware_saved(): 109 self.restore_firmware() 110 except (EnvironmentError, six.moves.xmlrpc_client.Fault, 111 error.AutoservError, error.TestBaseException): 112 logging.error("Problem restoring firmware:", exc_info=True) 113 114 try: 115 # Restore the old write-protection value at the end of the test. 116 if self._orig_sw_wp: 117 self.faft_client.bios.set_write_protect_range( 118 self._orig_sw_wp['start'], 119 self._orig_sw_wp['length'], 120 self._orig_sw_wp['enabled']) 121 except (EnvironmentError, six.moves.xmlrpc_client.Fault, 122 error.AutoservError, error.TestBaseException): 123 logging.error("Problem restoring software write-protect:", 124 exc_info=True) 125 126 if self._orig_hw_wp is not None: 127 self.set_ap_write_protect_and_reboot(self._orig_hw_wp) 128 elif hasattr(self, 'ec'): 129 self.sync_and_ec_reboot() 130 131 super(firmware_FWupdate, self).cleanup() 132 133 def get_installed_versions(self): 134 """Get the installed versions of BIOS and EC firmware. 135 136 @return: A nested dict keyed by target ('bios' or 'ec') and then section 137 @rtype: dict 138 """ 139 versions = dict() 140 versions['bios'] = self.faft_client.updater.get_device_fwids('bios') 141 if self.faft_config.chrome_ec: 142 versions['ec'] = self.faft_client.updater.get_device_fwids('ec') 143 return versions 144 145 def check_bios_specified(self, 146 old_ro=False, old_rw=False, new_ro=False, new_rw=False): 147 """Check if the required --args were specified. 148 149 @raise error.TestError: if args required for the test were not given 150 """ 151 missing = set() 152 if old_ro and not (self.old_bios_ro or self.old_bios): 153 missing.add('old_bios[_ro]=') 154 if old_rw and not (self.old_bios_rw or self.old_bios): 155 missing.add('old_bios[_rw]=') 156 if new_ro and not (self.new_bios_ro or self.new_bios): 157 missing.add('new_bios[_ro]=') 158 if new_rw and not (self.new_bios_rw or self.new_bios): 159 missing.add('new_bios[_rw]=') 160 if missing: 161 raise error.TestError('Must specify args: %s' % '; '.join(missing)) 162 163 def copy_cmdline_images(self, old_or_new, section=None): 164 """Copy the specified command line images into the extracted shellball. 165 166 @param old_or_new: 'old' or 'new', to select a set from self.images 167 @param section: 'ro' or 'rw', to use bios_ro or bios_rw. 168 """ 169 local_bios = ( 170 self.images.get('%s_bios_%s' % (old_or_new, section)) or 171 self.images.get('%s_bios' % old_or_new) 172 ) 173 local_ec = self.images.get('%s_ec' % old_or_new) 174 local_pd = self.images.get('%s_pd' % old_or_new) 175 176 extract_dir = self.faft_client.updater.get_work_path() 177 178 if local_bios: 179 bios_rel = self.faft_client.updater.get_bios_relative_path() 180 remote_bios = os.path.join(extract_dir, bios_rel) 181 self._client.send_file(local_bios, remote_bios) 182 183 if local_ec: 184 ec_rel = self.faft_client.updater.get_ec_relative_path() 185 remote_ec = os.path.join(extract_dir, ec_rel) 186 self._client.send_file(local_ec, remote_ec) 187 188 if local_pd: 189 # note: pd.bin might likewise need special path logic 190 remote_pd = os.path.join(extract_dir, 'pd.bin') 191 self._client.send_file(local_pd, remote_pd) 192 193 def prepare_shellball(self, old_or_new, ro_or_rw=None): 194 """Prepare a shellball with the given set of images (old or new). 195 196 @param old_or_new: 'old' or 'new', to select a set from self.images 197 @param section: 'ro' or 'rw', to use bios_ro or bios_rw. 198 """ 199 self.faft_client.updater.reset_shellball() 200 self.copy_cmdline_images(old_or_new, ro_or_rw) 201 self.faft_client.updater.reload_images() 202 self.faft_client.updater.repack_shellball(old_or_new) 203 204 def run_shellball(self, append, wp, host_only=False): 205 """Run chromeos-firmwareupdate with given sub-case 206 207 @param append: additional piece to add to shellball name 208 @param wp: is the flash write protected (--wp)? 209 @return: a list of failure messages for the case 210 """ 211 have_ec = bool(self.faft_config.chrome_ec) 212 options = [] 213 214 if host_only: 215 options += ['--host_only'] 216 options += ['--quirks=ec_partial_recovery=0'] 217 218 before_fwids = self.get_installed_versions() 219 image_fwids = self.identify_shellball(include_ec=have_ec) 220 221 # Unlock the protection of the wp-enable and wp-range registers 222 self.set_ap_write_protect_and_reboot(False) 223 224 if wp: 225 self.faft_client.bios.set_write_protect_region( 226 self.WP_REGION, True) 227 self.set_ap_write_protect_and_reboot(True) 228 else: 229 self.faft_client.bios.set_write_protect_region( 230 self.WP_REGION, False) 231 232 cmd_desc = ['chromeos-firmwareupdate-%s' % append, 233 '--mode=%s' % self.MODE] 234 cmd_desc += options 235 cmd_desc += ['[wp=%s]' % wp] 236 cmd_desc = ' '.join(cmd_desc) 237 238 expected_written = {} 239 240 if wp: 241 bios_written = ['a', 'b'] 242 ec_written = [] # EC write is all-or-nothing 243 244 elif host_only: 245 bios_written = ['ro', 'a', 'b'] 246 ec_written = [] 247 248 else: 249 bios_written = ['ro', 'a', 'b'] 250 ec_written = ['ro', 'rw'] 251 252 expected_written['bios'] = bios_written 253 254 if self.faft_config.chrome_ec and ec_written: 255 expected_written['ec'] = ec_written 256 257 # remove quotes and braces: bios: [a, b], ec: [ro, rw] 258 written_desc = repr(expected_written).replace("'", "")[1:-1] 259 logging.debug('Before(%s): %s', append, before_fwids) 260 logging.debug('Image(%s): %s', append, image_fwids) 261 logging.info("Run %s (should write %s)", cmd_desc, written_desc) 262 263 # make sure we restore firmware after the test, if it tried to flash. 264 self.flashed = True 265 266 errors = [] 267 result = self.run_chromeos_firmwareupdate( 268 self.MODE, append, options, ignore_status=True) 269 270 if result.exit_status == 255: 271 logging.warning("DUT network dropped during update.") 272 elif result.exit_status != 0: 273 if (image_fwids == before_fwids and 274 'Good. It seems nothing was changed.' in result.stdout): 275 logging.info("DUT already matched the image; updater aborted.") 276 else: 277 errors.append('...updater: unexpectedly failed (rc=%s)' % 278 result.exit_status) 279 280 after_fwids = self.get_installed_versions() 281 logging.debug('After(%s): %s', append, after_fwids) 282 283 errors += self.check_fwids_written( 284 before_fwids, image_fwids, after_fwids, expected_written) 285 286 if errors: 287 logging.debug('%s', '\n'.join(errors)) 288 return ["%s (should write %s)\n%s" 289 % (cmd_desc, written_desc, '\n'.join(errors))] 290 return [] 291 292 def test_upgrade_rw(self, raise_error=True): 293 """Test case: RO=old, RW=new""" 294 logging.info('%s', self.test_upgrade_rw.__doc__) 295 self.check_bios_specified(old_ro=True, new_rw=True) 296 297 errors = [] 298 299 # wp=0: update RO+RW 300 self.prepare_shellball('old', 'ro') 301 errors += self.run_shellball('old', wp=0) 302 303 # wp=1: update RW 304 self.prepare_shellball('new', 'rw') 305 errors += self.run_shellball('new', wp=1) 306 307 self.reboot_and_reset_tpm() 308 self.sync_and_ec_reboot() 309 self.switcher.wait_for_client() 310 311 if errors: 312 fail_msg = "After flashing new RW over old RO, FWIDs were wrong." 313 errors.insert(0, fail_msg) 314 if raise_error: 315 raise error.TestFail('\n'.join(errors)) 316 return ['\n'.join(errors)] 317 return [] 318 319 def test_downgrade_rw(self, raise_error=True): 320 """Test case: RO=old, RW=old->new->old (with reboots)""" 321 logging.info('%s', self.test_downgrade_rw.__doc__) 322 self.check_bios_specified(old_ro=True, old_rw=True, new_rw=True) 323 errors = [] 324 325 # wp=0: update RO+RW 326 self.prepare_shellball('old', 'ro') 327 errors += self.run_shellball('old', wp=0) 328 329 self.reboot_and_reset_tpm() 330 self.sync_and_ec_reboot() 331 self.switcher.wait_for_client() 332 333 self.prepare_shellball('new', 'rw') 334 errors += self.run_shellball('new', wp=1) 335 336 self.sync_and_ec_reboot() 337 self.switcher.wait_for_client() 338 339 # Downgrade BIOS RW, but leave EC/PD at newer firmware 340 self.prepare_shellball('old', 'rw') 341 errors += self.run_shellball('old', wp=1, host_only=True) 342 343 self.reboot_and_reset_tpm() 344 self.sync_and_ec_reboot() 345 self.switcher.wait_for_client() 346 347 if errors: 348 fail_msg = "After upgrading then downgrading RW, FWIDs were wrong." 349 errors.insert(0, fail_msg) 350 if raise_error: 351 raise error.TestFail('\n'.join(errors)) 352 return ['\n'.join(errors)] 353 return [] 354 355 def test_new(self, raise_error=True): 356 """Test case: RO=new, RW=new""" 357 logging.info('%s', self.test_new.__doc__) 358 self.check_bios_specified(new_ro=True, old_rw=True) 359 360 errors = [] 361 362 # wp=0: update RO+RW 363 self.prepare_shellball('new', 'ro') 364 errors += self.run_shellball('new', wp=0) 365 366 self.reboot_and_reset_tpm() 367 self.sync_and_ec_reboot() 368 self.switcher.wait_for_client() 369 370 if errors: 371 fail_msg = "After flashing new RO+RW, FWIDs were wrong." 372 errors.insert(0, fail_msg) 373 if raise_error: 374 raise error.TestFail('\n'.join(errors)) 375 return ['\n'.join(errors)] 376 return [] 377 378 def test_old(self, raise_error=True): 379 """Test case: RO=old, RW=old""" 380 logging.info('%s', self.test_old.__doc__) 381 self.check_bios_specified(old_ro=True, old_rw=True) 382 383 errors = [] 384 385 # wp=0: update RO+RW 386 self.prepare_shellball('old', 'ro') 387 errors += self.run_shellball('old', wp=0) 388 389 self.reboot_and_reset_tpm() 390 self.sync_and_ec_reboot() 391 self.switcher.wait_for_client() 392 393 if errors: 394 fail_msg = "After flashing old RO+RW, FWIDs were wrong." 395 errors.insert(0, fail_msg) 396 if raise_error: 397 raise error.TestFail('\n'.join(errors)) 398 return ['\n'.join(errors)] 399 return [] 400 401 def test(self): 402 """Run all the test_* cases""" 403 self.check_bios_specified(old_ro=True, old_rw=True, new_rw=True) 404 405 errors = [] 406 errors += self.test_old(raise_error=False) 407 errors += self.test_downgrade_rw(raise_error=False) 408 errors += self.test_upgrade_rw(raise_error=False) 409 if self.new_bios or (self.new_bios_ro and self.new_bios_rw): 410 errors += self.test_new(raise_error=False) 411 else: 412 logging.warning("No 'new_bios_ro' given, skipping: %s", 413 self.test_new.__doc__) 414 if errors: 415 if len(errors) > 1: 416 errors.insert(0, "%s RO+RW combinations failed." % len(errors)) 417 raise error.TestFail('\n'.join(errors)) 418