xref: /aosp_15_r20/external/autotest/server/cros/factory_install_test.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7Factory install tests.
8
9FactoryInstallTest is an abstract superclass; factory_InstallVM and
10factory_InstallServo are two concrete implementations.
11
12Subclasses of FactoryInstallTest supports the following flags:
13
14    factory_install_image: (required) path to factory install shim
15    factory_test_image: (required) path to factory test image
16    test_image: (required) path to ChromeOS test image
17    miniomaha_port: port for miniomaha
18    debug_make_factory_package: whether to re-make the factory package before
19        running tests (defaults to true; may be set to false for debugging
20        only)
21"""
22
23import glob, logging, os, re, shutil, socket, sys, six.moves._thread, time, traceback
24from abc import abstractmethod
25from six import StringIO
26
27from autotest_lib.client.bin import utils as client_utils
28from autotest_lib.client.common_lib import error
29from autotest_lib.server import test, utils
30
31
32# How long to wait for the mini-Omaha server to come up.
33_MINIOMAHA_TIMEOUT_SEC = 50
34
35# Path to make_factory_package.sh within the source root.
36_MAKE_FACTORY_PACKAGE_PATH = \
37    "platform/factory-utils/factory_setup/make_factory_package.sh"
38
39# Path to miniomaha.py within the source root.
40_MINIOMAHA_PATH = "platform/factory-utils/factory_setup/miniomaha.py"
41
42# Sleep interval for nontrivial operations (like rsyncing).
43_POLL_SLEEP_INTERVAL_SEC = 2
44
45# The hwid_updater script (run in the factory install shim).  This is a format
46# string with a single argument (the name of the HWID cfg).
47_HWID_UPDATER_SH_TEMPLATE = """
48echo Running hwid_updater "$@" >&2
49set -ex
50MOUNT_DIR=$(mktemp -d --tmpdir)
51mount "$1" "$MOUNT_DIR"
52ls -l "$MOUNT_DIR"
53mkdir -p "$MOUNT_DIR/dev_image/share/chromeos-hwid"
54echo %s > "$MOUNT_DIR/dev_image/share/chromeos-hwid/cfg"
55umount "$MOUNT_DIR"
56"""
57
58
59class FactoryInstallTest(test.test):
60    """
61    Factory install VM tests.
62
63    See file-level docstring for details.
64    """
65
66    version = 1
67
68    # How long to wait for the factory tests to install.
69    FACTORY_INSTALL_TIMEOUT_SEC = 1800
70
71    # How long to wait for the factory test image to come up.
72    WAIT_UP_TIMEOUT_SEC = 30
73
74    # How long to wait for the factory tests to run.
75    FACTORY_TEST_TIMEOUT_SEC = 240
76
77    # How long to wait for the ChromeOS image to run.
78    FIRST_BOOT_TIMEOUT_SEC = 480
79
80    #
81    # Abstract functions that must be overridden by subclasses.
82    #
83
84    @abstractmethod
85    def get_hwid_cfg(self):
86        """
87        Returns the HWID cfg, used to select a test list.
88        """
89        pass
90
91    @abstractmethod
92    def run_factory_install(self, shim_image):
93        """
94        Performs the factory install and starts the factory tests.
95
96        When this returns, the DUT should be starting up (or have already
97        started up) in factory test mode.
98        """
99        pass
100
101    @abstractmethod
102    def get_dut_client(self):
103        """
104        Returns a client (subclass of CrosHost) to control the DUT.
105        """
106        pass
107
108    @abstractmethod
109    def reboot_for_wipe(self):
110        """
111        Reboots the machine after preparing to wipe the hard drive.
112        """
113        pass
114
115    #
116    # Utility methods that may be used by subclasses.
117    #
118
119    def src_root(self):
120        """
121        Returns the CrOS source root.
122        """
123        return os.path.join(os.environ["CROS_WORKON_SRCROOT"], "src")
124
125    def parse_boolean(self, val):
126        """
127        Parses a string as a Boolean value.
128        """
129        # Insist on True or False, because (e.g.) bool('false') == True.
130        if str(val) not in ["True", "False"]:
131            raise error.TestError("Not a boolean: '%s'" % val)
132        return str(val) == "True"
133
134    #
135    # Private utility methods.
136    #
137
138    def _modify_file(self, path, func):
139        """
140        Modifies a file as the root user.
141
142        @param path: The path to the file to modify.
143        @param func: A function that will be invoked with a single argument
144            (the current contents of the file, or None if the file does not
145            exist) and which should return the new contents.
146        """
147        if os.path.exists(path):
148            contents = utils.system_output("sudo cat %s" % path)
149        else:
150            contents = func(None)
151
152        utils.run("sudo dd of=%s" % path, stdin=func(contents))
153
154    def _mount_partition(self, image, index):
155        """
156        Mounts a partition of an image temporarily using loopback.
157
158        The partition will be automatically unmounted when the test exits.
159
160        @param image: The image to mount.
161        @param index: The partition number to mount.
162        @return: The mount point.
163        """
164        mount_point = os.path.join(self.tmpdir,
165                                   "%s_%d" % (image, index))
166        if not os.path.exists(mount_point):
167            os.makedirs(mount_point)
168        common_args = "cgpt show -i %d %s" % (index, image)
169        offset = int(utils.system_output(common_args + " -b")) * 512
170        size = int(utils.system_output(common_args + " -s")) * 512
171        utils.run("sudo mount -o rw,loop,offset=%d,sizelimit=%d %s %s" % (
172                offset, size, image, mount_point))
173        self.cleanup_tasks.append(lambda: self._umount_partition(mount_point))
174        return mount_point
175
176    def _umount_partition(self, mount_point):
177        """
178        Unmounts the mount at the given mount point.
179
180        Also deletes the mount point directory.  Does not raise an
181        exception if the mount point does not exist or the mount fails.
182        """
183        if os.path.exists(mount_point):
184            utils.run("sudo umount -d %s" % mount_point)
185            os.rmdir(mount_point)
186
187    def _make_factory_package(self, factory_test_image, test_image):
188        """
189        Makes the factory package.
190        """
191        # Create a pseudo-HWID-updater that merely sets the HWID to "vm" or
192        # "servo" so that the appropriate test list will run.  (This gets run by
193        # the factory install shim.)
194        hwid_updater = os.path.join(self.tmpdir, "hwid_updater.sh")
195        with open(hwid_updater, "w") as f:
196            f.write(_HWID_UPDATER_SH_TEMPLATE % self.get_hwid_cfg())
197
198        utils.run("%s --factory=%s --release=%s "
199                  "--firmware_updater=none --hwid_updater=%s " %
200                  (os.path.join(self.src_root(), _MAKE_FACTORY_PACKAGE_PATH),
201                   factory_test_image, test_image, hwid_updater))
202
203    def _start_miniomaha(self):
204        """
205        Starts a mini-Omaha server and drains its log output.
206        """
207        def is_miniomaha_up():
208            try:
209                utils.urlopen(
210                    "http://localhost:%d" % self.miniomaha_port).read()
211                return True
212            except:
213                return False
214
215        assert not is_miniomaha_up()
216
217        self.miniomaha_output = os.path.join(self.outputdir, "miniomaha.out")
218
219        # TODO(jsalz): Add cwd to BgJob rather than including the 'cd' in the
220        # command.
221        bg_job = utils.BgJob(
222            "cd %s; exec ./%s --port=%d --factory_config=miniomaha.conf"
223            % (os.path.join(self.src_root(),
224                            os.path.dirname(_MINIOMAHA_PATH)),
225               os.path.basename(_MINIOMAHA_PATH),
226               self.miniomaha_port), verbose=True,
227            stdout_tee=utils.TEE_TO_LOGS,
228            stderr_tee=open(self.miniomaha_output, "w"))
229        self.cleanup_tasks.append(lambda: utils.nuke_subprocess(bg_job.sp))
230        six.moves._thread.start_new_thread(utils.join_bg_jobs, ([bg_job],))
231
232        client_utils.poll_for_condition(is_miniomaha_up,
233                                        timeout=_MINIOMAHA_TIMEOUT_SEC,
234                                        desc="Miniomaha server")
235
236    def _prepare_factory_install_shim(self, factory_install_image):
237        # Make a copy of the factory install shim image (to use as hdb).
238        modified_image = os.path.join(self.tmpdir, "shim.bin")
239        logging.info("Creating factory install image: %s", modified_image)
240        shutil.copyfile(factory_install_image, modified_image)
241
242        # Mount partition 1 of the modified_image and set the mini-Omaha server.
243        mount = self._mount_partition(modified_image, 1)
244        self._modify_file(
245            os.path.join(mount, "dev_image/etc/lsb-factory"),
246            lambda contents: re.sub(
247                r"^(CHROMEOS_(AU|DEV)SERVER)=.+",
248                r"\1=http://%s:%d/update" % (
249                    socket.gethostname(), self.miniomaha_port),
250                contents,
251                re.MULTILINE))
252        self._umount_partition(mount)
253
254        return modified_image
255
256    def _run_factory_tests_and_prepare_wipe(self):
257        """
258        Runs the factory tests and prepares the machine for wiping.
259        """
260        dut_client = self.get_dut_client()
261        if not dut_client.wait_up(FactoryInstallTest.WAIT_UP_TIMEOUT_SEC):
262            raise error.TestFail("DUT never came up to run factory tests")
263
264        # Poll the factory log, and wait for the factory_Review test to become
265        # active.
266        local_factory_log = os.path.join(self.outputdir, "factory.log")
267        remote_factory_log = "/var/log/factory.log"
268
269        # Wait for factory.log file to exist
270        dut_client.run(
271            "while ! [ -e %s ]; do sleep 1; done" % remote_factory_log,
272            timeout=FactoryInstallTest.FACTORY_TEST_TIMEOUT_SEC)
273
274        status_map = {}
275
276        def wait_for_factory_logs():
277            dut_client.get_file(remote_factory_log, local_factory_log)
278            data = open(local_factory_log).read()
279            new_status_map = dict(
280                re.findall(r"status change for (\S+) : \S+ -> (\S+)", data))
281            if status_map != new_status_map:
282                logging.info("Test statuses: %s", status_map)
283                # Can't assign directly since it's in a context outside
284                # this function.
285                status_map.clear()
286                status_map.update(new_status_map)
287            return status_map.get("factory_Review.z") == "ACTIVE"
288
289        client_utils.poll_for_condition(
290            wait_for_factory_logs,
291            timeout=FactoryInstallTest.FACTORY_TEST_TIMEOUT_SEC,
292            sleep_interval=_POLL_SLEEP_INTERVAL_SEC,
293            desc="Factory logs")
294
295        # All other statuses should be "PASS".
296        expected_status_map = {
297            "memoryrunin": "PASS",
298            "factory_Review.z": "ACTIVE",
299            "factory_Start.e": "PASS",
300            "hardware_SAT.memoryrunin_s1": "PASS",
301        }
302        if status_map != expected_status_map:
303            raise error.TestFail("Expected statuses of %s but found %s" % (
304                    expected_status_map, status_map))
305
306        dut_client.run("cd /usr/local/factory/bin; "
307                       "./gooftool --prepare_wipe --verbose")
308
309    def _complete_install(self):
310        """
311        Completes the install, resulting in a full ChromeOS image.
312        """
313        # Restart the SSH client: with a new OS, some configuration
314        # properties (e.g., availability of rsync) may have changed.
315        dut_client = self.get_dut_client()
316
317        if not dut_client.wait_up(FactoryInstallTest.FIRST_BOOT_TIMEOUT_SEC):
318            raise error.TestFail("DUT never came up after install")
319
320        # Check lsb-release to make sure we have a real live ChromeOS image
321        # (it should be the test build).
322        lsb_release = os.path.join(self.tmpdir, "lsb-release")
323        dut_client.get_file("/etc/lsb-release", lsb_release)
324        expected_re = r"^CHROMEOS_RELEASE_DESCRIPTION=.*Test Build"
325        data = open(lsb_release).read()
326        assert re.search(
327            "^CHROMEOS_RELEASE_DESCRIPTION=.*Test Build", data, re.MULTILINE), (
328            "Didn't find expected regular expression %s in lsb-release: " % (
329                expected_re, data))
330        logging.info("Install succeeded!  lsb-release is:\n%s", data)
331
332        dut_client.halt()
333        if not dut_client.wait_down(
334            timeout=FactoryInstallTest.WAIT_UP_TIMEOUT_SEC):
335            raise error.TestFail("Client never went down after ChromeOS boot")
336
337    #
338    # Autotest methods.
339    #
340
341    def setup(self):
342        self.cleanup_tasks = []
343        self.ssh_tunnel_port = utils.get_unused_port()
344
345    def run_once(self, factory_install_image, factory_test_image, test_image,
346                 miniomaha_port=None, debug_make_factory_package=True,
347                 **args):
348        """
349        Runs the test once.
350
351        See the file-level comments for an explanation of the test arguments.
352
353        @param args: Must be empty (present as a check against misspelled
354            arguments on the command line)
355        """
356        assert not args, "Unexpected arguments %s" % args
357
358        self.miniomaha_port = (
359            int(miniomaha_port) if miniomaha_port else utils.get_unused_port())
360
361        if self.parse_boolean(debug_make_factory_package):
362            self._make_factory_package(factory_test_image, test_image)
363        self._start_miniomaha()
364        shim_image = self._prepare_factory_install_shim(factory_install_image)
365        self.run_factory_install(shim_image)
366        self._run_factory_tests_and_prepare_wipe()
367        self.reboot_for_wipe()
368        self._complete_install()
369
370    def cleanup(self):
371        for task in self.cleanup_tasks:
372            try:
373                task()
374            except:
375                logging.info("Exception in cleanup task:")
376                traceback.print_exc(file=sys.stdout)
377