| # Copyright (c) 2012 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. |
| """A module to support automatic firmware update. |
| |
| See FirmwareUpdater object below. |
| """ |
| import array |
| import json |
| import os |
| |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib.cros import chip_utils |
| from autotest_lib.client.common_lib.cros import cros_config |
| from autotest_lib.client.cros.faft.utils import flashrom_handler |
| |
| |
| class FirmwareUpdaterError(Exception): |
| """Error in the FirmwareUpdater module.""" |
| |
| |
| class FirmwareUpdater(object): |
| """An object to support firmware update. |
| |
| This object will create a temporary directory in /usr/local/tmp/faft/autest |
| with two subdirs, keys/ and work/. You can modify the keys in keys/ dir. If |
| you want to provide a given shellball to do firmware update, put shellball |
| under /usr/local/tmp/faft/autest with name chromeos-firmwareupdate. |
| |
| @type os_if: autotest_lib.client.cros.faft.utils.os_interface.OSInterface |
| """ |
| |
| DAEMON = 'update-engine' |
| CBFSTOOL = 'cbfstool' |
| HEXDUMP = 'hexdump -v -e \'1/1 "0x%02x\\n"\'' |
| |
| DEFAULT_SHELLBALL = '/usr/sbin/chromeos-firmwareupdate' |
| DEFAULT_SUBDIR = 'autest' # subdirectory of os_interface.state_dir |
| DEFAULT_SECTION_FOR_TARGET = {'bios': 'a', 'ec': 'rw'} |
| |
| CBFS_REGIONS_MAP = {'a': 'FW_MAIN_A', 'b': 'FW_MAIN_B'} |
| |
| def __init__(self, os_if): |
| """Initialize the updater tools, but don't load the image data yet.""" |
| self.os_if = os_if |
| self._temp_path = self.os_if.state_dir_file(self.DEFAULT_SUBDIR) |
| self._cbfs_work_path = os.path.join(self._temp_path, 'cbfs') |
| self._keys_path = os.path.join(self._temp_path, 'keys') |
| self._work_path = os.path.join(self._temp_path, 'work') |
| self._bios_path = 'bios.bin' |
| self._ec_path = 'ec.bin' |
| |
| self.pubkey_path = os.path.join(self._keys_path, 'root_key.vbpubk') |
| self._real_bios_handler = self._create_handler('bios') |
| self._real_ec_handler = self._create_handler('ec') |
| self.initialized = False |
| |
| def init(self): |
| """Extract the shellball and other files, unless they already exist.""" |
| |
| if self.os_if.is_dir(self._work_path): |
| # If work dir is present, assume the whole temp dir is usable as-is. |
| self._detect_image_paths() |
| else: |
| # If work dir is missing, assume the whole temp dir is unusable, and |
| # recreate it. |
| self._create_temp_dir() |
| self.extract_shellball() |
| |
| self.initialized = True |
| |
| def _get_handler(self, target): |
| """Return the handler for the target, after initializing it if needed. |
| |
| @param target: image type ('bios' or 'ec') |
| @return: the handler for that target |
| |
| @type target: str |
| @rtype: flashrom_handler.FlashromHandler |
| """ |
| if target == 'bios': |
| if not self._real_bios_handler.initialized: |
| bios_file = self._get_image_path('bios') |
| self._real_bios_handler.init(bios_file) |
| return self._real_bios_handler |
| elif target == 'ec': |
| if not self._real_ec_handler.initialized: |
| ec_file = self._get_image_path('ec') |
| self._real_ec_handler.init(ec_file, allow_fallback=True) |
| return self._real_ec_handler |
| else: |
| raise FirmwareUpdaterError("Unhandled target: %r" % target) |
| |
| def _create_handler(self, target, suffix=None): |
| """Return a new (not pre-populated) handler for the given target, |
| such as for use in checking installed versions. |
| |
| @param target: image type ('bios' or 'ec') |
| @param suffix: additional piece for subdirectory of handler |
| Example: 'tmp' -> 'autest/<target>.tmp/' |
| @return: a new handler for that target |
| |
| @type target: str |
| @rtype: flashrom_handler.FlashromHandler |
| """ |
| if suffix: |
| subdir = '%s/%s.%s' % (self.DEFAULT_SUBDIR, target, suffix) |
| else: |
| subdir = '%s/%s' % (self.DEFAULT_SUBDIR, target) |
| return flashrom_handler.FlashromHandler(self.os_if, |
| self.pubkey_path, |
| self._keys_path, |
| target=target, |
| subdir=subdir) |
| |
| def _get_image_path(self, target): |
| """Return the handler for the given target |
| |
| @param target: image type ('bios' or 'ec') |
| @return: the path of the image file for that target |
| |
| @type target: str |
| @rtype: str |
| """ |
| if target == 'bios': |
| return os.path.join(self._work_path, self._bios_path) |
| elif target == 'ec': |
| return os.path.join(self._work_path, self._ec_path) |
| else: |
| raise FirmwareUpdaterError("Unhandled target: %r" % target) |
| |
| def _get_default_section(self, target): |
| """Return the default section to work with, for the given target |
| |
| @param target: image type ('bios' or 'ec') |
| @return: the default section for that target |
| |
| @type target: str |
| @rtype: str |
| """ |
| if target in self.DEFAULT_SECTION_FOR_TARGET: |
| return self.DEFAULT_SECTION_FOR_TARGET[target] |
| else: |
| raise FirmwareUpdaterError("Unhandled target: %r" % target) |
| |
| def _create_temp_dir(self): |
| """Create (or recreate) the temporary directory. |
| |
| The default /usr/sbin/chromeos-firmwareupdate is copied into _temp_dir, |
| and devkeys are copied to _key_path. The caller is responsible for |
| extracting the copied shellball. |
| """ |
| self.cleanup_temp_dir() |
| |
| self.os_if.create_dir(self._temp_path) |
| self.os_if.create_dir(self._cbfs_work_path) |
| self.os_if.create_dir(self._work_path) |
| self.os_if.copy_dir('/usr/share/vboot/devkeys', self._keys_path) |
| |
| working_shellball = os.path.join(self._temp_path, |
| 'chromeos-firmwareupdate') |
| self.os_if.copy_file(self.DEFAULT_SHELLBALL, working_shellball) |
| |
| def cleanup_temp_dir(self): |
| """Cleanup temporary directory.""" |
| if self.os_if.is_dir(self._temp_path): |
| self.os_if.remove_dir(self._temp_path) |
| |
| def stop_daemon(self): |
| """Stop update-engine daemon.""" |
| self.os_if.log('Stopping %s...' % self.DAEMON) |
| cmd = 'status %s | grep stop || stop %s' % (self.DAEMON, self.DAEMON) |
| self.os_if.run_shell_command(cmd) |
| |
| def start_daemon(self): |
| """Start update-engine daemon.""" |
| self.os_if.log('Starting %s...' % self.DAEMON) |
| cmd = 'status %s | grep start || start %s' % (self.DAEMON, self.DAEMON) |
| self.os_if.run_shell_command(cmd) |
| |
| def get_ec_hash(self): |
| """Retrieve the hex string of the EC hash.""" |
| ec = self._get_handler('ec') |
| return ec.get_section_hash('rw') |
| |
| def get_section_fwid(self, target='bios', section=None): |
| """Get one fwid from in-memory image, for the given target. |
| |
| @param target: the image type to get from: 'bios (default) or 'ec' |
| @param section: section to return. Default: A for bios, RW for EC |
| |
| @type target: str | None |
| @rtype: str |
| """ |
| if section is None: |
| section = self._get_default_section(target) |
| image_path = self._get_image_path(target) |
| if target == 'ec' and not os.path.isfile(image_path): |
| # If the EC image is missing, report a specific error message. |
| raise FirmwareUpdaterError("Shellball does not contain ec.bin") |
| |
| handler = self._get_handler(target) |
| handler.new_image(image_path) |
| fwid = handler.get_section_fwid(section) |
| if fwid is not None: |
| return str(fwid) |
| else: |
| return None |
| |
| def get_device_fwids(self, target='bios'): |
| """Get all non-empty fwids from flash, for the given target. |
| |
| @param target: the image type to get from: 'bios' (default) or 'ec' |
| @return: fwid for the sections |
| |
| @type target: str |
| @type filename: str |
| @rtype: dict |
| """ |
| handler = self._create_handler(target, 'flashdevice') |
| handler.new_image() |
| |
| fwids = {} |
| for section in handler.fv_sections: |
| fwid = handler.get_section_fwid(section) |
| if fwid is not None: |
| fwids[section] = fwid |
| return fwids |
| |
| def get_image_fwids(self, target='bios', filename=None): |
| """Get all non-empty fwids from disk, for the given target. |
| |
| @param target: the image type to get from: 'bios' (default) or 'ec' |
| @param filename: filename to read instead of using the default shellball |
| @return: fwid for the sections |
| |
| @type target: str |
| @type filename: str |
| @rtype: dict |
| """ |
| if filename: |
| filename = os.path.join(self._temp_path, filename) |
| handler = self._create_handler(target, 'image') |
| handler.new_image(filename) |
| else: |
| filename = self._get_image_path(target) |
| handler = self._get_handler(target) |
| if target == 'ec' and not os.path.isfile(filename): |
| # If the EC image is missing, report a specific error message. |
| raise FirmwareUpdaterError("Shellball does not contain ec.bin") |
| |
| fwids = {} |
| for section in handler.fv_sections: |
| fwid = handler.get_section_fwid(section) |
| if fwid is not None: |
| fwids[section] = fwid |
| return fwids |
| |
| def modify_image_fwids(self, target='bios', sections=None): |
| """Modify the fwid in the image, but don't flash it. |
| |
| @param target: the image type to modify: 'bios' (default) or 'ec' |
| @param sections: section(s) to modify. Default: A for bios, RW for ec |
| @return: fwids for the modified sections, as {section: fwid} |
| |
| @type target: str |
| @type sections: tuple | list |
| @rtype: dict |
| """ |
| if sections is None: |
| sections = [self._get_default_section(target)] |
| |
| image_fullpath = self._get_image_path(target) |
| if target == 'ec' and not os.path.isfile(image_fullpath): |
| # If the EC image is missing, report a specific error message. |
| raise FirmwareUpdaterError("Shellball does not contain ec.bin") |
| |
| handler = self._get_handler(target) |
| fwids = handler.modify_fwids(sections) |
| |
| handler.dump_whole(image_fullpath) |
| handler.new_image(image_fullpath) |
| |
| return fwids |
| |
| def modify_ecid_and_flash_to_bios(self): |
| """Modify ecid, put it to AP firmware, and flash it to the system. |
| |
| This method is used for testing EC software sync for EC EFS (Early |
| Firmware Selection). It creates a slightly different EC RW image |
| (a different EC fwid) in AP firmware, in order to trigger EC |
| software sync on the next boot (a different hash with the original |
| EC RW). |
| |
| The steps of this method: |
| * Modify the EC fwid by appending a '~', like from |
| 'fizz_v1.1.7374-147f1bd64' to 'fizz_v1.1.7374-147f1bd64~'. |
| * Resign the EC image. |
| * Store the modififed EC RW image to CBFS component 'ecrw' of the |
| AP firmware's FW_MAIN_A and FW_MAIN_B, and also the new hash. |
| * Resign the AP image. |
| * Flash the modified AP image back to the system. |
| """ |
| self.cbfs_setup_work_dir() |
| |
| fwid = self.get_section_fwid('ec', 'rw') |
| if fwid.endswith('~'): |
| raise FirmwareUpdaterError('The EC fwid is already modified') |
| |
| # Modify the EC FWID and resign |
| fwid = fwid[:-1] + '~' |
| ec = self._get_handler('ec') |
| ec.set_section_fwid('rw', fwid) |
| ec.resign_ec_rwsig() |
| |
| # Replace ecrw to the new one |
| ecrw_bin_path = os.path.join(self._cbfs_work_path, |
| chip_utils.ecrw.cbfs_bin_name) |
| ec.dump_section_body('rw', ecrw_bin_path) |
| |
| # Replace ecrw.hash to the new one |
| ecrw_hash_path = os.path.join(self._cbfs_work_path, |
| chip_utils.ecrw.cbfs_hash_name) |
| with open(ecrw_hash_path, 'wb') as f: |
| f.write(self.get_ec_hash()) |
| |
| # Store the modified ecrw and its hash to cbfs |
| self.cbfs_replace_chip(chip_utils.ecrw.fw_name, extension='') |
| |
| # Resign and flash the AP firmware back to the system |
| self.cbfs_sign_and_flash() |
| |
| def corrupt_diagnostics_image(self, local_path): |
| """Corrupts a diagnostics image in the CBFS working directory. |
| |
| @param local_path: Filename for storing the diagnostics image in the |
| CBFS working directory |
| """ |
| |
| # Invert the last few bytes of the image. Note that cbfstool will |
| # silently ignore bytes added after the end of the ELF, and it will |
| # refuse to use an ELF with noticeably corrupted headers as a payload. |
| num_bytes = 4 |
| with open(local_path, 'rb+') as image: |
| image.seek(-num_bytes, os.SEEK_END) |
| last_bytes = array.array('B') |
| last_bytes.fromfile(image, num_bytes) |
| |
| for i in range(len(last_bytes)): |
| last_bytes[i] = last_bytes[i] ^ 0xff |
| |
| image.seek(-num_bytes, os.SEEK_END) |
| last_bytes.tofile(image) |
| |
| def resign_firmware(self, version=None, work_path=None): |
| """Resign firmware with version. |
| |
| Args: |
| version: new firmware version number, default to no modification. |
| work_path: work path, default to the updater work path. |
| """ |
| if work_path is None: |
| work_path = self._work_path |
| self.os_if.run_shell_command( |
| '/usr/share/vboot/bin/resign_firmwarefd.sh ' |
| '%s %s %s %s %s %s %s %s' % |
| (os.path.join(work_path, self._bios_path), |
| os.path.join(self._temp_path, 'output.bin'), |
| os.path.join(self._keys_path, 'firmware_data_key.vbprivk'), |
| os.path.join(self._keys_path, 'firmware.keyblock'), |
| os.path.join(self._keys_path, |
| 'dev_firmware_data_key.vbprivk'), |
| os.path.join(self._keys_path, 'dev_firmware.keyblock'), |
| os.path.join(self._keys_path, 'kernel_subkey.vbpubk'), |
| ('%d' % version) if version is not None else '')) |
| self.os_if.copy_file( |
| '%s' % os.path.join(self._temp_path, 'output.bin'), |
| '%s' % os.path.join(work_path, self._bios_path)) |
| |
| def _read_manifest(self, shellball=None): |
| """This gets the manifest from the shellball or the extracted directory. |
| |
| @param shellball: Path of the shellball to read from (via --manifest). |
| If None (default), read from extracted manifest.json. |
| @return: the manifest information, or None |
| |
| @type shellball: str | None |
| @rtype: dict |
| """ |
| |
| if shellball: |
| output = self.os_if.run_shell_command_get_output( |
| 'sh %s --manifest' % shellball) |
| manifest_text = '\n'.join(output or []) |
| else: |
| manifest_file = os.path.join(self._work_path, 'manifest.json') |
| manifest_text = self.os_if.read_file(manifest_file) |
| |
| if manifest_text: |
| return json.loads(manifest_text) |
| else: |
| return None |
| |
| def _detect_image_paths(self, shellball=None): |
| """Scans shellball manifest to find correct bios and ec image paths. |
| |
| @param shellball: Path of the shellball to read from (via --manifest). |
| If None (default), read from extracted manifest.json. |
| @type shellball: str | None |
| """ |
| model_name = cros_config.call_cros_config_get_output( |
| '/ name', self.os_if.run_shell_command_get_result) |
| |
| if not model_name: |
| return |
| |
| manifest = self._read_manifest(shellball) |
| |
| if manifest: |
| model_info = manifest.get(model_name) |
| if model_info: |
| |
| try: |
| self._bios_path = model_info['host']['image'] |
| except KeyError: |
| pass |
| |
| try: |
| self._ec_path = model_info['ec']['image'] |
| except KeyError: |
| pass |
| |
| def extract_shellball(self, append=None): |
| """Extract the working shellball. |
| |
| Args: |
| append: decide which shellball to use with format |
| chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate' |
| if append is None. |
| Returns: |
| string: the full path of the shellball |
| """ |
| working_shellball = os.path.join(self._temp_path, |
| 'chromeos-firmwareupdate') |
| if append: |
| working_shellball = working_shellball + '-%s' % append |
| |
| self.os_if.run_shell_command('sh %s --unpack %s' % |
| (working_shellball, self._work_path)) |
| |
| # use the json file that was extracted, to catch extraction problems. |
| self._detect_image_paths() |
| return working_shellball |
| |
| def repack_shellball(self, append=None): |
| """Repack shellball with new fwid. |
| |
| New fwid follows the rule: [orignal_fwid]-[append]. |
| |
| Args: |
| append: save the new shellball with a suffix, for example, |
| chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate' |
| if append is None. |
| Returns: |
| string: The full path to the shellball |
| """ |
| |
| working_shellball = os.path.join(self._temp_path, |
| 'chromeos-firmwareupdate') |
| if append: |
| new_shellball = working_shellball + '-%s' % append |
| self.os_if.copy_file(working_shellball, new_shellball) |
| working_shellball = new_shellball |
| |
| self.os_if.run_shell_command('sh %s --repack %s' % |
| (working_shellball, self._work_path)) |
| |
| # use the shellball that was repacked, to catch repacking problems. |
| self._detect_image_paths(working_shellball) |
| return working_shellball |
| |
| def reset_shellball(self): |
| """Extract shellball, then revert the AP and EC handlers' data.""" |
| self._create_temp_dir() |
| self.extract_shellball() |
| self.reload_images() |
| |
| def reload_images(self): |
| """Reload handlers from the on-disk images, in case they've changed.""" |
| bios_file = os.path.join(self._work_path, self._bios_path) |
| self._real_bios_handler.deinit() |
| self._real_bios_handler.init(bios_file) |
| if self._real_ec_handler.is_available(): |
| ec_file = os.path.join(self._work_path, self._ec_path) |
| self._real_ec_handler.deinit() |
| self._real_ec_handler.init(ec_file, allow_fallback=True) |
| |
| def get_firmwareupdate_command(self, mode, append=None, options=None): |
| """Get the command to run firmwareupdate with updater in temp_dir. |
| |
| @param append: decide which shellball to use with format |
| chromeos-firmwareupdate-[append]. |
| Use'chromeos-firmwareupdate' if append is None. |
| @param mode: ex.'autoupdate', 'recovery', 'bootok', 'factory_install'... |
| @param options: ex. ['--noupdate_ec', '--force'] or [] or None. |
| |
| @type append: str |
| @type mode: str |
| @type options: list | tuple | None |
| """ |
| if mode == 'bootok': |
| # Since CL:459837, bootok is moved to chromeos-setgoodfirmware. |
| set_good_cmd = '/usr/sbin/chromeos-setgoodfirmware' |
| if os.path.isfile(set_good_cmd): |
| return set_good_cmd |
| |
| updater = os.path.join(self._temp_path, 'chromeos-firmwareupdate') |
| if append: |
| updater = '%s-%s' % (updater, append) |
| |
| if options is None: |
| options = [] |
| if isinstance(options, tuple): |
| options = list(options) |
| |
| def _has_emulate(option): |
| return option == '--emulate' or option.startswith('--emulate=') |
| |
| if self.os_if.test_mode and not filter(_has_emulate, options): |
| # if in test mode, forcibly use --emulate, if not already used. |
| fake_bios = os.path.join(self._temp_path, 'rpc-test-fake-bios.bin') |
| if not os.path.exists(fake_bios): |
| bios_reader = self._create_handler('bios', 'tmp') |
| bios_reader.dump_flash(fake_bios) |
| options = ['--emulate', fake_bios] + options |
| |
| return '/bin/sh %s --mode %s %s' % (updater, mode, ' '.join(options)) |
| |
| def run_firmwareupdate(self, mode, append=None, options=None): |
| """Do firmwareupdate with updater in temp_dir. |
| |
| @param append: decide which shellball to use with format |
| chromeos-firmwareupdate-[append]. |
| Use'chromeos-firmwareupdate' if append is None. |
| @param mode: ex.'autoupdate', 'recovery', 'bootok', 'factory_install'... |
| @param options: ex. ['--noupdate_ec', '--force'] or [] or None. |
| |
| @type append: str |
| @type mode: str |
| @type options: list | tuple | None |
| """ |
| return self.os_if.run_shell_command_get_status( |
| self.get_firmwareupdate_command(mode, append, options)) |
| |
| def cbfs_setup_work_dir(self): |
| """Sets up cbfs on DUT. |
| |
| Finds bios.bin on the DUT and sets up a temp dir to operate on |
| bios.bin. If a bios.bin was specified, it is copied to the DUT |
| and used instead of the native bios.bin. |
| |
| @return: The cbfs work directory path. |
| """ |
| self.os_if.remove_dir(self._cbfs_work_path) |
| self.os_if.copy_dir(self._work_path, self._cbfs_work_path) |
| |
| return self._cbfs_work_path |
| |
| @classmethod |
| def _cbfs_regions(cls, sections): |
| """Map from ['A', 'B'] to ['FW_MAIN_A', 'FW_MAIN_B']""" |
| regions = set() |
| for section in sections: |
| region = cls.CBFS_REGIONS_MAP.get(section.lower(), section) |
| regions.add(region) |
| return sorted(regions) |
| |
| def cbfs_expand(self, regions): |
| """Expand the CBFS to fill available space |
| |
| @param regions: string, such as FW_MAIN_A,FW_MAIN_B |
| """ |
| bios = os.path.join(self._cbfs_work_path, self._bios_path) |
| expand_cmd = '%s %s expand -r %s' % (self.CBFSTOOL, bios, |
| ','.join(regions)) |
| self.os_if.run_shell_command(expand_cmd) |
| return True |
| |
| def cbfs_truncate(self, regions): |
| """Truncate the CBFS to fill minimum space |
| |
| @param regions: string, such as FW_MAIN_A,FW_MAIN_B |
| """ |
| bios = os.path.join(self._cbfs_work_path, self._bios_path) |
| truncate_cmd = '%s %s truncate -r %s' % (self.CBFSTOOL, bios, |
| ','.join(regions)) |
| self.os_if.run_shell_command(truncate_cmd) |
| return True |
| |
| def cbfs_extract(self, |
| filename, |
| extension, |
| regions=('a', ), |
| local_filename=None, |
| arch=None): |
| """Extracts an arbitrary file from cbfs. |
| |
| Note that extracting from |
| @param filename: Filename in cbfs, including extension |
| @param extension: Extension of the file, including '.' |
| @param regions: Tuple of regions (the default is just 'a') |
| @param arch: Specific machine architecture to extract (default unset) |
| @param local_filename: Path to use on the DUT, overriding the default in |
| the cbfs work dir. |
| @return: The full path of the extracted file, or None |
| """ |
| regions = self._cbfs_regions(regions) |
| bios = os.path.join(self._cbfs_work_path, self._bios_path) |
| |
| cbfs_filename = filename + extension |
| if local_filename is None: |
| local_filename = os.path.join(self._cbfs_work_path, |
| filename + extension) |
| |
| extract_cmd = ('%s %s extract -r %s -n %s%s -f %s' % |
| (self.CBFSTOOL, bios, ','.join(regions), filename, |
| extension, local_filename)) |
| if arch: |
| extract_cmd += ' -m %s' % arch |
| try: |
| self.os_if.run_shell_command(extract_cmd) |
| if not self.os_if.path_exists(local_filename): |
| self.os_if.log("Warning: file does not exist after extracting:" |
| " %s" % local_filename) |
| return os.path.abspath(local_filename) |
| except error.CmdError: |
| # already logged by run_shell_command() |
| return None |
| |
| def cbfs_extract_chip(self, |
| fw_name, |
| extension='.bin', |
| hash_extension='.bash', |
| regions=('a', )): |
| """Extracts chip firmware blob from cbfs. |
| |
| For a given chip type, looks for the corresponding firmware |
| blob and hash in the specified bios. The firmware blob and |
| hash are extracted into self._cbfs_work_path. |
| |
| The extracted blobs will be <fw_name><extension> and |
| <fw_name>.hash located in cbfs_work_path. |
| |
| @param fw_name: Chip firmware name to be extracted. |
| @param extension: File extension of the cbfs file, including '.' |
| @param hash_extension: File extension of the hash file, including '.' |
| @return: dict of {'image': image_fullpath, 'hash': hash_fullpath}, |
| """ |
| regions = self._cbfs_regions(regions) |
| |
| results = {} |
| |
| if extension is not None: |
| image_path = self.cbfs_extract(fw_name, extension, regions) |
| if image_path: |
| results['image'] = image_path |
| |
| if hash_extension is not None and hash_extension != extension: |
| hash_path = self.cbfs_extract(fw_name, hash_extension, regions) |
| if hash_path: |
| results['hash'] = hash_path |
| |
| return results |
| |
| def cbfs_extract_diagnostics(self, diag_name, local_path): |
| """Runs cbfstool to extract a diagnostics image. |
| |
| @param diag_name: Name of the diagnostics image in CBFS |
| @param local_path: Filename for storing the diagnostics image in the |
| CBFS working directory |
| """ |
| return self.cbfs_extract(diag_name, |
| '', ['RW_LEGACY'], |
| local_path, |
| arch='x86') |
| |
| def cbfs_get_chip_hash(self, fw_name, hash_extension='.hash'): |
| """Returns chip firmware hash blob. |
| |
| For a given chip type, returns the chip firmware hash blob. |
| Before making this request, the chip blobs must have been |
| extracted from cbfs using cbfs_extract_chip(). |
| The hash data is returned as a list of stringified two-byte pieces: |
| \x12\x34...\xab\xcd\xef -> ['0x12', '0x34', ..., '0xab', '0xcd', '0xef'] |
| |
| @param fw_name: Chip firmware name whose hash blob to get. |
| @return: Boolean success status. |
| @raise error.CmdError: Underlying remote shell operations failed. |
| """ |
| fw_path = os.path.join(self._cbfs_work_path, fw_name) |
| hexdump_cmd = '%s %s%s' % (self.HEXDUMP, fw_path, hash_extension) |
| hashblob = self.os_if.run_shell_command_get_output(hexdump_cmd) |
| return hashblob |
| |
| def cbfs_remove(self, filename, extension, regions=('a', 'b')): |
| """Remove the given binary from CBFS, in FW_MAIN_A/FW_MAIN_B |
| |
| @param filename: Name within cbfs of the file, without extension |
| @param extension: Extension of the name of the cbfs component. |
| @param regions: tuple of regions to act on (full name, or 'A' or 'B') |
| @return: Boolean success status. |
| @raise error.CmdError: If underlying remote shell operations failed. |
| """ |
| regions = self._cbfs_regions(regions) |
| |
| bios = os.path.join(self._cbfs_work_path, self._bios_path) |
| rm_cmd = '%s %s remove -r %s -n %s%s' % ( |
| self.CBFSTOOL, bios, ','.join(regions), filename, extension) |
| |
| self.os_if.run_shell_command(rm_cmd) |
| return True |
| |
| def cbfs_add(self, |
| filename, |
| extension, |
| regions=('a', 'b'), |
| local_filename=None): |
| """Add the given binary to CBFS, in the specified regions |
| |
| If extension is .hash, the compression is assumed to be none. |
| For any other extension, it's assumed to be lzma. |
| |
| @param filename: Name within cbfs of the file, without extension |
| @param extension: Extension of the name of the cbfs component. |
| @param regions: tuple of regions to act on (full name, or 'A' or 'B') |
| @param local_filename |
| @return: Boolean success status. |
| @raise error.CmdError: If underlying remote shell operations failed. |
| """ |
| regions = self._cbfs_regions(regions) |
| |
| if extension == '.hash': |
| compression = 'none' |
| else: |
| compression = 'lzma' |
| |
| if local_filename is None: |
| local_filename = os.path.join(self._cbfs_work_path, |
| filename + extension) |
| |
| bios = os.path.join(self._cbfs_work_path, self._bios_path) |
| add_cmd = '%s %s add -r %s -t raw -c %s -n %s%s -f %s' % ( |
| self.CBFSTOOL, bios, ','.join(regions), compression, filename, |
| extension, local_filename) |
| |
| self.os_if.run_shell_command(add_cmd) |
| return True |
| |
| def cbfs_replace_chip(self, |
| fw_name, |
| extension='.bin', |
| hash_extension='.hash', |
| regions=('a', 'b')): |
| """Replaces chip firmware and its hash in CBFS (bios.bin). |
| |
| For a given chip type, replaces its firmware blob and hash in |
| bios.bin. All files referenced are expected to be in the |
| directory set up using cbfs_setup_work_dir(). |
| |
| @param cbfs_filename: Name within cbfs of the file, without extension |
| @param extension: Extension of the name of the cbfs component. |
| @param regions: tuple of regions to act on (full name, or 'A' or 'B') |
| @return: Boolean success status. |
| @raise error.CmdError: If underlying remote shell operations failed. |
| """ |
| regions = self._cbfs_regions(regions) |
| self.cbfs_expand(regions) |
| if hash_extension is not None and hash_extension != extension: |
| self.cbfs_remove(fw_name, hash_extension, regions) |
| self.cbfs_remove(fw_name, extension, regions) |
| if hash_extension is not None and hash_extension != extension: |
| self.cbfs_add(fw_name, hash_extension, regions) |
| self.cbfs_add(fw_name, extension, regions) |
| self.cbfs_truncate(regions) |
| return True |
| |
| def cbfs_replace_diagnostics(self, diag_name, local_path): |
| """Runs cbfstool to replace a diagnostics image in the firmware image. |
| |
| @param diag_name: Name of the diagnostics image in CBFS |
| @param local_path: Filename for storing the diagnostics image in the |
| CBFS working directory |
| """ |
| regions = ['RW_LEGACY'] |
| self.cbfs_expand(regions) |
| self.cbfs_remove(diag_name, '', regions) |
| self.cbfs_add(diag_name, '', regions, local_path) |
| self.cbfs_truncate(regions) |
| |
| def cbfs_sign_and_flash(self): |
| """Signs CBFS (bios.bin) and flashes it.""" |
| self.resign_firmware(work_path=self._cbfs_work_path) |
| bios = self._get_handler('bios') |
| bios_file = os.path.join(self._cbfs_work_path, self._bios_path) |
| bios.new_image(bios_file) |
| # futility makes sure to preserve important sections (HWID, GBB, VPD). |
| self.os_if.run_shell_command_get_result( |
| 'futility update --mode=recovery -i %s' % bios_file) |
| return True |
| |
| def copy_bios(self, filename): |
| """Copy the shellball BIOS to the given name in the temp dir |
| |
| @param filename: the filename to use for the copy |
| @return: the full path of the BIOS |
| |
| @type filename: str |
| @rtype: str |
| """ |
| if not isinstance(filename, basestring): |
| raise FirmwareUpdaterError("Filename must be a string: %s" % |
| repr(filename)) |
| src_bios = os.path.join(self._work_path, self._bios_path) |
| dst_bios = os.path.join(self._temp_path, filename) |
| self.os_if.copy_file(src_bios, dst_bios) |
| return dst_bios |
| |
| def get_temp_path(self): |
| """Get temp directory path.""" |
| return self._temp_path |
| |
| def get_keys_path(self): |
| """Get keys directory path.""" |
| return self._keys_path |
| |
| def get_work_path(self): |
| """Get work directory path.""" |
| return self._work_path |
| |
| def get_bios_relative_path(self): |
| """Gets the relative path of the bios image in the shellball.""" |
| return self._bios_path |
| |
| def get_ec_relative_path(self): |
| """Gets the relative path of the ec image in the shellball.""" |
| return self._ec_path |
| |
| def get_image_gbb_flags(self, filename=None): |
| """Get the GBB flags in the given image (shellball image if unspecified) |
| |
| @param filename: the image path to act on (None to use shellball image) |
| @return: An integer of the GBB flags. |
| """ |
| if filename: |
| filename = os.path.join(self._temp_path, filename) |
| handler = self._create_handler('bios', 'image') |
| handler.new_image(filename) |
| else: |
| handler = self._get_handler('bios') |
| return handler.get_gbb_flags() |
| |
| def set_image_gbb_flags(self, flags, filename=None): |
| """Set the GBB flags in the given image (shellball image if unspecified) |
| |
| @param flags: the flags to set |
| @param filename: the image path to act on (None to use shellball image) |
| |
| @type flags: int |
| @type filename: str | None |
| """ |
| if filename: |
| filename = os.path.join(self._temp_path, filename) |
| handler = self._create_handler('bios', 'image') |
| handler.new_image(filename) |
| else: |
| filename = self._get_image_path('bios') |
| handler = self._get_handler('bios') |
| handler.set_gbb_flags(flags) |
| handler.dump_whole(filename) |