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