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