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 tuple.
    110 
    111         This method should be called after _setup_temp_dir.
    112 
    113         Returns:
    114             Shellball's fwid tuple (ro_fwid, rw_fwid).
    115         """
    116         self._bios_handler.new_image(
    117                 os.path.join(self._work_path, self._bios_path))
    118         # Remove the tailing null characters
    119         ro_fwid = self._bios_handler.get_section_fwid('ro').rstrip('\0')
    120         rw_fwid = self._bios_handler.get_section_fwid('a').rstrip('\0')
    121         return (ro_fwid, rw_fwid)
    122 
    123     def retrieve_ecid(self):
    124         """Retrieve shellball's ecid.
    125 
    126         This method should be called after _setup_temp_dir.
    127 
    128         Returns:
    129             Shellball's ecid.
    130         """
    131         self._ec_handler.new_image(
    132                 os.path.join(self._work_path, self._ec_path))
    133         fwid = self._ec_handler.get_section_fwid('rw')
    134         # Remove the tailing null characters
    135         return fwid.rstrip('\0')
    136 
    137     def retrieve_ec_hash(self):
    138         """Retrieve the hex string of the EC hash."""
    139         return self._ec_handler.get_section_hash('rw')
    140 
    141     def modify_ecid_and_flash_to_bios(self):
    142         """Modify ecid, put it to AP firmware, and flash it to the system.
    143 
    144         This method is used for testing EC software sync for EC EFS (Early
    145         Firmware Selection). It creates a slightly different EC RW image
    146         (a different EC fwid) in AP firmware, in order to trigger EC
    147         software sync on the next boot (a different hash with the original
    148         EC RW).
    149 
    150         The steps of this method:
    151          * Modify the EC fwid by appending a '~', like from
    152            'fizz_v1.1.7374-147f1bd64' to 'fizz_v1.1.7374-147f1bd64~'.
    153          * Resign the EC image.
    154          * Store the modififed EC RW image to CBFS component 'ecrw' of the
    155            AP firmware's FW_MAIN_A and FW_MAIN_B, and also the new hash.
    156          * Resign the AP image.
    157          * Flash the modified AP image back to the system.
    158         """
    159         self.cbfs_setup_work_dir()
    160 
    161         fwid = self.retrieve_ecid()
    162         if fwid.endswith('~'):
    163             raise FirmwareUpdaterError('The EC fwid is already modified')
    164 
    165         # Modify the EC FWID and resign
    166         fwid = fwid[:-1] + '~'
    167         self._ec_handler.set_section_fwid('rw', fwid)
    168         self._ec_handler.resign_ec_rwsig()
    169 
    170         # Replace ecrw to the new one
    171         ecrw_bin_path = os.path.join(self._cbfs_work_path,
    172                                      chip_utils.ecrw.cbfs_bin_name)
    173         self._ec_handler.dump_section_body('rw', ecrw_bin_path)
    174 
    175         # Replace ecrw.hash to the new one
    176         ecrw_hash_path = os.path.join(self._cbfs_work_path,
    177                                       chip_utils.ecrw.cbfs_hash_name)
    178         with open(ecrw_hash_path, 'w') as f:
    179             f.write(self.retrieve_ec_hash())
    180 
    181         # Store the modified ecrw and its hash to cbfs
    182         self.cbfs_replace_chip(chip_utils.ecrw.fw_name, extension='')
    183 
    184         # Resign and flash the AP firmware back to the system
    185         self.cbfs_sign_and_flash()
    186 
    187     def resign_firmware(self, version=None, work_path=None):
    188         """Resign firmware with version.
    189 
    190         Args:
    191             version: new firmware version number, default to no modification.
    192             work_path: work path, default to the updater work path.
    193         """
    194         if work_path is None:
    195             work_path = self._work_path
    196         self.os_if.run_shell_command(
    197                 '/usr/share/vboot/bin/resign_firmwarefd.sh '
    198                 '%s %s %s %s %s %s %s %s' % (
    199                     os.path.join(work_path, self._bios_path),
    200                     os.path.join(self._temp_path, 'output.bin'),
    201                     os.path.join(self._keys_path, 'firmware_data_key.vbprivk'),
    202                     os.path.join(self._keys_path, 'firmware.keyblock'),
    203                     os.path.join(self._keys_path,
    204                                  'dev_firmware_data_key.vbprivk'),
    205                     os.path.join(self._keys_path, 'dev_firmware.keyblock'),
    206                     os.path.join(self._keys_path, 'kernel_subkey.vbpubk'),
    207                     ('%d' % version) if version is not None else ''))
    208         self.os_if.copy_file('%s' % os.path.join(self._temp_path, 'output.bin'),
    209                              '%s' % os.path.join(
    210                                  work_path, self._bios_path))
    211 
    212     def _detect_image_paths(self):
    213         """Scans shellball to find correct bios and ec image paths."""
    214         def _extract_path_from_match(match_result, model):
    215           """Extract a path from a matched line of setvars.sh.
    216 
    217           Args:
    218             match_result: Match object: group 1 contains the quoted filename.
    219             model: Name of model to use to resolve ${MODEL_DIR} in the filename.
    220 
    221           Returns:
    222             pathname to firmware file (e.g. 'models/grunt/bios.bin').
    223           """
    224           pathname = match_result.group(1).replace('"', '')
    225           pathname = pathname.replace('${MODEL_DIR}', 'models/' + model)
    226           return pathname
    227 
    228         model_result = self.os_if.run_shell_command_get_output(
    229             'mosys platform model')
    230         if model_result:
    231             model = model_result[0]
    232             search_path = os.path.join(
    233                 self._work_path, 'models', model, 'setvars.sh')
    234             grep_result = self.os_if.run_shell_command_get_output(
    235                 'grep IMAGE_MAIN= %s' % search_path)
    236             if grep_result:
    237                 match = re.match('IMAGE_MAIN=(.*)', grep_result[0])
    238                 if match:
    239                   self._bios_path = _extract_path_from_match(match, model)
    240             grep_result = self.os_if.run_shell_command_get_output(
    241                 'grep IMAGE_EC= %s' % search_path)
    242             if grep_result:
    243                 match = re.match('IMAGE_EC=(.*)', grep_result[0])
    244                 if match:
    245                   self._ec_path = _extract_path_from_match(match, model)
    246 
    247     def _update_target_fwid(self):
    248         """Update target fwid/ecid in the setvars.sh."""
    249         model_result = self.os_if.run_shell_command_get_output(
    250             'mosys platform model')
    251         if model_result:
    252             model = model_result[0]
    253             setvars_path = os.path.join(
    254                 self._work_path, 'models', model, 'setvars.sh')
    255             if self.os_if.path_exists(setvars_path):
    256                 ro_fwid, rw_fwid = self.retrieve_fwid()
    257                 args = ['-i']
    258                 args.append(
    259                     '"s/TARGET_FWID=\\".*\\"/TARGET_FWID=\\"%s\\"/g"'
    260                     % rw_fwid)
    261                 args.append(setvars_path)
    262                 cmd = 'sed %s' % ' '.join(args)
    263                 self.os_if.run_shell_command(cmd)
    264 
    265                 args = ['-i']
    266                 args.append(
    267                     '"s/TARGET_RO_FWID=\\".*\\"/TARGET_RO_FWID=\\"%s\\"/g"'
    268                     % ro_fwid)
    269                 args.append(setvars_path)
    270                 cmd = 'sed %s' % ' '.join(args)
    271                 self.os_if.run_shell_command(cmd)
    272 
    273                 # Only update ECID if an EC image is found
    274                 if self.get_ec_relative_path():
    275                     ecid = self.retrieve_ecid()
    276                     args = ['-i']
    277                     args.append(
    278                         '"s/TARGET_ECID=\\".*\\"/TARGET_ECID=\\"%s\\"/g"'
    279                         % ecid)
    280                     args.append(setvars_path)
    281                     cmd = 'sed %s' % ' '.join(args)
    282                     self.os_if.run_shell_command(cmd)
    283 
    284     def extract_shellball(self, append=None):
    285         """Extract the working shellball.
    286 
    287         Args:
    288             append: decide which shellball to use with format
    289                 chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate'
    290                 if append is None.
    291         """
    292         working_shellball = os.path.join(self._temp_path,
    293                                          'chromeos-firmwareupdate')
    294         if append:
    295             working_shellball = working_shellball + '-%s' % append
    296 
    297         self.os_if.run_shell_command('sh %s --sb_extract %s' % (
    298                 working_shellball, self._work_path))
    299 
    300         self._detect_image_paths()
    301 
    302     def repack_shellball(self, append=None):
    303         """Repack shellball with new fwid.
    304 
    305         New fwid follows the rule: [orignal_fwid]-[append].
    306 
    307         Args:
    308             append: save the new shellball with a suffix, for example,
    309                 chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate'
    310                 if append is None.
    311         """
    312         self._update_target_fwid();
    313 
    314         working_shellball = os.path.join(self._temp_path,
    315                                          'chromeos-firmwareupdate')
    316         if append:
    317             self.os_if.copy_file(working_shellball,
    318                                  working_shellball + '-%s' % append)
    319             working_shellball = working_shellball + '-%s' % append
    320 
    321         self.os_if.run_shell_command('sh %s --sb_repack %s' % (
    322                 working_shellball, self._work_path))
    323 
    324         if append:
    325             args = ['-i']
    326             args.append(
    327                     '"s/TARGET_FWID=\\"\\(.*\\)\\"/TARGET_FWID=\\"\\1.%s\\"/g"'
    328                     % append)
    329             args.append(working_shellball)
    330             cmd = 'sed %s' % ' '.join(args)
    331             self.os_if.run_shell_command(cmd)
    332 
    333             args = ['-i']
    334             args.append('"s/TARGET_UNSTABLE=\\".*\\"/TARGET_UNSTABLE=\\"\\"/g"')
    335             args.append(working_shellball)
    336             cmd = 'sed %s' % ' '.join(args)
    337             self.os_if.run_shell_command(cmd)
    338 
    339     def run_firmwareupdate(self, mode, updater_append=None, options=[]):
    340         """Do firmwareupdate with updater in temp_dir.
    341 
    342         Args:
    343             updater_append: decide which shellball to use with format
    344                 chromeos-firmwareupdate-[append]. Use'chromeos-firmwareupdate'
    345                 if updater_append is None.
    346             mode: ex.'autoupdate', 'recovery', 'bootok', 'factory_install'...
    347             options: ex. ['--noupdate_ec', '--force'] or [] for
    348                 no option.
    349         """
    350         if updater_append:
    351             updater = os.path.join(
    352                 self._temp_path, 'chromeos-firmwareupdate-%s' % updater_append)
    353         else:
    354             updater = os.path.join(self._temp_path, 'chromeos-firmwareupdate')
    355         command = '/bin/sh %s --mode %s %s' % (updater, mode, ' '.join(options))
    356 
    357         if mode == 'bootok':
    358             # Since CL:459837, bootok is moved to chromeos-setgoodfirmware.
    359             new_command = '/usr/sbin/chromeos-setgoodfirmware'
    360             command = 'if [ -e %s ]; then %s; else %s; fi' % (
    361                     new_command, new_command, command)
    362 
    363         self.os_if.run_shell_command(command)
    364 
    365     def cbfs_setup_work_dir(self):
    366         """Sets up cbfs on DUT.
    367 
    368         Finds bios.bin on the DUT and sets up a temp dir to operate on
    369         bios.bin.  If a bios.bin was specified, it is copied to the DUT
    370         and used instead of the native bios.bin.
    371 
    372         Returns:
    373             The cbfs work directory path.
    374         """
    375 
    376         self.os_if.remove_dir(self._cbfs_work_path)
    377         self.os_if.copy_dir(self._work_path, self._cbfs_work_path)
    378 
    379         return self._cbfs_work_path
    380 
    381     def cbfs_extract_chip(self, fw_name, extension='.bin'):
    382         """Extracts chip firmware blob from cbfs.
    383 
    384         For a given chip type, looks for the corresponding firmware
    385         blob and hash in the specified bios.  The firmware blob and
    386         hash are extracted into self._cbfs_work_path.
    387 
    388         The extracted blobs will be <fw_name><extension> and
    389         <fw_name>.hash located in cbfs_work_path.
    390 
    391         Args:
    392             fw_name: Chip firmware name to be extracted.
    393             extension: Extension of the name of the cbfs component.
    394 
    395         Returns:
    396             Boolean success status.
    397         """
    398 
    399         bios = os.path.join(self._cbfs_work_path, self._bios_path)
    400         fw = fw_name
    401         cbfs_extract = '%s %s extract -r FW_MAIN_A -n %s%%s -f %s%%s' % (
    402             self.CBFSTOOL,
    403             bios,
    404             fw,
    405             os.path.join(self._cbfs_work_path, fw))
    406 
    407         cmd = cbfs_extract % (extension, extension)
    408         if self.os_if.run_shell_command_get_status(cmd) != 0:
    409             return False
    410 
    411         cmd = cbfs_extract % ('.hash', '.hash')
    412         if self.os_if.run_shell_command_get_status(cmd) != 0:
    413             return False
    414 
    415         return True
    416 
    417     def cbfs_get_chip_hash(self, fw_name):
    418         """Returns chip firmware hash blob.
    419 
    420         For a given chip type, returns the chip firmware hash blob.
    421         Before making this request, the chip blobs must have been
    422         extracted from cbfs using cbfs_extract_chip().
    423         The hash data is returned as hexadecimal string.
    424 
    425         Args:
    426             fw_name:
    427                 Chip firmware name whose hash blob to get.
    428 
    429         Returns:
    430             Boolean success status.
    431 
    432         Raises:
    433             shell_wrapper.ShellError: Underlying remote shell
    434                 operations failed.
    435         """
    436 
    437         hexdump_cmd = '%s %s.hash' % (
    438             self.HEXDUMP,
    439             os.path.join(self._cbfs_work_path, fw_name))
    440         hashblob = self.os_if.run_shell_command_get_output(hexdump_cmd)
    441         return hashblob
    442 
    443     def cbfs_replace_chip(self, fw_name, extension='.bin'):
    444         """Replaces chip firmware in CBFS (bios.bin).
    445 
    446         For a given chip type, replaces its firmware blob and hash in
    447         bios.bin.  All files referenced are expected to be in the
    448         directory set up using cbfs_setup_work_dir().
    449 
    450         Args:
    451             fw_name: Chip firmware name to be replaced.
    452             extension: Extension of the name of the cbfs component.
    453 
    454         Returns:
    455             Boolean success status.
    456 
    457         Raises:
    458             shell_wrapper.ShellError: Underlying remote shell
    459                 operations failed.
    460         """
    461 
    462         bios = os.path.join(self._cbfs_work_path, self._bios_path)
    463         rm_hash_cmd = '%s %s remove -r FW_MAIN_A,FW_MAIN_B -n %s.hash' % (
    464             self.CBFSTOOL, bios, fw_name)
    465         rm_bin_cmd = '%s %s remove -r FW_MAIN_A,FW_MAIN_B -n %s%s' % (
    466             self.CBFSTOOL, bios, fw_name, extension)
    467         expand_cmd = '%s %s expand -r FW_MAIN_A,FW_MAIN_B' % (
    468             self.CBFSTOOL, bios)
    469         add_hash_cmd = ('%s %s add -r FW_MAIN_A,FW_MAIN_B -t raw -c none '
    470                         '-f %s.hash -n %s.hash') % (
    471                             self.CBFSTOOL,
    472                             bios,
    473                             os.path.join(self._cbfs_work_path, fw_name),
    474                             fw_name)
    475         add_bin_cmd = ('%s %s add -r FW_MAIN_A,FW_MAIN_B -t raw -c lzma '
    476                        '-f %s%s -n %s%s') % (
    477                            self.CBFSTOOL,
    478                            bios,
    479                            os.path.join(self._cbfs_work_path, fw_name),
    480                            extension,
    481                            fw_name,
    482                            extension)
    483         truncate_cmd = '%s %s truncate -r FW_MAIN_A,FW_MAIN_B' % (
    484             self.CBFSTOOL, bios)
    485 
    486         self.os_if.run_shell_command(rm_hash_cmd)
    487         self.os_if.run_shell_command(rm_bin_cmd)
    488         try:
    489             self.os_if.run_shell_command(expand_cmd)
    490         except shell_wrapper.ShellError:
    491             self.os_if.log(('%s may be too old, '
    492                             'continuing without "expand" support') %
    493                            self.CBFSTOOL)
    494 
    495         self.os_if.run_shell_command(add_hash_cmd)
    496         self.os_if.run_shell_command(add_bin_cmd)
    497         try:
    498             self.os_if.run_shell_command(truncate_cmd)
    499         except shell_wrapper.ShellError:
    500             self.os_if.log(('%s may be too old, '
    501                             'continuing without "truncate" support') %
    502                            self.CBFSTOOL)
    503 
    504         return True
    505 
    506     def cbfs_sign_and_flash(self):
    507         """Signs CBFS (bios.bin) and flashes it."""
    508         self.resign_firmware(work_path=self._cbfs_work_path)
    509         self._bios_handler.new_image(
    510                 os.path.join(self._cbfs_work_path, self._bios_path))
    511         self._bios_handler.write_whole()
    512         return True
    513 
    514     def get_temp_path(self):
    515         """Get temp directory path."""
    516         return self._temp_path
    517 
    518     def get_keys_path(self):
    519         """Get keys directory path."""
    520         return self._keys_path
    521 
    522     def get_cbfs_work_path(self):
    523         """Get cbfs work directory path."""
    524         return self._cbfs_work_path
    525 
    526     def get_work_path(self):
    527         """Get work directory path."""
    528         return self._work_path
    529 
    530     def get_bios_relative_path(self):
    531         """Gets the relative path of the bios image in the shellball."""
    532         return self._bios_path
    533 
    534     def get_ec_relative_path(self):
    535         """Gets the relative path of the ec image in the shellball."""
    536         return self._ec_path
    537