• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7See FirmwareUpdater object below.
8"""
9
10import os
11import re
12
13from autotest_lib.client.common_lib.cros import chip_utils
14from autotest_lib.client.cros.faft.utils import (common,
15                                                 flashrom_handler,
16                                                 saft_flashrom_util,
17                                                 shell_wrapper)
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 /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