# 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 common from autotest_lib.client.common_lib import error from autotest_lib.server import test def _assert_equal(expected, actual): """Compares objects. @param expected: the expected value. @param actual: the actual value. @raises error.TestFail """ if expected != actual: raise error.TestFail('Expected: %r, actual: %r' % (expected, actual)) class brillo_BootLoader(test.test): """A/B tests for boot loader and boot_control HAL implementation.""" version = 1 def get_slots_and_suffix(self): """Gets number of slots supported and slot suffixes used. Prerequisite: The DUT is in ADB mode. """ self.num_slots = int(self.dut.run_output('bootctl get-number-slots')) logging.info('Number of slots: %d', self.num_slots) self.suffix_a = self.dut.run_output('bootctl get-suffix 0') self.suffix_b = self.dut.run_output('bootctl get-suffix 1') logging.info('Slot 0 suffix: "%s"', self.suffix_a) logging.info('Slot 1 suffix: "%s"', self.suffix_b) _assert_equal(2, self.num_slots) # We're going to need the size of the boot partitions later. self.boot_a_size = int(self.dut.run_output( 'blockdev --getsize64 /dev/block/by-name/boot%s' % self.suffix_a)) self.boot_b_size = int(self.dut.run_output( 'blockdev --getsize64 /dev/block/by-name/boot%s' % self.suffix_b)) if self.boot_a_size != self.boot_b_size: raise error.TestFail('boot partitions are not the same size') logging.info('boot partition size: %d bytes', self.boot_a_size) def fastboot_get_var(self, variable_name): """Gets a fastboot variable. Returns a string with the value or the empty string if the variable does not exist. Prerequisite: The DUT is in bootloader mode. @param variable_name: Name of fastboot variable to get. """ cmd_output = self.dut.fastboot_run('getvar %s' % variable_name) # Gah, 'fastboot getvar' prints requested output on stderr # instead of stdout as you'd expect. lines = cmd_output.stderr.split('\n') if lines[0].startswith(variable_name + ': '): return (lines[0])[len(variable_name + ': '):] return '' def check_fastboot_variables(self): """Checks that fastboot bootloader has necessary variables for A/B. Prerequisite: The DUT is in ADB mode. """ logging.info('Checking fastboot-compliant bootloader has necessary ' 'variables for A/B.') self.dut.ensure_bootloader_mode() # The slot-suffixes looks like '_a,_b' and may have a trailing comma. suffixes = self.fastboot_get_var('slot-suffixes') if suffixes.rstrip(',').split(',') != [self.suffix_a, self.suffix_b]: raise error.TestFail('Variable slot-suffixes has unexpected ' 'value "%s"' % suffixes) # Back to ADB mode. self.dut.fastboot_reboot() def get_current_slot(self): """Gets the current slot the DUT is running from. Prerequisite: The DUT is in ADB mode. """ return int(self.dut.run_output('bootctl get-current-slot')) def assert_current_slot(self, slot_number): """Checks that DUT is running from the given slot. Prerequisite: The DUT is in ADB mode. @param slot_number: Zero-based index of slot to be running from. """ _assert_equal(slot_number, self.get_current_slot()) def set_active_slot(self, slot_number): """Instructs the DUT to attempt booting from given slot. Prerequisite: The DUT is in ADB mode. @param slot_number: Zero-based index of slot to make active. """ logging.info('Setting slot %d active.', slot_number) self.dut.run('bootctl set-active-boot-slot %d' % slot_number) def ensure_running_slot(self, slot_number): """Ensures that DUT is running from the given slot. Prerequisite: The DUT is in ADB mode. @param slot_number: Zero-based index of slot to be running from. """ logging.info('Ensuring device is running from slot %d.', slot_number) if self.get_current_slot() != slot_number: logging.info('Rebooting into slot %d', slot_number) self.set_active_slot(slot_number) self.dut.reboot() self.assert_current_slot(slot_number) def copy_a_to_b(self): """Copies contents of slot A to slot B. Prerequisite: The DUT is in ADB mode and booted from slot A. """ self.assert_current_slot(0) for i in ['boot', 'system']: logging.info('Copying %s%s to %s%s.', i, self.suffix_a, i, self.suffix_b) self.dut.run('dd if=/dev/block/by-name/%s%s ' 'of=/dev/block/by-name/%s%s bs=4096' % (i, self.suffix_a, i, self.suffix_b)) def check_bootctl_set_active(self): """Checks that setActiveBootSlot in the boot_control HAL work. Prerequisite: The DUT is in ADB mode with populated A and B slots. """ logging.info('Check setActiveBootSlot() in boot_control HAL.') self.set_active_slot(0) self.dut.reboot() self.assert_current_slot(0) self.set_active_slot(1) self.dut.reboot() self.assert_current_slot(1) def check_fastboot_set_active(self): """Checks that 'fastboot set_active <SUFFIX>' work. Prerequisite: The DUT is in ADB mode with populated A and B slots. """ logging.info('Check set_active command in fastboot-compliant bootloader.') self.dut.ensure_bootloader_mode() self.dut.fastboot_run('set_active %s' % self.suffix_a) self.dut.fastboot_reboot() self.dut.adb_run('wait-for-device') self.assert_current_slot(0) self.dut.ensure_bootloader_mode() self.dut.fastboot_run('set_active %s' % self.suffix_b) self.dut.fastboot_reboot() self.dut.adb_run('wait-for-device') self.assert_current_slot(1) def check_bootloader_fallback_on_invalid(self): """Checks bootloader fallback if current slot is invalid. Prerequisite: The DUT is in ADB mode with populated A and B slots. """ logging.info('Checking bootloader fallback if current slot ' 'is invalid.') # Make sure we're in slot B, then zero out boot_b (so slot B # is invalid), reboot and check that the bootloader correctly # fell back to A. self.ensure_running_slot(1) self.dut.run('dd if=/dev/zero of=/dev/block/by-name/boot%s ' 'count=%d bs=4096' % (self.suffix_b, self.boot_b_size/4096)) self.dut.reboot() self.assert_current_slot(0) # Restore boot_b for use in future test cases. self.dut.run('dd if=/dev/block/by-name/boot%s ' 'of=/dev/block/by-name/boot%s bs=4096' % (self.suffix_a, self.suffix_b)) def check_bootloader_fallback_on_retries(self): """Checks bootloader fallback if slot made active runs out of tries. Prerequisite: The DUT is in ADB mode with populated A and B slots. @raises error.TestFail """ logging.info('Checking bootloader fallback if slot made active ' 'runs out of tries.') self.ensure_running_slot(0) self.set_active_slot(1) self.dut.reboot() num_retries = 1 while num_retries < 10 and self.get_current_slot() == 1: logging.info('Still with B after %d retries', num_retries) num_retries += 1 self.dut.reboot() if self.get_current_slot() != 0: raise error.TestFail('Bootloader did not fall back after ' '%d retries without the slot being marked ' 'as GOOD' % num_retries) logging.info('Fell back to A after %d retries.', num_retries) def check_bootloader_mark_successful(self): """Checks bootloader stays with slot after it has been marked good. Prerequisite: The DUT is in ADB mode with populated A and B slots. """ logging.info('Checking bootloader is staying with a slot after it has ' 'been marked as GOOD for at least 10 reboots.') self.ensure_running_slot(0) self.dut.run('bootctl mark-boot-successful') num_reboots = 0 while num_reboots < 10: self.dut.reboot() self.assert_current_slot(0) num_reboots += 1 logging.info('Still with A after %d reboots', num_reboots) def run_once(self, dut=None): """A/B tests for boot loader and boot_control HAL implementation. Verifies that boot loader and boot_control HAL implementation implements A/B correctly. Prerequisite: The DUT is in ADB mode. @param dut: host object representing the device under test. """ self.dut = dut self.get_slots_and_suffix() self.check_fastboot_variables() self.ensure_running_slot(0) self.copy_a_to_b() self.check_bootctl_set_active() self.check_fastboot_set_active() self.check_bootloader_fallback_on_invalid() self.check_bootloader_fallback_on_retries() self.check_bootloader_mark_successful()