xref: /aosp_15_r20/external/autotest/server/site_tests/firmware_FWupdate/firmware_FWupdate.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Copyright 2015 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
5import logging
6import os
7import six
8
9from autotest_lib.client.common_lib import error
10from autotest_lib.client.common_lib import utils
11from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
12
13
14class firmware_FWupdate(FirmwareTest):
15    """RO+RW firmware update using chromeos-firmware with various modes.
16    If restore=false is given, the DUT is left running that firmware, so the
17    test can be used to apply updates.
18
19    Accepted --args names:
20    old_bios=
21    old_bios_ro=
22    old_bios_rw=
23    old_ec=
24    old_pd=
25    new_bios=
26    new_bios_ro=
27    new_bios_rw=
28    new_ec=
29    new_pd=
30        apply the given image(s)
31    restore=false|true (default true)
32        if value is anything but 'false', once the test ends, the firmware will
33        be restored to the backup that was made at the start of the test.
34    """
35    # Region to use for flashrom wp-region commands
36    WP_REGION = 'WP_RO'
37
38    MODE = 'recovery'
39
40    def initialize(self, host, cmdline_args):
41
42        self.flashed = False
43        self._want_restore = None
44        self._orig_sw_wp = {}
45        self._orig_hw_wp = None
46
47        dict_args = utils.args_to_dict(cmdline_args)
48
49        if dict_args.get('restore', '').lower() == 'false':
50            self._want_restore = False
51        else:
52            self._want_restore = True
53
54        self.images = {}
55
56        for old_or_new in ('old', 'new'):
57            for target in ('bios', 'bios_ro', 'bios_rw', 'ec', 'pd'):
58                arg_name = '%s_%s' % (old_or_new, target)
59                arg_value = dict_args.get(arg_name)
60                if arg_value:
61                    logging.info('%s=%s', arg_name, arg_value)
62                    image_path = os.path.expanduser(arg_value)
63                    if not os.path.isabs(image_path):
64                        raise error.TestError(
65                            'Specified path must be absolute: %s=%s'
66                            % (arg_name, arg_value))
67                    if not os.path.isfile(image_path):
68                        raise error.TestError(
69                            'Specified file does not exist: %s=%s'
70                            % (arg_name, arg_value))
71                    self.images[arg_name] = image_path
72
73        self.old_bios = self.images.get('old_bios')
74        self.old_bios_ro = self.images.get('old_bios_ro')
75        self.old_bios_rw = self.images.get('old_bios_rw')
76
77        self.new_bios = self.images.get('new_bios')
78        self.new_bios_ro = self.images.get('new_bios_ro')
79        self.new_bios_rw = self.images.get('new_bios_rw')
80
81        if not (self.new_bios or self.new_bios_rw):
82            raise error.TestError('Must specify at least new_bios=<path>'
83                                  ' or new_bios_rw=<path>')
84
85        super(firmware_FWupdate, self).initialize(host, cmdline_args)
86
87        self._orig_sw_wp = self.faft_client.bios.get_write_protect_status()
88
89        self.backup_firmware()
90
91        if self.faft_config.ap_access_ec_flash:
92            self._setup_ec_write_protect(False)
93
94        self._original_hw_wp = 'on' in self.servo.get('fw_wp_state')
95
96        self.set_ap_write_protect_and_reboot(False)
97        self.faft_client.bios.set_write_protect_region(self.WP_REGION, True)
98        self.set_ap_write_protect_and_reboot(True)
99
100    def cleanup(self):
101        """Restore write protection, unless "restore" was false."""
102        if not hasattr(self, 'run_id'):
103            # Exited very early during initialize, so no cleanup needed
104            return
105
106        self.set_ap_write_protect_and_reboot(False)
107        try:
108            if self.flashed and self._want_restore and self.is_firmware_saved():
109                self.restore_firmware()
110        except (EnvironmentError, six.moves.xmlrpc_client.Fault,
111                error.AutoservError, error.TestBaseException):
112            logging.error("Problem restoring firmware:", exc_info=True)
113
114        try:
115            # Restore the old write-protection value at the end of the test.
116            if self._orig_sw_wp:
117                self.faft_client.bios.set_write_protect_range(
118                        self._orig_sw_wp['start'],
119                        self._orig_sw_wp['length'],
120                        self._orig_sw_wp['enabled'])
121        except (EnvironmentError, six.moves.xmlrpc_client.Fault,
122                error.AutoservError, error.TestBaseException):
123            logging.error("Problem restoring software write-protect:",
124                          exc_info=True)
125
126        if self._orig_hw_wp is not None:
127            self.set_ap_write_protect_and_reboot(self._orig_hw_wp)
128        elif hasattr(self, 'ec'):
129            self.sync_and_ec_reboot()
130
131        super(firmware_FWupdate, self).cleanup()
132
133    def get_installed_versions(self):
134        """Get the installed versions of BIOS and EC firmware.
135
136        @return: A nested dict keyed by target ('bios' or 'ec') and then section
137        @rtype: dict
138        """
139        versions = dict()
140        versions['bios'] = self.faft_client.updater.get_device_fwids('bios')
141        if self.faft_config.chrome_ec:
142            versions['ec'] = self.faft_client.updater.get_device_fwids('ec')
143        return versions
144
145    def check_bios_specified(self,
146            old_ro=False, old_rw=False, new_ro=False, new_rw=False):
147        """Check if the required --args were specified.
148
149        @raise error.TestError: if args required for the test were not given
150        """
151        missing = set()
152        if old_ro and not (self.old_bios_ro or self.old_bios):
153            missing.add('old_bios[_ro]=')
154        if old_rw and not (self.old_bios_rw or self.old_bios):
155            missing.add('old_bios[_rw]=')
156        if new_ro and not (self.new_bios_ro or self.new_bios):
157            missing.add('new_bios[_ro]=')
158        if new_rw and not (self.new_bios_rw or self.new_bios):
159            missing.add('new_bios[_rw]=')
160        if missing:
161            raise error.TestError('Must specify args: %s' % '; '.join(missing))
162
163    def copy_cmdline_images(self, old_or_new, section=None):
164        """Copy the specified command line images into the extracted shellball.
165
166        @param old_or_new: 'old' or 'new', to select a set from self.images
167        @param section: 'ro' or 'rw', to use bios_ro or bios_rw.
168        """
169        local_bios = (
170            self.images.get('%s_bios_%s' % (old_or_new, section)) or
171            self.images.get('%s_bios' % old_or_new)
172        )
173        local_ec = self.images.get('%s_ec' % old_or_new)
174        local_pd = self.images.get('%s_pd' % old_or_new)
175
176        extract_dir = self.faft_client.updater.get_work_path()
177
178        if local_bios:
179            bios_rel = self.faft_client.updater.get_bios_relative_path()
180            remote_bios = os.path.join(extract_dir, bios_rel)
181            self._client.send_file(local_bios, remote_bios)
182
183        if local_ec:
184            ec_rel = self.faft_client.updater.get_ec_relative_path()
185            remote_ec = os.path.join(extract_dir, ec_rel)
186            self._client.send_file(local_ec, remote_ec)
187
188        if local_pd:
189            # note: pd.bin might likewise need special path logic
190            remote_pd = os.path.join(extract_dir, 'pd.bin')
191            self._client.send_file(local_pd, remote_pd)
192
193    def prepare_shellball(self, old_or_new, ro_or_rw=None):
194        """Prepare a shellball with the given set of images (old or new).
195
196        @param old_or_new: 'old' or 'new', to select a set from self.images
197        @param section: 'ro' or 'rw', to use bios_ro or bios_rw.
198        """
199        self.faft_client.updater.reset_shellball()
200        self.copy_cmdline_images(old_or_new, ro_or_rw)
201        self.faft_client.updater.reload_images()
202        self.faft_client.updater.repack_shellball(old_or_new)
203
204    def run_shellball(self, append, wp, host_only=False):
205        """Run chromeos-firmwareupdate with given sub-case
206
207        @param append: additional piece to add to shellball name
208        @param wp: is the flash write protected (--wp)?
209        @return: a list of failure messages for the case
210        """
211        have_ec = bool(self.faft_config.chrome_ec)
212        options = []
213
214        if host_only:
215            options += ['--host_only']
216        options += ['--quirks=ec_partial_recovery=0']
217
218        before_fwids = self.get_installed_versions()
219        image_fwids = self.identify_shellball(include_ec=have_ec)
220
221        # Unlock the protection of the wp-enable and wp-range registers
222        self.set_ap_write_protect_and_reboot(False)
223
224        if wp:
225            self.faft_client.bios.set_write_protect_region(
226                    self.WP_REGION, True)
227            self.set_ap_write_protect_and_reboot(True)
228        else:
229            self.faft_client.bios.set_write_protect_region(
230                    self.WP_REGION, False)
231
232        cmd_desc = ['chromeos-firmwareupdate-%s' % append,
233                    '--mode=%s' % self.MODE]
234        cmd_desc += options
235        cmd_desc += ['[wp=%s]' % wp]
236        cmd_desc = ' '.join(cmd_desc)
237
238        expected_written = {}
239
240        if wp:
241            bios_written = ['a', 'b']
242            ec_written = []  # EC write is all-or-nothing
243
244        elif host_only:
245            bios_written = ['ro', 'a', 'b']
246            ec_written = []
247
248        else:
249            bios_written = ['ro', 'a', 'b']
250            ec_written = ['ro', 'rw']
251
252        expected_written['bios'] = bios_written
253
254        if self.faft_config.chrome_ec and ec_written:
255            expected_written['ec'] = ec_written
256
257        # remove quotes and braces: bios: [a, b], ec: [ro, rw]
258        written_desc = repr(expected_written).replace("'", "")[1:-1]
259        logging.debug('Before(%s): %s', append, before_fwids)
260        logging.debug('Image(%s):  %s', append, image_fwids)
261        logging.info("Run %s (should write %s)", cmd_desc, written_desc)
262
263        # make sure we restore firmware after the test, if it tried to flash.
264        self.flashed = True
265
266        errors = []
267        result = self.run_chromeos_firmwareupdate(
268                self.MODE, append, options, ignore_status=True)
269
270        if result.exit_status == 255:
271            logging.warning("DUT network dropped during update.")
272        elif result.exit_status != 0:
273            if (image_fwids == before_fwids and
274                    'Good. It seems nothing was changed.' in result.stdout):
275                logging.info("DUT already matched the image; updater aborted.")
276            else:
277                errors.append('...updater: unexpectedly failed (rc=%s)' %
278                              result.exit_status)
279
280        after_fwids = self.get_installed_versions()
281        logging.debug('After(%s):  %s', append, after_fwids)
282
283        errors += self.check_fwids_written(
284                before_fwids, image_fwids, after_fwids, expected_written)
285
286        if errors:
287            logging.debug('%s', '\n'.join(errors))
288            return ["%s (should write %s)\n%s"
289                    % (cmd_desc, written_desc, '\n'.join(errors))]
290        return []
291
292    def test_upgrade_rw(self, raise_error=True):
293        """Test case: RO=old, RW=new"""
294        logging.info('%s', self.test_upgrade_rw.__doc__)
295        self.check_bios_specified(old_ro=True, new_rw=True)
296
297        errors = []
298
299        # wp=0: update RO+RW
300        self.prepare_shellball('old', 'ro')
301        errors += self.run_shellball('old', wp=0)
302
303        # wp=1: update RW
304        self.prepare_shellball('new', 'rw')
305        errors += self.run_shellball('new', wp=1)
306
307        self.reboot_and_reset_tpm()
308        self.sync_and_ec_reboot()
309        self.switcher.wait_for_client()
310
311        if errors:
312            fail_msg = "After flashing new RW over old RO, FWIDs were wrong."
313            errors.insert(0, fail_msg)
314            if raise_error:
315                raise error.TestFail('\n'.join(errors))
316            return ['\n'.join(errors)]
317        return []
318
319    def test_downgrade_rw(self, raise_error=True):
320        """Test case: RO=old, RW=old->new->old (with reboots)"""
321        logging.info('%s', self.test_downgrade_rw.__doc__)
322        self.check_bios_specified(old_ro=True, old_rw=True, new_rw=True)
323        errors = []
324
325        # wp=0: update RO+RW
326        self.prepare_shellball('old', 'ro')
327        errors += self.run_shellball('old', wp=0)
328
329        self.reboot_and_reset_tpm()
330        self.sync_and_ec_reboot()
331        self.switcher.wait_for_client()
332
333        self.prepare_shellball('new', 'rw')
334        errors += self.run_shellball('new', wp=1)
335
336        self.sync_and_ec_reboot()
337        self.switcher.wait_for_client()
338
339        # Downgrade BIOS RW, but leave EC/PD at newer firmware
340        self.prepare_shellball('old', 'rw')
341        errors += self.run_shellball('old', wp=1, host_only=True)
342
343        self.reboot_and_reset_tpm()
344        self.sync_and_ec_reboot()
345        self.switcher.wait_for_client()
346
347        if errors:
348            fail_msg = "After upgrading then downgrading RW, FWIDs were wrong."
349            errors.insert(0, fail_msg)
350            if raise_error:
351                raise error.TestFail('\n'.join(errors))
352            return ['\n'.join(errors)]
353        return []
354
355    def test_new(self, raise_error=True):
356        """Test case: RO=new, RW=new"""
357        logging.info('%s', self.test_new.__doc__)
358        self.check_bios_specified(new_ro=True, old_rw=True)
359
360        errors = []
361
362        # wp=0: update RO+RW
363        self.prepare_shellball('new', 'ro')
364        errors += self.run_shellball('new', wp=0)
365
366        self.reboot_and_reset_tpm()
367        self.sync_and_ec_reboot()
368        self.switcher.wait_for_client()
369
370        if errors:
371            fail_msg = "After flashing new RO+RW, FWIDs were wrong."
372            errors.insert(0, fail_msg)
373            if raise_error:
374                raise error.TestFail('\n'.join(errors))
375            return ['\n'.join(errors)]
376        return []
377
378    def test_old(self, raise_error=True):
379        """Test case: RO=old, RW=old"""
380        logging.info('%s', self.test_old.__doc__)
381        self.check_bios_specified(old_ro=True, old_rw=True)
382
383        errors = []
384
385        # wp=0: update RO+RW
386        self.prepare_shellball('old', 'ro')
387        errors += self.run_shellball('old', wp=0)
388
389        self.reboot_and_reset_tpm()
390        self.sync_and_ec_reboot()
391        self.switcher.wait_for_client()
392
393        if errors:
394            fail_msg = "After flashing old RO+RW, FWIDs were wrong."
395            errors.insert(0, fail_msg)
396            if raise_error:
397                raise error.TestFail('\n'.join(errors))
398            return ['\n'.join(errors)]
399        return []
400
401    def test(self):
402        """Run all the test_* cases"""
403        self.check_bios_specified(old_ro=True, old_rw=True, new_rw=True)
404
405        errors = []
406        errors += self.test_old(raise_error=False)
407        errors += self.test_downgrade_rw(raise_error=False)
408        errors += self.test_upgrade_rw(raise_error=False)
409        if self.new_bios or (self.new_bios_ro and self.new_bios_rw):
410            errors += self.test_new(raise_error=False)
411        else:
412            logging.warning("No 'new_bios_ro' given, skipping: %s",
413                         self.test_new.__doc__)
414        if errors:
415            if len(errors) > 1:
416                errors.insert(0, "%s RO+RW combinations failed." % len(errors))
417            raise error.TestFail('\n'.join(errors))
418