firmware_Cr50Update.py revision c6193d70a7997348b864841099b0e291c53d2eff
1# Copyright 2017 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
7
8from autotest_lib.client.common_lib import error
9from autotest_lib.client.common_lib.cros import cr50_utils, tpm_utils
10from autotest_lib.server.cros import debugd_dev_tools
11from autotest_lib.server.cros.faft.cr50_test import Cr50Test
12
13
14class firmware_Cr50Update(Cr50Test):
15    """
16    Verify a dut can update to the given image.
17
18    Copy the new image onto the device and clear the update state to force
19    cr50-update to run. The test will fail if Cr50 does not update or if the
20    update script encounters any errors.
21
22    @param image: the location of the update image
23    @param image_type: string representing the image type. If it is "dev" then
24                       don't check the RO versions when comparing versions.
25    """
26    version = 1
27    UPDATE_TIMEOUT = 20
28    ERASE_NVMEM = "erase_nvmem"
29
30    DEV_NAME = "dev_image"
31    OLD_RELEASE_NAME = "old_release_image"
32    RELEASE_NAME = "release_image"
33    ORIGINAL_NAME = "original_image"
34    RESTORE_ORIGINAL_TRIES = 3
35    SUCCESS = 0
36    UPDATE_OK = 1
37
38
39    def initialize(self, host, cmdline_args, release_path="", release_ver="",
40                   old_release_path="", old_release_ver="", dev_path="",
41                   test=""):
42        """Initialize servo and process the given images"""
43        self.processed_images = False
44
45        super(firmware_Cr50Update, self).initialize(host, cmdline_args)
46        if not hasattr(self, "cr50"):
47            raise error.TestNAError('Test can only be run on devices with '
48                                    'access to the Cr50 console')
49
50        if not release_ver and not os.path.isfile(release_path):
51            raise error.TestError('Need to specify a release version or path')
52
53        self.devid = self.servo.get('cr50_devid')
54
55        # Make sure ccd is disabled so it won't interfere with the update
56        self.cr50.ccd_disable()
57
58        tpm_utils.ClearTPMOwnerRequest(host)
59        self.rootfs_tool = debugd_dev_tools.RootfsVerificationTool()
60        self.rootfs_tool.initialize(host)
61        if not self.rootfs_tool.is_enabled():
62            logging.debug('Removing rootfs verification.')
63            # 'enable' actually disables rootfs verification
64            self.rootfs_tool.enable()
65
66        self.host = host
67        self.erase_nvmem = test.lower() == self.ERASE_NVMEM
68
69        # A dict used to store relevant information for each image
70        self.images = {}
71
72        # Get the original image from the cr50 firmware directory on the dut
73        self.save_original_image(cr50_utils.CR50_FILE)
74
75        # Process the given images in order of oldest to newest. Get the version
76        # info and add them to the update order
77        self.update_order = []
78        if not self.erase_nvmem and (old_release_path or old_release_ver):
79            self.add_image_to_update_order(self.OLD_RELEASE_NAME,
80                                           old_release_path, old_release_ver)
81        self.add_image_to_update_order(self.RELEASE_NAME, release_path,
82                                       release_ver)
83        self.add_image_to_update_order(self.DEV_NAME, dev_path)
84        self.verify_update_order()
85        self.processed_images = True
86        logging.info("Update %s", self.update_order)
87
88        # Update to the dev image
89        self.run_update(self.DEV_NAME)
90
91
92    def restore_original_image(self):
93        """Update to the image that was running at the start of the test.
94
95        Returns SUCCESS if the update was successful or the update error if it
96        failed.
97        """
98        rv = self.SUCCESS
99
100        original_ver, _, original_path = self.images[self.ORIGINAL_NAME]
101        original_rw = original_ver[1]
102        cr50_utils.InstallImage(self.host, original_path)
103
104        _, running_rw, is_dev = self.cr50.get_active_version_info()
105        new_rw = cr50_utils.GetNewestVersion(running_rw, original_rw)
106
107        # If Cr50 is running the original image, then no update is needed.
108        if new_rw is None:
109            return rv
110
111        try:
112            # If a rollback is needed, update to the dev image so it can
113            # rollback to the original image.
114            if new_rw != original_rw and not is_dev:
115                logging.info("Updating to dev image to enable rollback")
116                self.cr50_update(self.images[self.DEV_NAME][2])
117
118            logging.info("Updating to the original image %s",
119                         original_rw)
120            self.cr50_update(original_path, rollback=True)
121        except Exception, e:
122            logging.info("cleanup update from %s to %s failed", running_rw,
123                          original_rw)
124            logging.debug(e)
125            rv = e
126        self.cr50.ccd_enable()
127        return rv
128
129
130    def cleanup(self):
131        """Update Cr50 to the image it was running at the start of the test"""
132        logging.warning('rootfs verification is disabled')
133
134        # Make sure keepalive is disabled
135        self.cr50.ccd_enable()
136        self.cr50.send_command("ccd keepalive disable")
137
138        # Restore the original Cr50 image
139        if self.processed_images:
140            for i in xrange(self.RESTORE_ORIGINAL_TRIES):
141                if self.restore_original_image() == self.SUCCESS:
142                    logging.info("Successfully restored the original image")
143                    break
144            else:
145                raise error.TestError("Could not restore the original image")
146
147        # Running usb_update commands stops trunksd. Reboot the device to reset
148        # it
149        self.host.run('reboot')
150        super(firmware_Cr50Update, self).cleanup()
151
152
153    def run_update(self, image_name, use_usb_update=False):
154        """Copy the image to the DUT and upate to it.
155
156        Normal updates will use the cr50-update script to update. If a rollback
157        is True, use usb_update to flash the image and then use the 'rw'
158        commands to force a rollback.
159
160        @param image_name: the key in the images dict that can be used to
161                           retrieve the image info.
162        @param use_usb_update: True if usb_updater should be used directly
163                               instead of running the update script.
164        """
165        self.cr50.ccd_disable()
166        # Get the current update information
167        image_ver, image_ver_str, image_path = self.images[image_name]
168
169        dest, ver = cr50_utils.InstallImage(self.host, image_path)
170        assert ver == image_ver, "Install failed"
171        image_rw = image_ver[1]
172
173        running_ver = cr50_utils.GetRunningVersion(self.host)
174        running_ver_str = cr50_utils.GetVersionString(running_ver)
175
176        # If the given image is older than the running one, then we will need
177        # to do a rollback to complete the update.
178        rollback = (cr50_utils.GetNewestVersion(running_ver[1], image_rw) !=
179                    image_rw)
180        logging.info("Attempting %s from %s to %s",
181                     "rollback" if rollback else "update", running_ver_str,
182                     image_ver_str)
183
184        # If a rollback is needed, flash the image into the inactive partition,
185        # on or use usb_update to update to the new image if it is requested.
186        if use_usb_update or rollback:
187            self.cr50_update(dest, rollback=rollback,
188                             erase_nvmem=self.erase_nvmem)
189            self.check_state((self.checkers.crossystem_checker,
190                              {'mainfw_type': 'normal'}))
191
192        # Running the usb update or rollback will enable ccd. Disable it again.
193        self.cr50.ccd_disable()
194
195        # Get the last cr50 update related message from /var/log/messages
196        last_message = cr50_utils.CheckForFailures(self.host, '')
197
198        # Clear the update state and reboot, so cr50-update will run again.
199        cr50_utils.ClearUpdateStateAndReboot(self.host)
200
201        # The cr50 updates happen over /dev/tpm0. It takes a while. After
202        # cr50-update has finished, cr50 should reboot. Wait until this happens
203        # before sending anymore commands.
204        self.cr50.wait_for_reboot()
205
206        # Verify the system boots normally after the update
207        self.check_state((self.checkers.crossystem_checker,
208                          {'mainfw_type': 'normal'}))
209
210        # Verify the version has been updated and that there have been no
211        # unexpected usb_updater exit codes.
212        cr50_utils.VerifyUpdate(self.host, image_ver, last_message)
213
214        logging.info("Successfully updated from %s to %s %s", running_ver_str,
215                     image_name, image_ver_str)
216
217
218    def fetch_image(self, ver=None):
219        """Fetch the image from gs and copy it to the host.
220
221        @param ver: The rw version of the prod image. If it is not None then the
222                    image will be retrieved from chromeos-localmirror otherwise
223                    it will be gotten from chromeos-localmirror-private using
224                    the devids
225        """
226        if ver:
227            return self.download_cr50_release_image(ver)
228        return self.download_cr50_debug_image(self.devid)
229
230
231    def add_image_to_update_order(self, image_name, image_path, ver=None):
232        """Process the image. Add it to the update_order list and images dict.
233
234        Copy the image to the DUT and get version information.
235
236        Store the image information in the images dictionary and add it to the
237        update_order list.
238
239        @param image_name: string that is what the image should be called. Used
240                           as the key in the images dict.
241        @param image_path: the path for the image.
242        @param ver: If the image path isn't specified, this will be used to find
243                    the cr50 image in gs://chromeos-localmirror/distfiles.
244        """
245        tmp_file = '/tmp/%s.bin' % image_name
246
247        if not os.path.isfile(image_path):
248            image_path = self.fetch_image(ver)
249
250        _, ver = cr50_utils.InstallImage(self.host, image_path, tmp_file)
251
252        ver_str = cr50_utils.GetVersionString(ver)
253
254        self.update_order.append(image_name)
255        self.images[image_name] = (ver, ver_str, image_path)
256        logging.info("%s stored at %s with version %s", image_name, image_path,
257                     ver_str)
258
259
260    def verify_update_order(self):
261        """Verify each image in the update order has a higher version than the
262        last.
263
264        The test uses the cr50 update script to update to the next image in the
265        update order. If the versions are not in ascending order then the update
266        won't work. Cr50 cannot update to an older version using the standard
267        update process.
268
269        Raises:
270            TestError if the versions are not in ascending order.
271        """
272        for i, name in enumerate(self.update_order[1::]):
273            rw = self.images[name][0][1]
274
275            last_name = self.update_order[i]
276            last_rw = self.images[last_name][0][1]
277            if cr50_utils.GetNewestVersion(last_rw, rw) != rw:
278                raise error.TestError("%s is version %s. %s needs to have a "
279                                      "higher version, but it has %s" %
280                                      (last_name, last_rw, name, rw))
281
282
283    def save_original_image(self, dut_path):
284        """Save the image currently running on the DUT.
285
286        Copy the image from the DUT to the local autotest directory and get
287        version information. Store the information in the images dict. Make sure
288        the saved version matches the running version.
289
290        Args:
291            dut_path: the location of the cr50 prod image on the DUT.
292
293        Raises:
294            error.TestError if the saved cr50 image version does not match the
295            version cr50 is running.
296        """
297        name = self.ORIGINAL_NAME
298        local_dest = os.path.join(self.resultsdir, name + '.bin')
299
300        running_ver = cr50_utils.GetRunningVersion(self.host)
301        running_ver_str = cr50_utils.GetVersionString(running_ver)
302
303        self.host.get_file(dut_path, local_dest)
304
305        saved_ver = cr50_utils.GetBinVersion(self.host, dut_path)
306        saved_ver_str = cr50_utils.GetVersionString(saved_ver)
307
308        # If Cr50 is not running the image in the cr50 firmware directory, then
309        # raise an error. We can't run this test unless we can restore the
310        # original state during cleanup.
311        if running_ver[1] != saved_ver[1]:
312            raise error.TestError("Can't determine original Cr50 version. "
313                                  "Running %s, but saved %s." %
314                                  (running_ver_str, saved_ver_str))
315
316        self.images[name] = (saved_ver, saved_ver_str, local_dest)
317        logging.info("%s stored at %s with version %s", name, local_dest,
318                     saved_ver_str)
319
320
321    def after_run_once(self):
322        """Add log printing what iteration we just completed"""
323        logging.info("Update iteration %s ran successfully", self.iteration)
324
325
326    def run_once(self):
327        """Update to each image in update_order"""
328        for name in self.update_order:
329            self.run_update(name)
330