xref: /aosp_15_r20/external/autotest/server/cros/minios/minios_test.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright 2022 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Lifrom __future__ import absolute_import
7*9c5db199SXin Lifrom __future__ import division
8*9c5db199SXin Lifrom __future__ import print_function
9*9c5db199SXin Li
10*9c5db199SXin Liimport json
11*9c5db199SXin Liimport logging
12*9c5db199SXin Liimport os
13*9c5db199SXin Liimport re
14*9c5db199SXin Liimport time
15*9c5db199SXin Li
16*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
17*9c5db199SXin Lifrom autotest_lib.server.cros.minios import minios_util
18*9c5db199SXin Lifrom autotest_lib.server.cros.update_engine import update_engine_test
19*9c5db199SXin Li
20*9c5db199SXin Li
21*9c5db199SXin Liclass MiniOsTest(update_engine_test.UpdateEngineTest):
22*9c5db199SXin Li    """
23*9c5db199SXin Li    Base class that sets up helper objects/functions for NBR tests.
24*9c5db199SXin Li
25*9c5db199SXin Li    """
26*9c5db199SXin Li
27*9c5db199SXin Li    _MINIOS_CLIENT_CMD = 'minios_client'
28*9c5db199SXin Li    _MINIOS_KERNEL_FLAG = 'cros_minios'
29*9c5db199SXin Li
30*9c5db199SXin Li    # Period to wait for firmware screen in seconds.
31*9c5db199SXin Li    # Value based on Brya, which is the slowest so far.
32*9c5db199SXin Li    _FIRMWARE_SCREEN_TIMEOUT = 30
33*9c5db199SXin Li
34*9c5db199SXin Li    # Number of times to attempt booting into MiniOS.
35*9c5db199SXin Li    _MINIOS_BOOT_MAX_ATTEMPTS = 3
36*9c5db199SXin Li
37*9c5db199SXin Li    # Timeout periods, given in seconds.
38*9c5db199SXin Li    _MINIOS_SHUTDOWN_TIMEOUT = 30
39*9c5db199SXin Li
40*9c5db199SXin Li    # Number of seconds to wait for the host to boot into MiniOS. Should always
41*9c5db199SXin Li    # be greater than `_FIRMWARE_SCREEN_TIMEOUT`.
42*9c5db199SXin Li    _MINIOS_WAIT_UP_TIME_SECONDS = 120
43*9c5db199SXin Li
44*9c5db199SXin Li    # Version reported to OMAHA/NEBRASKA for recovery.
45*9c5db199SXin Li    _RECOVERY_VERSION = '0.0.0.0'
46*9c5db199SXin Li
47*9c5db199SXin Li    # Files used by the tests.
48*9c5db199SXin Li    _DEPENDENCY_DIRS = ['bin', 'lib', 'lib64', 'libexec']
49*9c5db199SXin Li    _DEPENDENCY_INSTALL_DIR = '/usr/local'
50*9c5db199SXin Li    _MINIOS_TEMP_STATEFUL_DIR = '/usr/local/tmp/stateful'
51*9c5db199SXin Li    _STATEFUL_DEV_IMAGE_NAME = 'dev_image_new'
52*9c5db199SXin Li
53*9c5db199SXin Li    def initialize(self, host):
54*9c5db199SXin Li        """
55*9c5db199SXin Li        Sets default variables for the test.
56*9c5db199SXin Li
57*9c5db199SXin Li        @param host: The DUT we will be running on.
58*9c5db199SXin Li
59*9c5db199SXin Li        """
60*9c5db199SXin Li        super(MiniOsTest, self).initialize(host)
61*9c5db199SXin Li        self._nebraska = None
62*9c5db199SXin Li        self._servo = host.servo
63*9c5db199SXin Li        self._servo.initialize_dut()
64*9c5db199SXin Li
65*9c5db199SXin Li    def cleanup(self):
66*9c5db199SXin Li        """Clean up minios autotests."""
67*9c5db199SXin Li        if self._nebraska:
68*9c5db199SXin Li            self._nebraska.stop()
69*9c5db199SXin Li        super(MiniOsTest, self).cleanup()
70*9c5db199SXin Li        # Make sure to reboot DUT into CroS in case of failures.
71*9c5db199SXin Li        self._host.reboot()
72*9c5db199SXin Li
73*9c5db199SXin Li    def _boot_minios(self):
74*9c5db199SXin Li        """Boot the DUT into MiniOS."""
75*9c5db199SXin Li        # Turn off usbkey to avoid booting into usb-recovery image.
76*9c5db199SXin Li        self._servo.switch_usbkey('off')
77*9c5db199SXin Li        psc = self._servo.get_power_state_controller()
78*9c5db199SXin Li        psc.power_off()
79*9c5db199SXin Li        psc.power_on(psc.REC_ON)
80*9c5db199SXin Li        self._host.test_wait_for_shutdown(self._MINIOS_SHUTDOWN_TIMEOUT)
81*9c5db199SXin Li        logging.info('Waiting for firmware screen')
82*9c5db199SXin Li        time.sleep(self._FIRMWARE_SCREEN_TIMEOUT)
83*9c5db199SXin Li
84*9c5db199SXin Li        # Attempt multiple times to boot into MiniOS. If all attempts fail then
85*9c5db199SXin Li        # this is some kind of firmware issue. Since we failed to boot an OS use
86*9c5db199SXin Li        # servo to reset the unit and then report test failure.
87*9c5db199SXin Li        attempts = 0
88*9c5db199SXin Li        minios_is_up = False
89*9c5db199SXin Li        while not minios_is_up and attempts < self._MINIOS_BOOT_MAX_ATTEMPTS:
90*9c5db199SXin Li            # Use Ctrl+R shortcut to boot 'MiniOS
91*9c5db199SXin Li            logging.info('Try to boot MiniOS')
92*9c5db199SXin Li            self._servo.ctrl_r()
93*9c5db199SXin Li            minios_is_up = self._host.wait_up(
94*9c5db199SXin Li                    timeout=self._MINIOS_WAIT_UP_TIME_SECONDS,
95*9c5db199SXin Li                    host_is_down=True)
96*9c5db199SXin Li            attempts += 1
97*9c5db199SXin Li
98*9c5db199SXin Li        if minios_is_up:
99*9c5db199SXin Li            # If mainfw_type is recovery then we are in MiniOS.
100*9c5db199SXin Li            mainfw_type = self._host.run_output('crossystem mainfw_type')
101*9c5db199SXin Li            if mainfw_type != 'recovery':
102*9c5db199SXin Li                raise error.TestError(
103*9c5db199SXin Li                        'Boot to MiniOS - invalid firmware: %s.' % mainfw_type)
104*9c5db199SXin Li            # There are multiple types of recovery images, make sure we booted
105*9c5db199SXin Li            # into minios.
106*9c5db199SXin Li            pattern = r'\b%s\b' % self._MINIOS_KERNEL_FLAG
107*9c5db199SXin Li            if not re.search(pattern, self._host.get_cmdline()):
108*9c5db199SXin Li                raise error.TestError(
109*9c5db199SXin Li                        'Boot to MiniOS - recovery image is not minios.')
110*9c5db199SXin Li        else:
111*9c5db199SXin Li            # Try to not leave unit on recovery firmware screen.
112*9c5db199SXin Li            self._host.power_cycle()
113*9c5db199SXin Li            raise error.TestError('Boot to MiniOS failed.')
114*9c5db199SXin Li
115*9c5db199SXin Li    def _create_minios_hostlog(self):
116*9c5db199SXin Li        """Create the minios hostlog file.
117*9c5db199SXin Li
118*9c5db199SXin Li        To ensure the recovery was successful we need to compare the update
119*9c5db199SXin Li        events against expected update events. This function creates the hostlog
120*9c5db199SXin Li        for minios before the recovery reboots the DUT.
121*9c5db199SXin Li
122*9c5db199SXin Li        """
123*9c5db199SXin Li        # Check that update logs exist.
124*9c5db199SXin Li        if len(self._get_update_engine_logs()) < 1:
125*9c5db199SXin Li            err_msg = 'update_engine logs are missing. Cannot verify recovery.'
126*9c5db199SXin Li            raise error.TestFail(err_msg)
127*9c5db199SXin Li
128*9c5db199SXin Li        # Download the logs instead of reading it over the network since it will
129*9c5db199SXin Li        # disappear after MiniOS reboots the DUT.
130*9c5db199SXin Li        logfile = os.path.join(self.resultsdir, 'minios_update_engine.log')
131*9c5db199SXin Li        self._host.get_file(self._UPDATE_ENGINE_LOG, logfile)
132*9c5db199SXin Li        logfile_content = None
133*9c5db199SXin Li        with open(logfile) as f:
134*9c5db199SXin Li            logfile_content = f.read()
135*9c5db199SXin Li        minios_hostlog = os.path.join(self.resultsdir, 'hostlog_minios')
136*9c5db199SXin Li        with open(minios_hostlog, 'w') as fp:
137*9c5db199SXin Li            # There are four expected hostlog events during recovery.
138*9c5db199SXin Li            extract_logs = self._extract_request_logs(logfile_content)
139*9c5db199SXin Li            json.dump(extract_logs[-4:], fp)
140*9c5db199SXin Li        return minios_hostlog
141*9c5db199SXin Li
142*9c5db199SXin Li    def _install_test_dependencies(self, public_bucket=False):
143*9c5db199SXin Li        """
144*9c5db199SXin Li        Install test dependencies from a downloaded stateful archive.
145*9c5db199SXin Li
146*9c5db199SXin Li        @param public_bucket: True to download stateful from a public bucket.
147*9c5db199SXin Li
148*9c5db199SXin Li        """
149*9c5db199SXin Li        if not self._job_repo_url:
150*9c5db199SXin Li            raise error.TestError('No job repo url set.')
151*9c5db199SXin Li
152*9c5db199SXin Li        statefuldev_url = self._stage_stateful(public_bucket)
153*9c5db199SXin Li        logging.info('Installing dependencies from %s', statefuldev_url)
154*9c5db199SXin Li
155*9c5db199SXin Li        # Create destination directories.
156*9c5db199SXin Li        minios_dev_image_dir = os.path.join(self._MINIOS_TEMP_STATEFUL_DIR,
157*9c5db199SXin Li                                            self._STATEFUL_DEV_IMAGE_NAME)
158*9c5db199SXin Li        install_dirs = [
159*9c5db199SXin Li                os.path.join(self._DEPENDENCY_INSTALL_DIR, dir)
160*9c5db199SXin Li                for dir in self._DEPENDENCY_DIRS
161*9c5db199SXin Li        ]
162*9c5db199SXin Li        self._run(['mkdir', '-p', minios_dev_image_dir] + install_dirs)
163*9c5db199SXin Li        # Symlink the install dirs into the staging destination.
164*9c5db199SXin Li        for dir in install_dirs:
165*9c5db199SXin Li            self._run(['ln', '-s', dir, minios_dev_image_dir])
166*9c5db199SXin Li
167*9c5db199SXin Li        # Generate the list of stateful archive members that we want to extract.
168*9c5db199SXin Li        members = [
169*9c5db199SXin Li                os.path.join(self._STATEFUL_DEV_IMAGE_NAME, dir)
170*9c5db199SXin Li                for dir in self._DEPENDENCY_DIRS
171*9c5db199SXin Li        ]
172*9c5db199SXin Li        try:
173*9c5db199SXin Li            self._download_and_extract_stateful(statefuldev_url,
174*9c5db199SXin Li                                                self._MINIOS_TEMP_STATEFUL_DIR,
175*9c5db199SXin Li                                                members=members,
176*9c5db199SXin Li                                                keep_symlinks=True)
177*9c5db199SXin Li        except error.AutoservRunError as e:
178*9c5db199SXin Li            err_str = 'Failed to install the test dependencies'
179*9c5db199SXin Li            raise error.TestFail('%s: %s' % (err_str, str(e)))
180*9c5db199SXin Li
181*9c5db199SXin Li        self._setup_python_symlinks()
182*9c5db199SXin Li
183*9c5db199SXin Li        # Clean-up unused files to save memory.
184*9c5db199SXin Li        self._run(['rm', '-rf', self._MINIOS_TEMP_STATEFUL_DIR])
185*9c5db199SXin Li
186*9c5db199SXin Li    def _setup_python_symlinks(self):
187*9c5db199SXin Li        """
188*9c5db199SXin Li        Create symlinks in the root to point to all python paths in /usr/local
189*9c5db199SXin Li        for stateful installed python to work. This is needed because Gentoo
190*9c5db199SXin Li        creates wrappers with hardcoded paths to the root (e.g. python-exec).
191*9c5db199SXin Li
192*9c5db199SXin Li        """
193*9c5db199SXin Li        for path in self._DEPENDENCY_DIRS:
194*9c5db199SXin Li            self._run([
195*9c5db199SXin Li                    'find',
196*9c5db199SXin Li                    os.path.join(self._DEPENDENCY_INSTALL_DIR, path),
197*9c5db199SXin Li                    '-maxdepth', '1', '\(', '-name', 'python*', '-o', '-name',
198*9c5db199SXin Li                    'portage', '\)', '-exec', 'ln', '-s', '{}',
199*9c5db199SXin Li                    os.path.join('/usr', path), '\;'
200*9c5db199SXin Li            ])
201*9c5db199SXin Li
202*9c5db199SXin Li    def _start_nebraska(self, payload_url=None):
203*9c5db199SXin Li        """
204*9c5db199SXin Li        Initialize and start nebraska on the DUT.
205*9c5db199SXin Li
206*9c5db199SXin Li        @param payload_url: The payload to served by nebraska.
207*9c5db199SXin Li
208*9c5db199SXin Li        """
209*9c5db199SXin Li        if not self._nebraska:
210*9c5db199SXin Li            self._nebraska = minios_util.NebraskaService(
211*9c5db199SXin Li                    self, self._host, payload_url)
212*9c5db199SXin Li        self._nebraska.start()
213*9c5db199SXin Li
214*9c5db199SXin Li    def _verify_reboot(self, old_boot_id):
215*9c5db199SXin Li        """
216*9c5db199SXin Li        Verify that the unit rebooted using the boot_id.
217*9c5db199SXin Li
218*9c5db199SXin Li        @param old_boot_id A boot id value obtained before the
219*9c5db199SXin Li                               reboot.
220*9c5db199SXin Li
221*9c5db199SXin Li        """
222*9c5db199SXin Li        self._host.test_wait_for_shutdown(self._MINIOS_SHUTDOWN_TIMEOUT)
223*9c5db199SXin Li        self._host.test_wait_for_boot(old_boot_id)
224