1# Copyright 2020 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 six
7
8from autotest_lib.client.common_lib import error
9from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
10from autotest_lib.server.cros.power import utils as PowerUtils
11
12
13class firmware_FWupdateThenSleep(FirmwareTest):
14    """Firmware update using chromeos-firmwareupdate --mode=recovery, then
15    sleep and wake, then make sure the host responds and the flash contents
16    don't change.
17    """
18    MODE = 'recovery'
19    MIN_BATTERY_LEVEL = 20
20
21    def initialize(self, host, cmdline_args, battery_only=False):
22
23        self.flashed = False
24        self._want_restore = None
25        self._original_sw_wp = {}
26        self._original_hw_wp = None
27
28        super(firmware_FWupdateThenSleep, self).initialize(host, cmdline_args)
29
30        ac_online = self._client.is_ac_connected()
31        battery_percentage = self._client.get_battery_display_percentage()
32        self.have_power_control = False
33        if battery_only:
34            if not self._client.has_battery():
35                raise error.TestNAError("DUT type does not have a battery.")
36
37            if self.servo.supports_built_in_pd_control():
38                self.have_power_control = True
39                PowerUtils.put_host_battery_in_range(self._client,
40                                                     min_level=25,
41                                                     max_level=100,
42                                                     timeout=600)
43            elif ac_online:
44                raise error.TestNAError(
45                    "For this version of the test, the DUT power supply must"
46                    " be unplugged, or connected through Servo V4 Type-C.")
47            elif battery_percentage < self.MIN_BATTERY_LEVEL:
48                raise error.TestError(
49                    "Battery level is too low (%s%%).  Please charge the DUT "
50                    "to at least %s%%, then try again."
51                    % (battery_percentage, self.MIN_BATTERY_LEVEL))
52            else:
53                logging.info("On battery: %s%% charge", battery_percentage)
54
55        self._original_sw_wp = self.faft_client.bios.get_write_protect_status()
56        self._original_hw_wp = 'on' in self.servo.get('fw_wp_state')
57
58        self.backup_firmware()
59        self.setup_firmwareupdate_shellball()
60
61        if self.faft_config.ap_access_ec_flash:
62            self._setup_ec_write_protect(False)
63        self.set_ap_write_protect_and_reboot(False)
64        self.faft_client.bios.set_write_protect_range(0, 0, False)
65
66        if battery_only and self.have_power_control:
67            self.set_servo_v4_role_to_snk()
68
69    def cleanup(self):
70        """Restore the original firmware and original write-protect."""
71        self._restore_servo_v4_role()
72
73        self.set_ap_write_protect_and_reboot(False)
74        try:
75            if self.flashed and self.is_firmware_saved():
76                self.restore_firmware()
77        except (EnvironmentError, six.moves.xmlrpc_client.Fault,
78                error.AutoservError, error.TestBaseException):
79            logging.error("Problem restoring firmware:", exc_info=True)
80
81        try:
82            # Restore the old write-protection value at the end of the test.
83            if self._original_sw_wp:
84                self.faft_client.bios.set_write_protect_range(
85                        self._original_sw_wp['start'],
86                        self._original_sw_wp['length'],
87                        self._original_sw_wp['enabled'])
88        except (EnvironmentError, six.moves.xmlrpc_client.Fault,
89                error.AutoservError, error.TestBaseException):
90            logging.error("Problem restoring SW write-protect:", exc_info=True)
91
92        if self._original_hw_wp is not None:
93            self.set_ap_write_protect_and_reboot(self._original_hw_wp)
94
95        super(firmware_FWupdateThenSleep, self).cleanup()
96
97    def get_installed_versions(self):
98        """Get the installed versions of BIOS and EC firmware.
99
100        @return: A nested dict keyed by target ('bios' or 'ec') and then section
101        @rtype: dict
102        """
103        versions = dict()
104        versions['bios'] = self.faft_client.updater.get_device_fwids('bios')
105        if self.faft_config.chrome_ec:
106            versions['ec'] = self.faft_client.updater.get_device_fwids('ec')
107        return versions
108
109    def apply_update(self, append, before_fwids, image_fwids, wp=0):
110        """Run chromeos-firmwareupdate with given sub-case
111
112        @param append: additional piece to add to shellball name
113        @param wp: is the flash write protected (--wp)?
114        @return: a list of failure messages for the case
115        """
116        options = ['--wp=%s' % wp, '--quirks=ec_partial_recovery=0']
117
118        if append:
119            cmd_desc = ['chromeos-firmwareupdate-%s' % append]
120        else:
121            cmd_desc = ['chromeos-firmwareupdate']
122        cmd_desc += ['--mode=%s' % self.MODE]
123        cmd_desc += options
124        cmd_desc = ' '.join(cmd_desc)
125
126        expected_written = {}
127
128        if wp:
129            bios_written = ['a', 'b']
130            ec_written = []  # EC write is all-or-nothing
131        else:
132            bios_written = ['ro', 'a', 'b']
133            ec_written = ['ro', 'rw']
134
135        expected_written['bios'] = bios_written
136
137        if self.faft_config.chrome_ec and ec_written:
138            expected_written['ec'] = ec_written
139
140        # remove quotes and braces: bios: [a, b], ec: [ro, rw]
141        written_desc = repr(expected_written).replace("'", "")[1:-1]
142        logging.debug('Before(%s): %s', append or '', before_fwids)
143        logging.debug('Image(%s):  %s', append or '', image_fwids)
144        logging.info("Run %s (should write %s)", cmd_desc, written_desc)
145
146        # make sure we restore firmware after the test, if it tried to flash.
147        self.flashed = True
148
149        errors = []
150        result = self.run_chromeos_firmwareupdate(
151                self.MODE, append, options, ignore_status=True)
152
153        if result.exit_status == 255:
154            logging.warning("DUT network dropped during update.")
155        elif result.exit_status != 0:
156            if (image_fwids == before_fwids and
157                    'Good. It seems nothing was changed.' in result.stdout):
158                logging.info("DUT already matched the image; updater aborted.")
159            else:
160                errors.append('...updater: unexpectedly failed (rc=%s)' %
161                              result.exit_status)
162
163        after_fwids = self.get_installed_versions()
164        logging.debug('After(%s):  %s', append or '', after_fwids)
165
166        errors += self.check_fwids_written(
167                before_fwids, image_fwids, after_fwids, expected_written)
168
169        if errors:
170            logging.debug('%s', '\n'.join(errors))
171            return ["%s (should write %s)\n%s"
172                    % (cmd_desc, written_desc, '\n'.join(errors))]
173        return []
174
175    def run_once(self, wp=0, battery_only=False):
176        """Try a firmware update, then put the machine in sleep and wake it."""
177
178        errors = []
179        original_boot_id = self._client.get_boot_id()
180
181        have_ec = bool(self.faft_config.chrome_ec)
182        before_fwids = self.get_installed_versions()
183
184        self.modify_shellball('mod', modify_ro=True, modify_ec=have_ec)
185        modded_fwids = self.identify_shellball(include_ec=have_ec)
186
187        # Check power again, in case it got reconnected during setup.
188        if battery_only and self._client.is_ac_connected():
189            if self.have_power_control:
190                raise error.TestError(
191                    "DUT is on AC power, even though Servo role was set to"
192                    " sink.  Is a second power supply connected?")
193            else:
194                raise error.TestError(
195                    "DUT is back on AC power, when it should be on battery."
196                    "  Did it get plugged back in?")
197
198        errors += self.apply_update('mod', before_fwids, modded_fwids, wp=wp)
199        if errors:
200            fail_msg = "Problem(s) occurred during flash, before sleep:"
201            errors.insert(0, fail_msg)
202            raise error.TestFail('\n'.join(errors))
203
204        logging.info("Firmware flashed successfully; trying sleep.")
205        before_sleep_fwids = self.get_installed_versions()
206        self.suspend()
207        if self.faft_config.chrome_ec:
208            if not self.wait_power_state(
209                    self.POWER_STATE_SUSPEND, self.DEFAULT_PWR_RETRIES):
210                raise error.TestFail('Platform failed to reach a suspend state'
211                                     ' after flashing firmware.')
212        else:
213            self.switcher.wait_for_client_offline(orig_boot_id=original_boot_id)
214        logging.info("System should now be in sleep; trying to wake.")
215
216        self.servo.power_normal_press()
217        self.switcher.wait_for_client()
218
219        after_sleep_fwids = self.get_installed_versions()
220        logging.debug('After sleep+wake:  %s', after_sleep_fwids)
221
222        if have_ec:
223            expected_changes = {'bios': None, 'ec': None}
224        else:
225            expected_changes = {'bios': None}
226
227        after_sleep_errors = self.check_fwids_written(
228                before_sleep_fwids, None, after_sleep_fwids, expected_changes)
229        if after_sleep_errors:
230            errors += ['After sleep, FWIDs unexpectedly differ from'
231                       ' what was written:'] + after_sleep_errors
232
233        if errors:
234            fail_msg = "Problem(s) occurred during flash + sleep + wake:"
235            errors.insert(0, fail_msg)
236            raise error.TestFail('\n'.join(errors))
237