xref: /aosp_15_r20/external/autotest/client/cros/faft/utils/firmware_updater.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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"""A module to support automatic firmware update.
5
6See FirmwareUpdater object below.
7"""
8import array
9import json
10import logging
11import os
12import six
13
14from autotest_lib.client.common_lib import error
15from autotest_lib.client.common_lib.cros import chip_utils
16from autotest_lib.client.common_lib.cros import cros_config
17from autotest_lib.client.cros.faft.utils import flashrom_handler
18
19
20class FirmwareUpdaterError(Exception):
21    """Error in the FirmwareUpdater module."""
22
23
24class FirmwareUpdater(object):
25    """An object to support firmware update.
26
27    This object will create a temporary directory in /usr/local/tmp/faft/autest
28    with two subdirs, keys/ and work/. You can modify the keys in keys/ dir. If
29    you want to provide a given shellball to do firmware update, put shellball
30    under /usr/local/tmp/faft/autest with name chromeos-firmwareupdate.
31
32    @type os_if: autotest_lib.client.cros.faft.utils.os_interface.OSInterface
33    """
34
35    DAEMON = 'update-engine'
36    CBFSTOOL = 'cbfstool'
37    HEXDUMP = 'hexdump -v -e \'1/1 "0x%02x\\n"\''
38
39    DEFAULT_SHELLBALL = '/usr/sbin/chromeos-firmwareupdate'
40    DEFAULT_SUBDIR = 'autest'  # subdirectory of os_interface.state_dir
41    DEFAULT_SECTION_FOR_TARGET = {'bios': 'a', 'ec': 'rw'}
42
43    CBFS_REGIONS_MAP = {'a': 'FW_MAIN_A', 'b': 'FW_MAIN_B'}
44
45    def __init__(self, os_if):
46        """Initialize the updater tools, but don't load the image data yet."""
47        self.os_if = os_if
48        self._temp_path = self.os_if.state_dir_file(self.DEFAULT_SUBDIR)
49        self._cbfs_work_path = os.path.join(self._temp_path, 'cbfs')
50        self._keys_path = os.path.join(self._temp_path, 'keys')
51        self._work_path = os.path.join(self._temp_path, 'work')
52        self._bios_path = 'bios.bin'
53        self._ec_path = 'ec.bin'
54
55        self.pubkey_path = os.path.join(self._keys_path, 'root_key.vbpubk')
56        self._real_bios_handler = self._create_handler('bios')
57        self._real_ec_handler = self._create_handler('ec')
58        self.initialized = False
59
60    def init(self):
61        """Extract the shellball and other files, unless they already exist."""
62
63        if self.os_if.is_dir(self._work_path):
64            # If work dir is present, assume the whole temp dir is usable as-is.
65            self._detect_image_paths()
66        else:
67            # If work dir is missing, assume the whole temp dir is unusable, and
68            # recreate it.
69            self._create_temp_dir()
70            self.extract_shellball()
71
72        self.initialized = True
73
74    def _get_handler(self, target):
75        """Return the handler for the target, after initializing it if needed.
76
77        @param target: image type ('bios' or 'ec')
78        @return: the handler for that target
79
80        @type target: str
81        @rtype: flashrom_handler.FlashromHandler
82        """
83        if target == 'bios':
84            if not self._real_bios_handler.initialized:
85                bios_file = self._get_image_path('bios')
86                self._real_bios_handler.init(bios_file)
87            return self._real_bios_handler
88        elif target == 'ec':
89            if not self._real_ec_handler.initialized:
90                ec_file = self._get_image_path('ec')
91                self._real_ec_handler.init(ec_file, allow_fallback=True)
92            return self._real_ec_handler
93        else:
94            raise FirmwareUpdaterError("Unhandled target: %r" % target)
95
96    def _create_handler(self, target, suffix=None):
97        """Return a new (not pre-populated) handler for the given target,
98        such as for use in checking installed versions.
99
100        @param target: image type ('bios' or 'ec')
101        @param suffix: additional piece for subdirectory of handler
102                       Example: 'tmp' -> 'autest/<target>.tmp/'
103        @return: a new handler for that target
104
105        @type target: str
106        @rtype: flashrom_handler.FlashromHandler
107        """
108        if suffix:
109            subdir = '%s/%s.%s' % (self.DEFAULT_SUBDIR, target, suffix)
110        else:
111            subdir = '%s/%s' % (self.DEFAULT_SUBDIR, target)
112        return flashrom_handler.FlashromHandler(self.os_if,
113                                                self.pubkey_path,
114                                                self._keys_path,
115                                                target=target,
116                                                subdir=subdir)
117
118    def _get_image_path(self, target):
119        """Return the handler for the given target
120
121        @param target: image type ('bios' or 'ec')
122        @return: the path of the image file for that target
123
124        @type target: str
125        @rtype: str
126        """
127        if target == 'bios':
128            return os.path.join(self._work_path, self._bios_path)
129        elif target == 'ec':
130            return os.path.join(self._work_path, self._ec_path)
131        else:
132            raise FirmwareUpdaterError("Unhandled target: %r" % target)
133
134    def _get_default_section(self, target):
135        """Return the default section to work with, for the given target
136
137        @param target: image type ('bios' or 'ec')
138        @return: the default section for that target
139
140        @type target: str
141        @rtype: str
142        """
143        if target in self.DEFAULT_SECTION_FOR_TARGET:
144            return self.DEFAULT_SECTION_FOR_TARGET[target]
145        else:
146            raise FirmwareUpdaterError("Unhandled target: %r" % target)
147
148    def _create_temp_dir(self):
149        """Create (or recreate) the temporary directory.
150
151        The default /usr/sbin/chromeos-firmwareupdate is copied into _temp_dir,
152        and devkeys are copied to _key_path. The caller is responsible for
153        extracting the copied shellball.
154        """
155        self.cleanup_temp_dir()
156
157        self.os_if.create_dir(self._temp_path)
158        self.os_if.create_dir(self._cbfs_work_path)
159        self.os_if.create_dir(self._work_path)
160        self.os_if.copy_dir('/usr/share/vboot/devkeys', self._keys_path)
161
162        working_shellball = os.path.join(self._temp_path,
163                                         'chromeos-firmwareupdate')
164        self.os_if.copy_file(self.DEFAULT_SHELLBALL, working_shellball)
165
166    def cleanup_temp_dir(self):
167        """Cleanup temporary directory."""
168        if self.os_if.is_dir(self._temp_path):
169            self.os_if.remove_dir(self._temp_path)
170
171    def stop_daemon(self):
172        """Stop update-engine daemon."""
173        logging.info('Stopping %s...', self.DAEMON)
174        cmd = 'status %s | grep stop || stop %s' % (self.DAEMON, self.DAEMON)
175        self.os_if.run_shell_command(cmd)
176
177    def start_daemon(self):
178        """Start update-engine daemon."""
179        logging.info('Starting %s...', self.DAEMON)
180        cmd = 'status %s | grep start || start %s' % (self.DAEMON, self.DAEMON)
181        self.os_if.run_shell_command(cmd)
182
183    def get_ec_hash(self):
184        """Retrieve the hex string of the EC hash."""
185        ec = self._get_handler('ec')
186        return ec.get_section_hash('rw')
187
188    def get_section_fwid(self, target='bios', section=None):
189        """Get one fwid from in-memory image, for the given target.
190
191        @param target: the image type to get from: 'bios (default) or 'ec'
192        @param section: section to return.  Default: A for bios, RW for EC
193
194        @type target: str | None
195        @rtype: str
196        """
197        if section is None:
198            section = self._get_default_section(target)
199        image_path = self._get_image_path(target)
200        if target == 'ec' and not os.path.isfile(image_path):
201            # If the EC image is missing, report a specific error message.
202            raise FirmwareUpdaterError("Shellball does not contain ec.bin")
203
204        handler = self._get_handler(target)
205        handler.new_image(image_path)
206        fwid = handler.get_section_fwid(section)
207        if fwid is not None:
208            return str(fwid, 'utf-8')
209        else:
210            return None
211
212    def get_device_fwids(self, target='bios'):
213        """Get all non-empty fwids from flash, for the given target.
214
215        @param target: the image type to get from: 'bios' (default) or 'ec'
216        @return: fwid for the sections
217
218        @type target: str
219        @type filename: str
220        @rtype: dict
221        """
222        handler = self._create_handler(target, 'flashdevice')
223        handler.new_image()
224
225        fwids = {}
226        for section in handler.fv_sections:
227            fwid = handler.get_section_fwid(section)
228            if fwid is not None:
229                fwids[section] = fwid
230        return fwids
231
232    def get_image_fwids(self, target='bios', filename=None):
233        """Get all non-empty fwids from disk, for the given target.
234
235        @param target: the image type to get from: 'bios' (default) or 'ec'
236        @param filename: filename to read instead of using the default shellball
237        @return: fwid for the sections
238
239        @type target: str
240        @type filename: str
241        @rtype: dict
242        """
243        if filename:
244            filename = os.path.join(self._temp_path, filename)
245            handler = self._create_handler(target, 'image')
246            handler.new_image(filename)
247        else:
248            filename = self._get_image_path(target)
249            handler = self._get_handler(target)
250            if target == 'ec' and not os.path.isfile(filename):
251                # If the EC image is missing, report a specific error message.
252                raise FirmwareUpdaterError("Shellball does not contain ec.bin")
253
254        fwids = {}
255        for section in handler.fv_sections:
256            fwid = handler.get_section_fwid(section)
257            if fwid is not None:
258                fwids[section] = fwid
259        return fwids
260
261    def modify_image_fwids(self, target='bios', sections=None):
262        """Modify the fwid in the image, but don't flash it.
263
264        @param target: the image type to modify: 'bios' (default) or 'ec'
265        @param sections: section(s) to modify.  Default: A for bios, RW for ec
266        @return: fwids for the modified sections, as {section: fwid}
267
268        @type target: str
269        @type sections: tuple | list
270        @rtype: dict
271        """
272        if sections is None:
273            sections = [self._get_default_section(target)]
274
275        image_fullpath = self._get_image_path(target)
276        if target == 'ec' and not os.path.isfile(image_fullpath):
277            # If the EC image is missing, report a specific error message.
278            raise FirmwareUpdaterError("Shellball does not contain ec.bin")
279
280        handler = self._get_handler(target)
281        fwids = handler.modify_fwids(sections)
282
283        handler.dump_whole(image_fullpath)
284        handler.new_image(image_fullpath)
285
286        return fwids
287
288    def modify_ecid_and_flash_to_bios(self):
289        """Modify ecid, put it to AP firmware, and flash it to the system.
290
291        This method is used for testing EC software sync for EC EFS (Early
292        Firmware Selection). It creates a slightly different EC RW image
293        (a different EC fwid) in AP firmware, in order to trigger EC
294        software sync on the next boot (a different hash with the original
295        EC RW).
296
297        The steps of this method:
298         * Modify the EC fwid by appending a '~', like from
299           'fizz_v1.1.7374-147f1bd64' to 'fizz_v1.1.7374-147f1bd64~'.
300         * Resign the EC image.
301         * Store the modififed EC RW image to CBFS component 'ecrw' of the
302           AP firmware's FW_MAIN_A and FW_MAIN_B, and also the new hash.
303         * Resign the AP image.
304         * Flash the modified AP image back to the system.
305        """
306        self.cbfs_setup_work_dir()
307
308        fwid = self.get_section_fwid('ec', 'rw')
309        if fwid.endswith('~'):
310            raise FirmwareUpdaterError('The EC fwid is already modified')
311
312        # Modify the EC FWID and resign
313        fwid = fwid[:-1] + '~'
314        ec = self._get_handler('ec')
315        ec.set_section_fwid('rw', fwid)
316        ec.resign_ec_rwsig()
317
318        # Replace ecrw to the new one
319        ecrw_bin_path = os.path.join(self._cbfs_work_path,
320                                     chip_utils.ecrw.cbfs_bin_name)
321        ec.dump_section_body('rw', ecrw_bin_path)
322
323        # Replace ecrw.hash to the new one
324        ecrw_hash_path = os.path.join(self._cbfs_work_path,
325                                      chip_utils.ecrw.cbfs_hash_name)
326        with open(ecrw_hash_path, 'wb') as f:
327            f.write(self.get_ec_hash())
328
329        # Store the modified ecrw and its hash to cbfs
330        self.cbfs_replace_chip(chip_utils.ecrw.fw_name, extension='')
331
332        # Resign and flash the AP firmware back to the system
333        self.cbfs_sign_and_flash()
334
335    def corrupt_diagnostics_image(self, local_path):
336        """Corrupts a diagnostics image in the CBFS working directory.
337
338        @param local_path: Filename for storing the diagnostics image in the
339            CBFS working directory
340        """
341
342        # Invert the last few bytes of the image. Note that cbfstool will
343        # silently ignore bytes added after the end of the ELF, and it will
344        # refuse to use an ELF with noticeably corrupted headers as a payload.
345        num_bytes = 4
346        with open(local_path, 'rb+') as image:
347            image.seek(-num_bytes, os.SEEK_END)
348            last_bytes = array.array('B')
349            last_bytes.fromfile(image, num_bytes)
350
351            for i in range(len(last_bytes)):
352                last_bytes[i] = last_bytes[i] ^ 0xff
353
354            image.seek(-num_bytes, os.SEEK_END)
355            last_bytes.tofile(image)
356
357    def resign_firmware(self, version=None, work_path=None):
358        """Resign firmware with version.
359
360        Args:
361            version: new firmware version number, default to no modification.
362            work_path: work path, default to the updater work path.
363        """
364        if work_path is None:
365            work_path = self._work_path
366        self.os_if.run_shell_command(
367                '/usr/share/vboot/bin/resign_firmwarefd.sh '
368                '%s %s %s %s %s %s' %
369                (os.path.join(work_path, self._bios_path),
370                 os.path.join(self._temp_path, 'output.bin'),
371                 os.path.join(self._keys_path, 'firmware_data_key.vbprivk'),
372                 os.path.join(self._keys_path, 'firmware.keyblock'),
373                 os.path.join(self._keys_path, 'kernel_subkey.vbpubk'),
374                 ('%d' % version) if version is not None else ''))
375        self.os_if.copy_file(
376                '%s' % os.path.join(self._temp_path, 'output.bin'),
377                '%s' % os.path.join(work_path, self._bios_path))
378
379    def _read_manifest(self, shellball=None):
380        """This gets the manifest from the shellball or the extracted directory.
381
382        @param shellball: Path of the shellball to read from (via --manifest).
383                          If None (default), read from extracted manifest.json.
384        @return: the manifest information, or None
385
386        @type shellball: str | None
387        @rtype: dict
388        """
389
390        if shellball:
391            output = self.os_if.run_shell_command_get_output(
392                    'sh %s --manifest' % shellball)
393            manifest_text = '\n'.join(output or [])
394        else:
395            manifest_file = os.path.join(self._work_path, 'manifest.json')
396            manifest_text = self.os_if.read_file(manifest_file)
397
398        if manifest_text:
399            return json.loads(manifest_text)
400        else:
401            return None
402
403    def _detect_image_paths(self, shellball=None):
404        """Scans shellball manifest to find correct bios and ec image paths.
405
406        @param shellball: Path of the shellball to read from (via --manifest).
407                          If None (default), read from extracted manifest.json.
408        @type shellball: str | None
409        """
410        model_name = cros_config.call_cros_config_get_output(
411                '/ name', self.os_if.run_shell_command_get_result)
412
413        if not model_name:
414            return
415
416        manifest = self._read_manifest(shellball)
417
418        if manifest:
419            model_info = manifest.get(model_name)
420            if model_info:
421
422                try:
423                    self._bios_path = model_info['host']['image']
424                except KeyError:
425                    pass
426
427                try:
428                    self._ec_path = model_info['ec']['image']
429                except KeyError:
430                    pass
431
432    def extract_shellball(self, append=None):
433        """Extract the working shellball.
434
435        Args:
436            append: decide which shellball to use with format
437                chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate'
438                if append is None.
439        Returns:
440            string: the full path of the shellball
441        """
442        working_shellball = os.path.join(self._temp_path,
443                                         'chromeos-firmwareupdate')
444        if append:
445            working_shellball = working_shellball + '-%s' % append
446
447        self.os_if.run_shell_command('sh %s --unpack %s' %
448                                     (working_shellball, self._work_path))
449
450        # use the json file that was extracted, to catch extraction problems.
451        self._detect_image_paths()
452        return working_shellball
453
454    def repack_shellball(self, append=None):
455        """Repack shellball with new fwid.
456
457        New fwid follows the rule: [orignal_fwid]-[append].
458
459        Args:
460            append: save the new shellball with a suffix, for example,
461                chromeos-firmwareupdate-[append]. Use 'chromeos-firmwareupdate'
462                if append is None.
463        Returns:
464            string: The full path to the shellball
465        """
466
467        working_shellball = os.path.join(self._temp_path,
468                                         'chromeos-firmwareupdate')
469        if append:
470            new_shellball = working_shellball + '-%s' % append
471            self.os_if.copy_file(working_shellball, new_shellball)
472            working_shellball = new_shellball
473
474        self.os_if.run_shell_command('sh %s --repack %s' %
475                                     (working_shellball, self._work_path))
476
477        # use the shellball that was repacked, to catch repacking problems.
478        self._detect_image_paths(working_shellball)
479        return working_shellball
480
481    def reset_shellball(self):
482        """Extract shellball, then revert the AP and EC handlers' data."""
483        self._create_temp_dir()
484        self.extract_shellball()
485        self.reload_images()
486
487    def reload_images(self):
488        """Reload handlers from the on-disk images, in case they've changed."""
489        bios_file = os.path.join(self._work_path, self._bios_path)
490        self._real_bios_handler.deinit()
491        self._real_bios_handler.init(bios_file)
492        if self._real_ec_handler.is_available():
493            ec_file = os.path.join(self._work_path, self._ec_path)
494            self._real_ec_handler.deinit()
495            self._real_ec_handler.init(ec_file, allow_fallback=True)
496
497    def get_firmwareupdate_command(self, mode, append=None, options=None):
498        """Get the command to run firmwareupdate with updater in temp_dir.
499
500        @param append: decide which shellball to use with format
501                chromeos-firmwareupdate-[append].
502                Use'chromeos-firmwareupdate' if append is None.
503        @param mode: ex.'autoupdate', 'recovery', 'bootok', 'factory_install'...
504        @param options: ex. ['--noupdate_ec', '--force'] or [] or None.
505
506        @type append: str
507        @type mode: str
508        @type options: list | tuple | None
509        """
510        if mode == 'bootok':
511            # Since CL:459837, bootok is moved to chromeos-setgoodfirmware.
512            set_good_cmd = '/usr/sbin/chromeos-setgoodfirmware'
513            if os.path.isfile(set_good_cmd):
514                return set_good_cmd
515
516        updater = os.path.join(self._temp_path, 'chromeos-firmwareupdate')
517        if append:
518            updater = '%s-%s' % (updater, append)
519
520        if options is None:
521            options = []
522        if isinstance(options, tuple):
523            options = list(options)
524
525        def _has_emulate(option):
526            return option == '--emulate' or option.startswith('--emulate=')
527
528        if self.os_if.test_mode and not list(filter(_has_emulate, options)):
529            # if in test mode, forcibly use --emulate, if not already used.
530            fake_bios = os.path.join(self._temp_path, 'rpc-test-fake-bios.bin')
531            if not os.path.exists(fake_bios):
532                bios_reader = self._create_handler('bios', 'tmp')
533                bios_reader.dump_flash(fake_bios)
534            options = ['--emulate', fake_bios] + options
535
536        return '/bin/sh %s --mode %s %s' % (updater, mode, ' '.join(options))
537
538    def run_firmwareupdate(self, mode, append=None, options=None):
539        """Do firmwareupdate with updater in temp_dir.
540
541        @param append: decide which shellball to use with format
542                chromeos-firmwareupdate-[append].
543                Use'chromeos-firmwareupdate' if append is None.
544        @param mode: ex.'autoupdate', 'recovery', 'bootok', 'factory_install'...
545        @param options: ex. ['--noupdate_ec', '--force'] or [] or None.
546
547        @type append: str
548        @type mode: str
549        @type options: list | tuple | None
550        """
551        return self.os_if.run_shell_command_get_status(
552                self.get_firmwareupdate_command(mode, append, options))
553
554    def cbfs_setup_work_dir(self):
555        """Sets up cbfs on DUT.
556
557        Finds bios.bin on the DUT and sets up a temp dir to operate on
558        bios.bin.  If a bios.bin was specified, it is copied to the DUT
559        and used instead of the built-in bios.bin.
560
561        @return: The cbfs work directory path.
562        """
563        self.os_if.remove_dir(self._cbfs_work_path)
564        self.os_if.copy_dir(self._work_path, self._cbfs_work_path)
565
566        return self._cbfs_work_path
567
568    @classmethod
569    def _cbfs_regions(cls, sections):
570        """Map from ['A', 'B'] to ['FW_MAIN_A', 'FW_MAIN_B']"""
571        regions = set()
572        for section in sections:
573            region = cls.CBFS_REGIONS_MAP.get(section.lower(), section)
574            regions.add(region)
575        return sorted(regions)
576
577    def cbfs_expand(self, regions):
578        """Expand the CBFS to fill available space
579
580        @param regions: string, such as FW_MAIN_A,FW_MAIN_B
581        """
582        bios = os.path.join(self._cbfs_work_path, self._bios_path)
583        expand_cmd = '%s %s expand -r %s' % (self.CBFSTOOL, bios,
584                                             ','.join(regions))
585        self.os_if.run_shell_command(expand_cmd)
586        return True
587
588    def cbfs_truncate(self, regions):
589        """Truncate the CBFS to fill minimum space
590
591        @param regions: string, such as FW_MAIN_A,FW_MAIN_B
592        """
593        bios = os.path.join(self._cbfs_work_path, self._bios_path)
594        truncate_cmd = '%s %s truncate -r %s' % (self.CBFSTOOL, bios,
595                                                 ','.join(regions))
596        self.os_if.run_shell_command(truncate_cmd)
597        return True
598
599    def cbfs_extract(self,
600                     filename,
601                     extension,
602                     regions=('a', ),
603                     local_filename=None,
604                     arch=None,
605                     bios=None):
606        """Extracts an arbitrary file from cbfs.
607
608        Note that extracting from
609        @param filename: Filename in cbfs, including extension
610        @param extension: Extension of the file, including '.'
611        @param regions: Tuple of regions (the default is just 'a')
612        @param arch: Specific machine architecture to extract (default unset)
613        @param local_filename: Path to use on the DUT, overriding the default in
614                           the cbfs work dir.
615        @param bios: Image from which the cbfs file to be extracted
616        @return: The full path of the extracted file, or None
617        """
618        regions = self._cbfs_regions(regions)
619        if bios is None:
620            bios = os.path.join(self._cbfs_work_path, self._bios_path)
621
622        cbfs_filename = filename + extension
623        if local_filename is None:
624            local_filename = os.path.join(self._cbfs_work_path,
625                                          filename + extension)
626
627        extract_cmd = ('%s %s extract -r %s -n %s%s -f %s' %
628                       (self.CBFSTOOL, bios, ','.join(regions), filename,
629                        extension, local_filename))
630        if arch:
631            extract_cmd += ' -m %s' % arch
632        try:
633            self.os_if.run_shell_command(extract_cmd)
634            if not self.os_if.path_exists(local_filename):
635                logging.warning("File does not exist after extracting:"
636                                " %s", local_filename)
637            return os.path.abspath(local_filename)
638        except error.CmdError:
639            # already logged by run_shell_command()
640            return None
641
642    def cbfs_extract_chip(self,
643                          fw_name,
644                          extension='.bin',
645                          hash_extension='.bash',
646                          regions=('a', )):
647        """Extracts chip firmware blob from cbfs.
648
649        For a given chip type, looks for the corresponding firmware
650        blob and hash in the specified bios.  The firmware blob and
651        hash are extracted into self._cbfs_work_path.
652
653        The extracted blobs will be <fw_name><extension> and
654        <fw_name>.hash located in cbfs_work_path.
655
656        @param fw_name: Chip firmware name to be extracted.
657        @param extension: File extension of the cbfs file, including '.'
658        @param hash_extension: File extension of the hash file, including '.'
659        @return: dict of {'image': image_fullpath, 'hash': hash_fullpath},
660        """
661        regions = self._cbfs_regions(regions)
662
663        results = {}
664
665        if extension is not None:
666            image_path = self.cbfs_extract(fw_name, extension, regions)
667            if image_path:
668                results['image'] = image_path
669
670        if hash_extension is not None and hash_extension != extension:
671            hash_path = self.cbfs_extract(fw_name, hash_extension, regions)
672            if hash_path:
673                results['hash'] = hash_path
674
675        return results
676
677    def cbfs_extract_diagnostics(self, diag_name, local_path):
678        """Runs cbfstool to extract a diagnostics image.
679
680        @param diag_name: Name of the diagnostics image in CBFS
681        @param local_path: Filename for storing the diagnostics image in the
682            CBFS working directory
683        """
684        return self.cbfs_extract(diag_name,
685                                 '', ['RW_LEGACY'],
686                                 local_path,
687                                 arch='x86')
688
689    def cbfs_get_chip_hash(self, fw_name, hash_extension='.hash'):
690        """Returns chip firmware hash blob.
691
692        For a given chip type, returns the chip firmware hash blob.
693        Before making this request, the chip blobs must have been
694        extracted from cbfs using cbfs_extract_chip().
695        The hash data is returned as a list of stringified two-byte pieces:
696        \x12\x34...\xab\xcd\xef -> ['0x12', '0x34', ..., '0xab', '0xcd', '0xef']
697
698        @param fw_name: Chip firmware name whose hash blob to get.
699        @return: Boolean success status.
700        @raise error.CmdError: Underlying remote shell operations failed.
701        """
702        fw_path = os.path.join(self._cbfs_work_path, fw_name)
703        hexdump_cmd = '%s %s%s' % (self.HEXDUMP, fw_path, hash_extension)
704        hashblob = self.os_if.run_shell_command_get_output(hexdump_cmd)
705        return hashblob
706
707    def cbfs_remove(self, filename, extension, regions=('a', 'b')):
708        """Remove the given binary from CBFS, in FW_MAIN_A/FW_MAIN_B
709
710        @param filename: Name within cbfs of the file, without extension
711        @param extension: Extension of the name of the cbfs component.
712        @param regions: tuple of regions to act on (full name, or 'A' or 'B')
713        @return: Boolean success status.
714        @raise error.CmdError: If underlying remote shell operations failed.
715        """
716        regions = self._cbfs_regions(regions)
717
718        bios = os.path.join(self._cbfs_work_path, self._bios_path)
719        rm_cmd = '%s %s remove -r %s -n %s%s' % (
720                self.CBFSTOOL, bios, ','.join(regions), filename, extension)
721
722        self.os_if.run_shell_command(rm_cmd)
723        return True
724
725    def cbfs_add(self,
726                 filename,
727                 extension,
728                 regions=('a', 'b'),
729                 local_filename=None):
730        """Add the given binary to CBFS, in the specified regions
731
732        If extension is .hash, the compression is assumed to be none.
733        For any other extension, it's assumed to be lzma.
734
735        @param filename: Name within cbfs of the file, without extension
736        @param extension: Extension of the name of the cbfs component.
737        @param regions: tuple of regions to act on (full name, or 'A' or 'B')
738        @param local_filename
739        @return: Boolean success status.
740        @raise error.CmdError: If underlying remote shell operations failed.
741        """
742        regions = self._cbfs_regions(regions)
743
744        if extension == '.hash':
745            compression = 'none'
746        else:
747            compression = 'lzma'
748
749        if local_filename is None:
750            local_filename = os.path.join(self._cbfs_work_path,
751                                          filename + extension)
752
753        bios = os.path.join(self._cbfs_work_path, self._bios_path)
754        add_cmd = '%s %s add -r %s -t raw -c %s -n %s%s -f %s' % (
755                self.CBFSTOOL, bios, ','.join(regions), compression, filename,
756                extension, local_filename)
757
758        self.os_if.run_shell_command(add_cmd)
759        return True
760
761    def cbfs_replace_chip(self,
762                          fw_name,
763                          extension='.bin',
764                          hash_extension='.hash',
765                          regions=('a', 'b')):
766        """Replaces chip firmware and its hash in CBFS (bios.bin).
767
768        For a given chip type, replaces its firmware blob and hash in
769        bios.bin.  All files referenced are expected to be in the
770        directory set up using cbfs_setup_work_dir().
771
772        @param cbfs_filename: Name within cbfs of the file, without extension
773        @param extension: Extension of the name of the cbfs component.
774        @param regions: tuple of regions to act on (full name, or 'A' or 'B')
775        @return: Boolean success status.
776        @raise error.CmdError: If underlying remote shell operations failed.
777        """
778        regions = self._cbfs_regions(regions)
779        self.cbfs_expand(regions)
780        if hash_extension is not None and hash_extension != extension:
781            self.cbfs_remove(fw_name, hash_extension, regions)
782        self.cbfs_remove(fw_name, extension, regions)
783        if hash_extension is not None and hash_extension != extension:
784            self.cbfs_add(fw_name, hash_extension, regions)
785        self.cbfs_add(fw_name, extension, regions)
786        self.cbfs_truncate(regions)
787        return True
788
789    def cbfs_replace_diagnostics(self, diag_name, local_path):
790        """Runs cbfstool to replace a diagnostics image in the firmware image.
791
792        @param diag_name: Name of the diagnostics image in CBFS
793        @param local_path: Filename for storing the diagnostics image in the
794            CBFS working directory
795        """
796        regions = ['RW_LEGACY']
797        self.cbfs_expand(regions)
798        self.cbfs_remove(diag_name, '', regions)
799        self.cbfs_add(diag_name, '', regions, local_path)
800        self.cbfs_truncate(regions)
801
802    def cbfs_sign_and_flash(self):
803        """Signs CBFS (bios.bin) and flashes it."""
804        self.resign_firmware(work_path=self._cbfs_work_path)
805        bios = self._get_handler('bios')
806        bios_file = os.path.join(self._cbfs_work_path, self._bios_path)
807        bios.new_image(bios_file)
808        # futility makes sure to preserve important sections (HWID, GBB, VPD).
809        self.os_if.run_shell_command_get_result(
810                'futility update --mode=recovery -i %s' % bios_file)
811        return True
812
813    def copy_bios(self, filename):
814        """Copy the shellball BIOS to the given name in the temp dir
815
816        @param filename: the filename to use for the copy
817        @return: the full path of the BIOS
818
819        @type filename: str
820        @rtype: str
821        """
822        if not isinstance(filename, six.string_types):
823            raise FirmwareUpdaterError("Filename must be a string: %s" %
824                                       repr(filename))
825        src_bios = os.path.join(self._work_path, self._bios_path)
826        dst_bios = os.path.join(self._temp_path, filename)
827        self.os_if.copy_file(src_bios, dst_bios)
828        return dst_bios
829
830    def get_temp_path(self):
831        """Get temp directory path."""
832        return self._temp_path
833
834    def get_keys_path(self):
835        """Get keys directory path."""
836        return self._keys_path
837
838    def get_work_path(self):
839        """Get work directory path."""
840        return self._work_path
841
842    def get_bios_relative_path(self):
843        """Gets the relative path of the bios image in the shellball."""
844        return self._bios_path
845
846    def get_ec_relative_path(self):
847        """Gets the relative path of the ec image in the shellball."""
848        return self._ec_path
849
850    def get_image_gbb_flags(self, filename=None):
851        """Get the GBB flags in the given image (shellball image if unspecified)
852
853        @param filename: the image path to act on (None to use shellball image)
854        @return: An integer of the GBB flags.
855        """
856        if filename:
857            filename = os.path.join(self._temp_path, filename)
858            handler = self._create_handler('bios', 'image')
859            handler.new_image(filename)
860        else:
861            handler = self._get_handler('bios')
862        return handler.get_gbb_flags()
863
864    def set_image_gbb_flags(self, flags, filename=None):
865        """Set the GBB flags in the given image (shellball image if unspecified)
866
867        @param flags: the flags to set
868        @param filename: the image path to act on (None to use shellball image)
869
870        @type flags: int
871        @type filename: str | None
872        """
873        if filename:
874            filename = os.path.join(self._temp_path, filename)
875            handler = self._create_handler('bios', 'image')
876            handler.new_image(filename)
877        else:
878            filename = self._get_image_path('bios')
879            handler = self._get_handler('bios')
880        handler.set_gbb_flags(flags)
881        handler.dump_whole(filename)
882