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