| # Copyright 2015 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import logging |
| import os |
| import re |
| |
| import common |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.server import test |
| |
| |
| # The /dev directory mapping partition names to block devices. |
| _BLK_DEV_BY_NAME_DIR = '/dev/block/by-name' |
| # By default, we kill and recover the active system partition. |
| _DEFAULT_PART_NAME = 'system_X' |
| |
| |
| class brillo_RecoverFromBadImage(test.test): |
| """Ensures that a Brillo device can recover from a bad image.""" |
| version = 1 |
| |
| |
| def resolve_slot(self, host, partition): |
| """Resolves a partition slot (if any). |
| |
| @param host: A host object representing the DUT. |
| @param partition: The name of the partition we are using. If it ends |
| with '_X' then we attempt to substitute it with some |
| non-active slot. |
| |
| @return A pair consisting of a fully resolved partition name and slot |
| index; the latter is None if the partition is not slotted. |
| |
| @raise TestError: If a target slot could not be resolved. |
| """ |
| # Check if the partition is slotted. |
| if not re.match('.+_[a-zX]$', partition): |
| return partition, None |
| |
| try: |
| current_slot = int( |
| host.run_output('bootctl get-current-slot').strip()) |
| if partition[-1] == 'X': |
| # Find a non-active target slot we could use. |
| num_slots = int( |
| host.run_output('bootctl get-number-slots').strip()) |
| if num_slots < 2: |
| raise error.TestError( |
| 'Device has no non-active slot that we can use') |
| target_slot = 0 if current_slot else 1 |
| partition = partition[:-1] + chr(ord('a') + target_slot) |
| logging.info( |
| 'Current slot is %d, partition resolved to %s ' |
| '(slot %d)', current_slot, partition, target_slot) |
| else: |
| # Make sure the partition slot is different from the active one. |
| target_slot = ord(partition[-1]) - ord('a') |
| if target_slot == current_slot: |
| target_slot = None |
| logging.warning( |
| 'Partition %s is associated with the current boot ' |
| 'slot (%d), wiping it might fail if it is mounted', |
| partition, current_slot) |
| except error.AutoservError: |
| raise error.TestError('Error resolving device slots') |
| |
| return partition, target_slot |
| |
| |
| def find_partition_device(self, host, partition): |
| """Returns the block device of the partition. |
| |
| @param host: A host object representing the DUT. |
| @param partition: The name of the partition we are using. |
| |
| @return Path to the device containing the partition. |
| |
| @raise TestError: If the partition name could not be mapped to a device. |
| """ |
| try: |
| cmd = 'find %s -type l' % os.path.join(_BLK_DEV_BY_NAME_DIR, '') |
| for device in host.run_output(cmd).splitlines(): |
| if os.path.basename(device) == partition: |
| logging.info('Mapped partition %s to device %s', |
| partition, device) |
| return device |
| except error.AutoservError: |
| raise error.TestError( |
| 'Error finding device for partition %s' % partition) |
| raise error.TestError( |
| 'No device found for partition %s' % partition) |
| |
| |
| def get_device_block_info(self, host, device): |
| """Returns the block size and count for a device. |
| |
| @param host: A host object representing the DUT. |
| @param device: Path to a block device. |
| |
| @return A pair consisting of the block size (in bytes) and the total |
| number of blocks on the device. |
| |
| @raise TestError: If we failed to get the block info for the device. |
| """ |
| try: |
| block_size = int( |
| host.run_output('blockdev --getbsz %s' % device).strip()) |
| device_size = int( |
| host.run_output('blockdev --getsize64 %s' % device).strip()) |
| except error.AutoservError: |
| raise error.TestError( |
| 'Failed to get block info for device %s' % device) |
| return block_size, device_size / block_size |
| |
| |
| def run_once(self, host=None, image_file=None, partition=_DEFAULT_PART_NAME, |
| device=None): |
| """Runs the test. |
| |
| @param host: A host object representing the DUT. |
| @param image_file: Image file to flash to the partition. |
| @param partition: Name of the partition to wipe/recover. |
| @param device: Path to the partition block device. |
| |
| @raise TestError: Something went wrong while trying to execute the test. |
| @raise TestFail: The test failed. |
| """ |
| # Check that the image file exists. |
| if image_file is None: |
| raise error.TestError('No image file provided') |
| if not os.path.isfile(image_file): |
| raise error.TestError('Image file %s not found' % image_file) |
| |
| try: |
| # Resolve partition name and slot. |
| partition, target_slot = self.resolve_slot(host, partition) |
| |
| # Figure out the partition device. |
| if device is None: |
| device = self.find_partition_device(host, partition) |
| |
| # Find the block size and count for the device. |
| block_size, num_blocks = self.get_device_block_info(host, device) |
| |
| # Wipe the partition. |
| logging.info('Wiping partition %s (%s)', partition, device) |
| cmd = ('dd if=/dev/zero of=%s bs=%d count=%d' % |
| (device, block_size, num_blocks)) |
| run_err = 'Failed to wipe partition using %s' % cmd |
| host.run(cmd) |
| |
| # Switch to the target slot, if required. |
| if target_slot is not None: |
| run_err = 'Error setting the active boot slot' |
| host.run('bootctl set-active-boot-slot %d' % target_slot) |
| |
| # Re-flash the partition with fastboot. |
| run_err = 'Failed to reboot the device into fastboot' |
| host.ensure_bootloader_mode() |
| run_err = 'Failed to flash image to partition %s' % partition |
| host.fastboot_run('flash', args=(partition, image_file)) |
| |
| # Reboot the device. |
| run_err = 'Failed to reboot the device after flashing image' |
| host.ensure_adb_mode() |
| |
| # Make sure we've booted from the alternate slot, if required. |
| if target_slot is not None: |
| run_err = 'Error checking the current boot slot' |
| current_slot = int( |
| host.run_output('bootctl get-current-slot').strip()) |
| if current_slot != target_slot: |
| logging.error('Rebooted from slot %d instead of %d', |
| current_slot, target_slot) |
| raise error.TestError( |
| 'Device did not reboot from the expected slot') |
| except error.AutoservError: |
| raise error.TestFail(run_err) |