Home | History | Annotate | Download | only in brillo_RecoverFromBadImage
      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 
      5 import logging
      6 import os
      7 import re
      8 
      9 import common
     10 from autotest_lib.client.common_lib import error
     11 from autotest_lib.server import test
     12 
     13 
     14 # The /dev directory mapping partition names to block devices.
     15 _BLK_DEV_BY_NAME_DIR = '/dev/block/by-name'
     16 # By default, we kill and recover the active system partition.
     17 _DEFAULT_PART_NAME = 'system_X'
     18 
     19 
     20 class brillo_RecoverFromBadImage(test.test):
     21     """Ensures that a Brillo device can recover from a bad image."""
     22     version = 1
     23 
     24 
     25     def resolve_slot(self, host, partition):
     26         """Resolves a partition slot (if any).
     27 
     28         @param host: A host object representing the DUT.
     29         @param partition: The name of the partition we are using. If it ends
     30                           with '_X' then we attempt to substitute it with some
     31                           non-active slot.
     32 
     33         @return A pair consisting of a fully resolved partition name and slot
     34                 index; the latter is None if the partition is not slotted.
     35 
     36         @raise TestError: If a target slot could not be resolved.
     37         """
     38         # Check if the partition is slotted.
     39         if not re.match('.+_[a-zX]$', partition):
     40             return partition, None
     41 
     42         try:
     43             current_slot = int(
     44                     host.run_output('bootctl get-current-slot').strip())
     45             if partition[-1] == 'X':
     46                 # Find a non-active target slot we could use.
     47                 num_slots = int(
     48                         host.run_output('bootctl get-number-slots').strip())
     49                 if num_slots < 2:
     50                     raise error.TestError(
     51                             'Device has no non-active slot that we can use')
     52                 target_slot = 0 if current_slot else 1
     53                 partition = partition[:-1] + chr(ord('a') + target_slot)
     54                 logging.info(
     55                         'Current slot is %d, partition resolved to %s '
     56                         '(slot %d)', current_slot, partition, target_slot)
     57             else:
     58                 # Make sure the partition slot is different from the active one.
     59                 target_slot = ord(partition[-1]) - ord('a')
     60                 if target_slot == current_slot:
     61                     target_slot = None
     62                     logging.warning(
     63                             'Partition %s is associated with the current boot '
     64                             'slot (%d), wiping it might fail if it is mounted',
     65                             partition, current_slot)
     66         except error.AutoservError:
     67             raise error.TestError('Error resolving device slots')
     68 
     69         return partition, target_slot
     70 
     71 
     72     def find_partition_device(self, host, partition):
     73         """Returns the block device of the partition.
     74 
     75         @param host: A host object representing the DUT.
     76         @param partition: The name of the partition we are using.
     77 
     78         @return Path to the device containing the partition.
     79 
     80         @raise TestError: If the partition name could not be mapped to a device.
     81         """
     82         try:
     83             cmd = 'find %s -type l' % os.path.join(_BLK_DEV_BY_NAME_DIR, '')
     84             for device in host.run_output(cmd).splitlines():
     85                 if os.path.basename(device) == partition:
     86                     logging.info('Mapped partition %s to device %s',
     87                                  partition, device)
     88                     return device
     89         except error.AutoservError:
     90             raise error.TestError(
     91                     'Error finding device for partition %s' % partition)
     92         raise error.TestError(
     93                 'No device found for partition %s' % partition)
     94 
     95 
     96     def get_device_block_info(self, host, device):
     97         """Returns the block size and count for a device.
     98 
     99         @param host: A host object representing the DUT.
    100         @param device: Path to a block device.
    101 
    102         @return A pair consisting of the block size (in bytes) and the total
    103                 number of blocks on the device.
    104 
    105         @raise TestError: If we failed to get the block info for the device.
    106         """
    107         try:
    108             block_size = int(
    109                     host.run_output('blockdev --getbsz %s' % device).strip())
    110             device_size = int(
    111                     host.run_output('blockdev --getsize64 %s' % device).strip())
    112         except error.AutoservError:
    113             raise error.TestError(
    114                     'Failed to get block info for device %s' % device)
    115         return block_size, device_size / block_size
    116 
    117 
    118     def run_once(self, host=None, image_file=None, partition=_DEFAULT_PART_NAME,
    119                  device=None):
    120         """Runs the test.
    121 
    122         @param host: A host object representing the DUT.
    123         @param image_file: Image file to flash to the partition.
    124         @param partition: Name of the partition to wipe/recover.
    125         @param device: Path to the partition block device.
    126 
    127         @raise TestError: Something went wrong while trying to execute the test.
    128         @raise TestFail: The test failed.
    129         """
    130         # Check that the image file exists.
    131         if image_file is None:
    132             raise error.TestError('No image file provided')
    133         if not os.path.isfile(image_file):
    134             raise error.TestError('Image file %s not found' % image_file)
    135 
    136         try:
    137             # Resolve partition name and slot.
    138             partition, target_slot = self.resolve_slot(host, partition)
    139 
    140             # Figure out the partition device.
    141             if device is None:
    142                 device = self.find_partition_device(host, partition)
    143 
    144             # Find the block size and count for the device.
    145             block_size, num_blocks = self.get_device_block_info(host, device)
    146 
    147             # Wipe the partition.
    148             logging.info('Wiping partition %s (%s)', partition, device)
    149             cmd = ('dd if=/dev/zero of=%s bs=%d count=%d' %
    150                    (device, block_size, num_blocks))
    151             run_err = 'Failed to wipe partition using %s' % cmd
    152             host.run(cmd)
    153 
    154             # Switch to the target slot, if required.
    155             if target_slot is not None:
    156                 run_err = 'Error setting the active boot slot'
    157                 host.run('bootctl set-active-boot-slot %d' % target_slot)
    158 
    159             # Re-flash the partition with fastboot.
    160             run_err = 'Failed to reboot the device into fastboot'
    161             host.ensure_bootloader_mode()
    162             run_err = 'Failed to flash image to partition %s' % partition
    163             host.fastboot_run('flash', args=(partition, image_file))
    164 
    165             # Reboot the device.
    166             run_err = 'Failed to reboot the device after flashing image'
    167             host.ensure_adb_mode()
    168 
    169             # Make sure we've booted from the alternate slot, if required.
    170             if target_slot is not None:
    171                 run_err = 'Error checking the current boot slot'
    172                 current_slot = int(
    173                         host.run_output('bootctl get-current-slot').strip())
    174                 if current_slot != target_slot:
    175                     logging.error('Rebooted from slot %d instead of %d',
    176                                   current_slot, target_slot)
    177                     raise error.TestError(
    178                             'Device did not reboot from the expected slot')
    179         except error.AutoservError:
    180             raise error.TestFail(run_err)
    181