Home | History | Annotate | Download | only in utils
      1 # Copyright (c) 2012 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 """A module to support automatic firmware update.
      6 
      7 See FirmwareUpdater object below.
      8 """
      9 
     10 import os
     11 import re
     12 
     13 from autotest_lib.client.common_lib.cros import chip_utils
     14 from autotest_lib.client.cros.faft.utils import (common,
     15                                                  flashrom_handler,
     16                                                  saft_flashrom_util,
     17                                                  shell_wrapper)
     18 
     19 
     20 class FirmwareUpdaterError(Exception):
     21     """Error in the FirmwareUpdater module."""
     22 
     23 
     24 class FirmwareUpdater(object):
     25     """An object to support firmware update.
     26 
     27     This object will create a temporary directory in /var/tmp/faft/autest with
     28     two subdirectory keys/ and work/. You can modify the keys in keys/
     29     directory. If you want to provide a given shellball to do firmware update,
     30     put shellball under /var/tmp/faft/autest with name chromeos-firmwareupdate.
     31     """
     32 
     33     DAEMON = 'update-engine'
     34     CBFSTOOL = 'cbfstool'
     35     HEXDUMP = 'hexdump -v -e \'1/1 "0x%02x\\n"\''
     36 
     37     def __init__(self, os_if):
     38         self.os_if = os_if
     39         self._temp_path = '/var/tmp/faft/autest'
     40         self._cbfs_work_path = os.path.join(self._temp_path, 'cbfs')
     41         self._keys_path = os.path.join(self._temp_path, 'keys')
     42         self._work_path = os.path.join(self._temp_path, 'work')
     43         self._bios_path = 'bios.bin'
     44         self._ec_path = 'ec.bin'
     45         pubkey_path = os.path.join(self._keys_path, 'root_key.vbpubk')
     46         self._bios_handler = common.LazyInitHandlerProxy(
     47                 flashrom_handler.FlashromHandler,
     48                 saft_flashrom_util,
     49                 os_if,
     50                 pubkey_path,
     51                 self._keys_path,
     52                 'bios')
     53         self._ec_handler = common.LazyInitHandlerProxy(
     54                 flashrom_handler.FlashromHandler,
     55                 saft_flashrom_util,
     56                 os_if,
     57                 pubkey_path,
     58                 self._keys_path,
     59                 'ec')
     60 
     61         # _detect_image_paths always needs to run during initialization
     62         # or after extract_shellball is called.
     63         #
     64         # If we are setting up the temp dir from scratch, we'll transitively
     65         # call _detect_image_paths since extract_shellball is called.
     66         # Otherwise, we need to scan the existing temp directory.
     67         if not self.os_if.is_dir(self._temp_path):
     68             self._setup_temp_dir()
     69         else:
     70             self._detect_image_paths()
     71 
     72     def _setup_temp_dir(self):
     73         """Setup temporary directory.
     74 
     75         Devkeys are copied to _key_path. Then, shellball (default:
     76         /usr/sbin/chromeos-firmwareupdate) is extracted to _work_path.
     77         """
     78         self.cleanup_temp_dir()
     79 
     80         self.os_if.create_dir(self._temp_path)
     81         self.os_if.create_dir(self._cbfs_work_path)
     82         self.os_if.create_dir(self._work_path)
     83         self.os_if.copy_dir('/usr/share/vboot/devkeys', self._keys_path)
     84 
     85         original_shellball = '/usr/sbin/chromeos-firmwareupdate'
     86         working_shellball = os.path.join(self._temp_path,
     87                                          'chromeos-firmwareupdate')
     88         self.os_if.copy_file(original_shellball, working_shellball)
     89         self.extract_shellball()
     90 
     91     def cleanup_temp_dir(self):
     92         """Cleanup temporary directory."""
     93         if self.os_if.is_dir(self._temp_path):
     94             self.os_if.remove_dir(self._temp_path)
     95 
     96     def stop_daemon(self):
     97         """Stop update-engine daemon."""
     98         self.os_if.log('Stopping %s...' % self.DAEMON)
     99         cmd = 'status %s | grep stop || stop %s' % (self.DAEMON, self.DAEMON)
    100         self.os_if.run_shell_command(cmd)
    101 
    102     def start_daemon(self):
    103         """Start update-engine daemon."""
    104         self.os_if.log('Starting %s...' % self.DAEMON)
    105         cmd = 'status %s | grep start || start %s' % (self.DAEMON, self.DAEMON)
    106         self.os_if.run_shell_command(cmd)
    107 
    108     def retrieve_fwid(self):
    109         """Retrieve shellball's fwid.
    110 
    111         This method should be called after _setup_temp_dir.
    112 
    113         Returns:
    114             Shellball's fwid.
    115         """
    116         self._bios_handler.new_image(
    117                 os.path.join(self._work_path, self._bios_path))
    118         fwid = self._bios_handler.get_section_fwid('a')
    119         # Remove the tailing null characters
    120         return fwid.rstrip('\0')
    121 
    122     def retrieve_ecid(self):
    123         """Retrieve shellball's ecid.
    124 
    125         This method should be called after _setup_temp_dir.
    126 
    127         Returns:
    128             Shellball's ecid.
    129         """
    130         self._ec_handler.new_image(
    131                 os.path.join(self._work_path, self._ec_path))
    132         fwid = self._ec_handler.get_section_fwid('rw')
    133         # Remove the tailing null characters
    134         return fwid.rstrip('\0')
    135 
    136     def retrieve_ec_hash(self):
    137         """Retrieve the hex string of the EC hash."""
    138         return self._ec_handler.get_section_hash('rw')
    139 
    140     def modify_ecid_and_flash_to_bios(self):
    141         """Modify ecid, put it to AP firmware, and flash it to the system.
    142 
    143         This method is used for testing EC software sync for EC EFS (Early
    144         Firmware Selection). It creates a slightly different EC RW image
    145         (a different EC fwid) in AP firmware, in order to trigger EC
    146         software sync on the next boot (a different hash with the original
    147         EC RW).
    148 
    149         The steps of this method:
    150          * Modify the EC fwid by appending a '~', like from
    151            'fizz_v1.1.7374-147f1bd64' to 'fizz_v1.1.7374-147f1bd64~'.
    152          * Resign the EC image.
    153          * Store the modififed EC RW image to CBFS component 'ecrw' of the
    154            AP firmware's FW_MAIN_A and FW_MAIN_B, and also the new hash.
    155          * Resign the AP image.
    156          * Flash the modified AP image back to the system.
    157         """
    158         self.cbfs_setup_work_dir()
    159 
    160         fwid = self.retrieve_ecid()
    161         if fwid.endswith('~'):
    162             raise FirmwareUpdaterError('The EC fwid is already modified')
    163 
    164         # Modify the EC FWID and resign
    165         fwid = fwid[:-1] + '~'
    166         self._ec_handler.set_section_fwid('rw', fwid)
    167         self._ec_handler.resign_ec_rwsig()
    168 
    169         # Replace ecrw to the new one
    170         ecrw_bin_path = os.path.join(self._cbfs_work_path,
    171                                      chip_utils.ecrw.cbfs_bin_name)
    172         self._ec_handler.dump_section_body('rw', ecrw_bin_path)
    173 
    174         # Replace ecrw.hash to the new one
    175         ecrw_hash_path = os.path.join(self._cbfs_work_path,
    176                                       chip_utils.ecrw.cbfs_hash_name)
    177         with open(ecrw_hash_path, 'w') as f:
    178             f.write(self.retrieve_ec_hash())
    179 
    180         # Store the modified ecrw and its hash to cbfs
    181         self.cbfs_replace_chip(chip_utils.ecrw.fw_name, extension='')
    182 
    183         # Resign and flash the AP firmware back to the system
    184         self.cbfs_sign_and_flash()
    185 
    186     def resign_firmware(self, version=None, work_path=None):
    187         """Resign firmware with version.
    188 
    189         Args:
    190             version: new firmware version number, default to no modification.
    191             work_path: work path, default to the updater work path.
    192         """
    193         if work_path is None:
    194             work_path = self._work_path
    195         self.os_if.run_shell_command(
    196                 '/usr/share/vboot/bin/resign_firmwarefd.sh '
    197                 '%s %s %s %s %s %s %s %s' % (
    198                     os.path.join(work_path, self._bios_path),
    199                     os.path.join(self._temp_path, 'output.bin'),
    200                     os.path.join(self._keys_path, 'firmware_data_key.vbprivk'),
    201                     os.path.join(self._keys_path, 'firmware.keyblock'),
    202                     os.path.join(self._keys_path,
    203                                  'dev_firmware_data_key.vbprivk'),
    204                     os.path.join(self._keys_path, 'dev_firmware.keyblock'),
    205                     os.path.join(self._keys_path, 'kernel_subkey.vbpubk'),
    206                     ('%d' % version) if version is not None else ''))
    207         self.os_if.copy_file('%s' % os.path.join(self._temp_path, 'output.bin'),
    208                              '%s' % os.path.join(
    209                                  work_path, self._bios_path))
    210 
    211     def _detect_image_paths(self):
    212         """Scans shellball to find correct bios and ec image paths."""
    213         model_result = self.os_if.run_shell_command_get_output(
    214             'mosys platform model')
    215         if model_result:
    216             model = model_result[0]
    217             search_path = os.path.join(
    218                 self._work_path, 'models', model, 'setvars.sh')
    219             grep_result = self.os_if.run_shell_command_get_output(
    220                 'grep IMAGE_MAIN= %s' % search_path)
    221             if grep_result:
    222                 match = re.match('IMAGE_MAIN=(.*)', grep_result[0])
    223                 if match:
    224                     self._bios_path = match.group(1).replace('"', '')
    225             grep_result = self.os_if.run_shell_command_get_output(
    226                 'grep IMAGE_EC= %s' % search_path)
    227             if grep_result:
    228                 match = re.match('IMAGE_EC=(.*)', grep_result[0])
    229                 if match:
    230                   self._ec_path = match.group(1).replace('"', '')
    231 
    232     def _update_target_fwid(self):
    233         """Update target fwid/ecid in the setvars.sh."""
    234         model_result = self.os_if.run_shell_command_get_output(
    235             'mosys platform model')
    236         if model_result:
    237             model = model_result[0]
    238             setvars_path = os.path.join(
    239                 self._work_path, 'models', model, 'setvars.sh')
    240             if self.os_if.path_exists(setvars_path):
    241                 fwid = self.retrieve_fwid()
    242                 ecid = self.retrieve_ecid()
    243                 args = ['-i']
    244                 args.append(
    245                     '"s/TARGET_FWID=\\".*\\"/TARGET_FWID=\\"%s\\"/g"'
    246                     % fwid)
    247                 args.append(setvars_path)
    248                 cmd = 'sed %s' % ' '.join(args)
    249                 self.os_if.run_shell_command(cmd)
    250 
    251                 args = ['-i']
    252                 args.append(
    253                     '"s/TARGET_RO_FWID=\\".*\\"/TARGET_RO_FWID=\\"%s\\"/g"'
    254                     % fwid)
    255                 args.append(setvars_path)
    256                 cmd = 'sed %s' % ' '.join(args)
    257                 self.os_if.run_shell_command(cmd)
    258 
    259                 args = ['-i']
    260                 args.append(
    261                     '"s/TARGET_ECID=\\".*\\"/TARGET_ECID=\\"%s\\"/g"'
    262                     % ecid)
    263                 args.append(setvars_path)
    264                 cmd = 'sed %s' % ' '.join(args)
    265                 self.os_if.run_shell_command(cmd)
    266 
    267     def extract_shellball(self, append=None):
    268         """Extract the working shellball.
    269 
    270         Args:
    271             append: decide which shellball to use with format
    272                 chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate'
    273                 if append is None.
    274         """
    275         working_shellball = os.path.join(self._temp_path,
    276                                          'chromeos-firmwareupdate')
    277         if append:
    278             working_shellball = working_shellball + '-%s' % append
    279 
    280         self.os_if.run_shell_command('sh %s --sb_extract %s' % (
    281                 working_shellball, self._work_path))
    282 
    283         self._detect_image_paths()
    284 
    285     def repack_shellball(self, append=None):
    286         """Repack shellball with new fwid.
    287 
    288         New fwid follows the rule: [orignal_fwid]-[append].
    289 
    290         Args:
    291             append: save the new shellball with a suffix, for example,
    292                 chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate'
    293                 if append is None.
    294         """
    295         self._update_target_fwid();
    296 
    297         working_shellball = os.path.join(self._temp_path,
    298                                          'chromeos-firmwareupdate')
    299         if append:
    300             self.os_if.copy_file(working_shellball,
    301                                  working_shellball + '-%s' % append)
    302             working_shellball = working_shellball + '-%s' % append
    303 
    304         self.os_if.run_shell_command('sh %s --sb_repack %s' % (
    305                 working_shellball, self._work_path))
    306 
    307         if append:
    308             args = ['-i']
    309             args.append(
    310                     '"s/TARGET_FWID=\\"\\(.*\\)\\"/TARGET_FWID=\\"\\1.%s\\"/g"'
    311                     % append)
    312             args.append(working_shellball)
    313             cmd = 'sed %s' % ' '.join(args)
    314             self.os_if.run_shell_command(cmd)
    315 
    316             args = ['-i']
    317             args.append('"s/TARGET_UNSTABLE=\\".*\\"/TARGET_UNSTABLE=\\"\\"/g"')
    318             args.append(working_shellball)
    319             cmd = 'sed %s' % ' '.join(args)
    320             self.os_if.run_shell_command(cmd)
    321 
    322     def run_firmwareupdate(self, mode, updater_append=None, options=[]):
    323         """Do firmwareupdate with updater in temp_dir.
    324 
    325         Args:
    326             updater_append: decide which shellball to use with format
    327                 chromeos-firmwareupdate-[append]. Use'chromeos-firmwareupdate'
    328                 if updater_append is None.
    329             mode: ex.'autoupdate', 'recovery', 'bootok', 'factory_install'...
    330             options: ex. ['--noupdate_ec', '--nocheck_rw_compatible'] or [] for
    331                 no option.
    332         """
    333         if updater_append:
    334             updater = os.path.join(
    335                 self._temp_path, 'chromeos-firmwareupdate-%s' % updater_append)
    336         else:
    337             updater = os.path.join(self._temp_path, 'chromeos-firmwareupdate')
    338         command = '/bin/sh %s --mode %s %s' % (updater, mode, ' '.join(options))
    339 
    340         if mode == 'bootok':
    341             # Since CL:459837, bootok is moved to chromeos-setgoodfirmware.
    342             new_command = '/usr/sbin/chromeos-setgoodfirmware'
    343             command = 'if [ -e %s ]; then %s; else %s; fi' % (
    344                     new_command, new_command, command)
    345 
    346         self.os_if.run_shell_command(command)
    347 
    348     def cbfs_setup_work_dir(self):
    349         """Sets up cbfs on DUT.
    350 
    351         Finds bios.bin on the DUT and sets up a temp dir to operate on
    352         bios.bin.  If a bios.bin was specified, it is copied to the DUT
    353         and used instead of the native bios.bin.
    354 
    355         Returns:
    356             The cbfs work directory path.
    357         """
    358 
    359         self.os_if.remove_dir(self._cbfs_work_path)
    360         self.os_if.copy_dir(self._work_path, self._cbfs_work_path)
    361 
    362         return self._cbfs_work_path
    363 
    364     def cbfs_extract_chip(self, fw_name, extension='.bin'):
    365         """Extracts chip firmware blob from cbfs.
    366 
    367         For a given chip type, looks for the corresponding firmware
    368         blob and hash in the specified bios.  The firmware blob and
    369         hash are extracted into self._cbfs_work_path.
    370 
    371         The extracted blobs will be <fw_name><extension> and
    372         <fw_name>.hash located in cbfs_work_path.
    373 
    374         Args:
    375             fw_name: Chip firmware name to be extracted.
    376             extension: Extension of the name of the cbfs component.
    377 
    378         Returns:
    379             Boolean success status.
    380         """
    381 
    382         bios = os.path.join(self._cbfs_work_path, self._bios_path)
    383         fw = fw_name
    384         cbfs_extract = '%s %s extract -r FW_MAIN_A -n %s%%s -f %s%%s' % (
    385             self.CBFSTOOL,
    386             bios,
    387             fw,
    388             os.path.join(self._cbfs_work_path, fw))
    389 
    390         cmd = cbfs_extract % (extension, extension)
    391         if self.os_if.run_shell_command_get_status(cmd) != 0:
    392             return False
    393 
    394         cmd = cbfs_extract % ('.hash', '.hash')
    395         if self.os_if.run_shell_command_get_status(cmd) != 0:
    396             return False
    397 
    398         return True
    399 
    400     def cbfs_get_chip_hash(self, fw_name):
    401         """Returns chip firmware hash blob.
    402 
    403         For a given chip type, returns the chip firmware hash blob.
    404         Before making this request, the chip blobs must have been
    405         extracted from cbfs using cbfs_extract_chip().
    406         The hash data is returned as hexadecimal string.
    407 
    408         Args:
    409             fw_name:
    410                 Chip firmware name whose hash blob to get.
    411 
    412         Returns:
    413             Boolean success status.
    414 
    415         Raises:
    416             shell_wrapper.ShellError: Underlying remote shell
    417                 operations failed.
    418         """
    419 
    420         hexdump_cmd = '%s %s.hash' % (
    421             self.HEXDUMP,
    422             os.path.join(self._cbfs_work_path, fw_name))
    423         hashblob = self.os_if.run_shell_command_get_output(hexdump_cmd)
    424         return hashblob
    425 
    426     def cbfs_replace_chip(self, fw_name, extension='.bin'):
    427         """Replaces chip firmware in CBFS (bios.bin).
    428 
    429         For a given chip type, replaces its firmware blob and hash in
    430         bios.bin.  All files referenced are expected to be in the
    431         directory set up using cbfs_setup_work_dir().
    432 
    433         Args:
    434             fw_name: Chip firmware name to be replaced.
    435             extension: Extension of the name of the cbfs component.
    436 
    437         Returns:
    438             Boolean success status.
    439 
    440         Raises:
    441             shell_wrapper.ShellError: Underlying remote shell
    442                 operations failed.
    443         """
    444 
    445         bios = os.path.join(self._cbfs_work_path, self._bios_path)
    446         rm_hash_cmd = '%s %s remove -r FW_MAIN_A,FW_MAIN_B -n %s.hash' % (
    447             self.CBFSTOOL, bios, fw_name)
    448         rm_bin_cmd = '%s %s remove -r FW_MAIN_A,FW_MAIN_B -n %s%s' % (
    449             self.CBFSTOOL, bios, fw_name, extension)
    450         expand_cmd = '%s %s expand -r FW_MAIN_A,FW_MAIN_B' % (
    451             self.CBFSTOOL, bios)
    452         add_hash_cmd = ('%s %s add -r FW_MAIN_A,FW_MAIN_B -t raw -c none '
    453                         '-f %s.hash -n %s.hash') % (
    454                             self.CBFSTOOL,
    455                             bios,
    456                             os.path.join(self._cbfs_work_path, fw_name),
    457                             fw_name)
    458         add_bin_cmd = ('%s %s add -r FW_MAIN_A,FW_MAIN_B -t raw -c lzma '
    459                        '-f %s%s -n %s%s') % (
    460                            self.CBFSTOOL,
    461                            bios,
    462                            os.path.join(self._cbfs_work_path, fw_name),
    463                            extension,
    464                            fw_name,
    465                            extension)
    466         truncate_cmd = '%s %s truncate -r FW_MAIN_A,FW_MAIN_B' % (
    467             self.CBFSTOOL, bios)
    468 
    469         self.os_if.run_shell_command(rm_hash_cmd)
    470         self.os_if.run_shell_command(rm_bin_cmd)
    471         try:
    472             self.os_if.run_shell_command(expand_cmd)
    473         except shell_wrapper.ShellError:
    474             self.os_if.log(('%s may be too old, '
    475                             'continuing without "expand" support') %
    476                            self.CBFSTOOL)
    477 
    478         self.os_if.run_shell_command(add_hash_cmd)
    479         self.os_if.run_shell_command(add_bin_cmd)
    480         try:
    481             self.os_if.run_shell_command(truncate_cmd)
    482         except shell_wrapper.ShellError:
    483             self.os_if.log(('%s may be too old, '
    484                             'continuing without "truncate" support') %
    485                            self.CBFSTOOL)
    486 
    487         return True
    488 
    489     def cbfs_sign_and_flash(self):
    490         """Signs CBFS (bios.bin) and flashes it."""
    491         self.resign_firmware(work_path=self._cbfs_work_path)
    492         self._bios_handler.new_image(
    493                 os.path.join(self._cbfs_work_path, self._bios_path))
    494         self._bios_handler.write_whole()
    495         return True
    496 
    497     def get_temp_path(self):
    498         """Get temp directory path."""
    499         return self._temp_path
    500 
    501     def get_keys_path(self):
    502         """Get keys directory path."""
    503         return self._keys_path
    504 
    505     def get_cbfs_work_path(self):
    506         """Get cbfs work directory path."""
    507         return self._cbfs_work_path
    508 
    509     def get_work_path(self):
    510         """Get work directory path."""
    511         return self._work_path
    512 
    513     def get_bios_relative_path(self):
    514         """Gets the relative path of the bios image in the shellball."""
    515         return self._bios_path
    516 
    517     def get_ec_relative_path(self):
    518         """Gets the relative path of the ec image in the shellball."""
    519         return self._ec_path
    520