| # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """ |
| Factory install tests. |
| |
| FactoryInstallTest is an abstract superclass; factory_InstallVM and |
| factory_InstallServo are two concrete implementations. |
| |
| Subclasses of FactoryInstallTest supports the following flags: |
| |
| factory_install_image: (required) path to factory install shim |
| factory_test_image: (required) path to factory test image |
| test_image: (required) path to ChromeOS test image |
| miniomaha_port: port for miniomaha |
| debug_make_factory_package: whether to re-make the factory package before |
| running tests (defaults to true; may be set to false for debugging |
| only) |
| """ |
| |
| import glob, logging, os, re, shutil, socket, sys, thread, time, traceback |
| from abc import abstractmethod |
| from StringIO import StringIO |
| |
| from autotest_lib.client.bin import utils as client_utils |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.server import test, utils |
| |
| |
| # How long to wait for the mini-Omaha server to come up. |
| _MINIOMAHA_TIMEOUT_SEC = 50 |
| |
| # Path to make_factory_package.sh within the source root. |
| _MAKE_FACTORY_PACKAGE_PATH = \ |
| "platform/factory-utils/factory_setup/make_factory_package.sh" |
| |
| # Path to miniomaha.py within the source root. |
| _MINIOMAHA_PATH = "platform/factory-utils/factory_setup/miniomaha.py" |
| |
| # Sleep interval for nontrivial operations (like rsyncing). |
| _POLL_SLEEP_INTERVAL_SEC = 2 |
| |
| # The hwid_updater script (run in the factory install shim). This is a format |
| # string with a single argument (the name of the HWID cfg). |
| _HWID_UPDATER_SH_TEMPLATE = """ |
| echo Running hwid_updater "$@" >&2 |
| set -ex |
| MOUNT_DIR=$(mktemp -d --tmpdir) |
| mount "$1" "$MOUNT_DIR" |
| ls -l "$MOUNT_DIR" |
| mkdir -p "$MOUNT_DIR/dev_image/share/chromeos-hwid" |
| echo %s > "$MOUNT_DIR/dev_image/share/chromeos-hwid/cfg" |
| umount "$MOUNT_DIR" |
| """ |
| |
| |
| class FactoryInstallTest(test.test): |
| """ |
| Factory install VM tests. |
| |
| See file-level docstring for details. |
| """ |
| |
| version = 1 |
| |
| # How long to wait for the factory tests to install. |
| FACTORY_INSTALL_TIMEOUT_SEC = 1800 |
| |
| # How long to wait for the factory test image to come up. |
| WAIT_UP_TIMEOUT_SEC = 30 |
| |
| # How long to wait for the factory tests to run. |
| FACTORY_TEST_TIMEOUT_SEC = 240 |
| |
| # How long to wait for the ChromeOS image to run. |
| FIRST_BOOT_TIMEOUT_SEC = 480 |
| |
| # |
| # Abstract functions that must be overridden by subclasses. |
| # |
| |
| @abstractmethod |
| def get_hwid_cfg(self): |
| """ |
| Returns the HWID cfg, used to select a test list. |
| """ |
| pass |
| |
| @abstractmethod |
| def run_factory_install(self, shim_image): |
| """ |
| Performs the factory install and starts the factory tests. |
| |
| When this returns, the DUT should be starting up (or have already |
| started up) in factory test mode. |
| """ |
| pass |
| |
| @abstractmethod |
| def get_dut_client(self): |
| """ |
| Returns a client (subclass of CrosHost) to control the DUT. |
| """ |
| pass |
| |
| @abstractmethod |
| def reboot_for_wipe(self): |
| """ |
| Reboots the machine after preparing to wipe the hard drive. |
| """ |
| pass |
| |
| # |
| # Utility methods that may be used by subclasses. |
| # |
| |
| def src_root(self): |
| """ |
| Returns the CrOS source root. |
| """ |
| return os.path.join(os.environ["CROS_WORKON_SRCROOT"], "src") |
| |
| def parse_boolean(self, val): |
| """ |
| Parses a string as a Boolean value. |
| """ |
| # Insist on True or False, because (e.g.) bool('false') == True. |
| if str(val) not in ["True", "False"]: |
| raise error.TestError("Not a boolean: '%s'" % val) |
| return str(val) == "True" |
| |
| # |
| # Private utility methods. |
| # |
| |
| def _modify_file(self, path, func): |
| """ |
| Modifies a file as the root user. |
| |
| @param path: The path to the file to modify. |
| @param func: A function that will be invoked with a single argument |
| (the current contents of the file, or None if the file does not |
| exist) and which should return the new contents. |
| """ |
| if os.path.exists(path): |
| contents = utils.system_output("sudo cat %s" % path) |
| else: |
| contents = func(None) |
| |
| utils.run("sudo dd of=%s" % path, stdin=func(contents)) |
| |
| def _mount_partition(self, image, index): |
| """ |
| Mounts a partition of an image temporarily using loopback. |
| |
| The partition will be automatically unmounted when the test exits. |
| |
| @param image: The image to mount. |
| @param index: The partition number to mount. |
| @return: The mount point. |
| """ |
| mount_point = os.path.join(self.tmpdir, |
| "%s_%d" % (image, index)) |
| if not os.path.exists(mount_point): |
| os.makedirs(mount_point) |
| common_args = "cgpt show -i %d %s" % (index, image) |
| offset = int(utils.system_output(common_args + " -b")) * 512 |
| size = int(utils.system_output(common_args + " -s")) * 512 |
| utils.run("sudo mount -o rw,loop,offset=%d,sizelimit=%d %s %s" % ( |
| offset, size, image, mount_point)) |
| self.cleanup_tasks.append(lambda: self._umount_partition(mount_point)) |
| return mount_point |
| |
| def _umount_partition(self, mount_point): |
| """ |
| Unmounts the mount at the given mount point. |
| |
| Also deletes the mount point directory. Does not raise an |
| exception if the mount point does not exist or the mount fails. |
| """ |
| if os.path.exists(mount_point): |
| utils.run("sudo umount -d %s" % mount_point) |
| os.rmdir(mount_point) |
| |
| def _make_factory_package(self, factory_test_image, test_image): |
| """ |
| Makes the factory package. |
| """ |
| # Create a pseudo-HWID-updater that merely sets the HWID to "vm" or |
| # "servo" so that the appropriate test list will run. (This gets run by |
| # the factory install shim.) |
| hwid_updater = os.path.join(self.tmpdir, "hwid_updater.sh") |
| with open(hwid_updater, "w") as f: |
| f.write(_HWID_UPDATER_SH_TEMPLATE % self.get_hwid_cfg()) |
| |
| utils.run("%s --factory=%s --release=%s " |
| "--firmware_updater=none --hwid_updater=%s " % |
| (os.path.join(self.src_root(), _MAKE_FACTORY_PACKAGE_PATH), |
| factory_test_image, test_image, hwid_updater)) |
| |
| def _start_miniomaha(self): |
| """ |
| Starts a mini-Omaha server and drains its log output. |
| """ |
| def is_miniomaha_up(): |
| try: |
| utils.urlopen( |
| "http://localhost:%d" % self.miniomaha_port).read() |
| return True |
| except: |
| return False |
| |
| assert not is_miniomaha_up() |
| |
| self.miniomaha_output = os.path.join(self.outputdir, "miniomaha.out") |
| |
| # TODO(jsalz): Add cwd to BgJob rather than including the 'cd' in the |
| # command. |
| bg_job = utils.BgJob( |
| "cd %s; exec ./%s --port=%d --factory_config=miniomaha.conf" |
| % (os.path.join(self.src_root(), |
| os.path.dirname(_MINIOMAHA_PATH)), |
| os.path.basename(_MINIOMAHA_PATH), |
| self.miniomaha_port), verbose=True, |
| stdout_tee=utils.TEE_TO_LOGS, |
| stderr_tee=open(self.miniomaha_output, "w")) |
| self.cleanup_tasks.append(lambda: utils.nuke_subprocess(bg_job.sp)) |
| thread.start_new_thread(utils.join_bg_jobs, ([bg_job],)) |
| |
| client_utils.poll_for_condition(is_miniomaha_up, |
| timeout=_MINIOMAHA_TIMEOUT_SEC, |
| desc="Miniomaha server") |
| |
| def _prepare_factory_install_shim(self, factory_install_image): |
| # Make a copy of the factory install shim image (to use as hdb). |
| modified_image = os.path.join(self.tmpdir, "shim.bin") |
| logging.info("Creating factory install image: %s", modified_image) |
| shutil.copyfile(factory_install_image, modified_image) |
| |
| # Mount partition 1 of the modified_image and set the mini-Omaha server. |
| mount = self._mount_partition(modified_image, 1) |
| self._modify_file( |
| os.path.join(mount, "dev_image/etc/lsb-factory"), |
| lambda contents: re.sub( |
| r"^(CHROMEOS_(AU|DEV)SERVER)=.+", |
| r"\1=http://%s:%d/update" % ( |
| socket.gethostname(), self.miniomaha_port), |
| contents, |
| re.MULTILINE)) |
| self._umount_partition(mount) |
| |
| return modified_image |
| |
| def _run_factory_tests_and_prepare_wipe(self): |
| """ |
| Runs the factory tests and prepares the machine for wiping. |
| """ |
| dut_client = self.get_dut_client() |
| if not dut_client.wait_up(FactoryInstallTest.WAIT_UP_TIMEOUT_SEC): |
| raise error.TestFail("DUT never came up to run factory tests") |
| |
| # Poll the factory log, and wait for the factory_Review test to become |
| # active. |
| local_factory_log = os.path.join(self.outputdir, "factory.log") |
| remote_factory_log = "/var/log/factory.log" |
| |
| # Wait for factory.log file to exist |
| dut_client.run( |
| "while ! [ -e %s ]; do sleep 1; done" % remote_factory_log, |
| timeout=FactoryInstallTest.FACTORY_TEST_TIMEOUT_SEC) |
| |
| status_map = {} |
| |
| def wait_for_factory_logs(): |
| dut_client.get_file(remote_factory_log, local_factory_log) |
| data = open(local_factory_log).read() |
| new_status_map = dict( |
| re.findall(r"status change for (\S+) : \S+ -> (\S+)", data)) |
| if status_map != new_status_map: |
| logging.info("Test statuses: %s", status_map) |
| # Can't assign directly since it's in a context outside |
| # this function. |
| status_map.clear() |
| status_map.update(new_status_map) |
| return status_map.get("factory_Review.z") == "ACTIVE" |
| |
| client_utils.poll_for_condition( |
| wait_for_factory_logs, |
| timeout=FactoryInstallTest.FACTORY_TEST_TIMEOUT_SEC, |
| sleep_interval=_POLL_SLEEP_INTERVAL_SEC, |
| desc="Factory logs") |
| |
| # All other statuses should be "PASS". |
| expected_status_map = { |
| "memoryrunin": "PASS", |
| "factory_Review.z": "ACTIVE", |
| "factory_Start.e": "PASS", |
| "hardware_SAT.memoryrunin_s1": "PASS", |
| } |
| if status_map != expected_status_map: |
| raise error.TestFail("Expected statuses of %s but found %s" % ( |
| expected_status_map, status_map)) |
| |
| dut_client.run("cd /usr/local/factory/bin; " |
| "./gooftool --prepare_wipe --verbose") |
| |
| def _complete_install(self): |
| """ |
| Completes the install, resulting in a full ChromeOS image. |
| """ |
| # Restart the SSH client: with a new OS, some configuration |
| # properties (e.g., availability of rsync) may have changed. |
| dut_client = self.get_dut_client() |
| |
| if not dut_client.wait_up(FactoryInstallTest.FIRST_BOOT_TIMEOUT_SEC): |
| raise error.TestFail("DUT never came up after install") |
| |
| # Check lsb-release to make sure we have a real live ChromeOS image |
| # (it should be the test build). |
| lsb_release = os.path.join(self.tmpdir, "lsb-release") |
| dut_client.get_file("/etc/lsb-release", lsb_release) |
| expected_re = r"^CHROMEOS_RELEASE_DESCRIPTION=.*Test Build" |
| data = open(lsb_release).read() |
| assert re.search( |
| "^CHROMEOS_RELEASE_DESCRIPTION=.*Test Build", data, re.MULTILINE), ( |
| "Didn't find expected regular expression %s in lsb-release: " % ( |
| expected_re, data)) |
| logging.info("Install succeeded! lsb-release is:\n%s", data) |
| |
| dut_client.halt() |
| if not dut_client.wait_down( |
| timeout=FactoryInstallTest.WAIT_UP_TIMEOUT_SEC): |
| raise error.TestFail("Client never went down after ChromeOS boot") |
| |
| # |
| # Autotest methods. |
| # |
| |
| def setup(self): |
| self.cleanup_tasks = [] |
| self.ssh_tunnel_port = utils.get_unused_port() |
| |
| def run_once(self, factory_install_image, factory_test_image, test_image, |
| miniomaha_port=None, debug_make_factory_package=True, |
| **args): |
| """ |
| Runs the test once. |
| |
| See the file-level comments for an explanation of the test arguments. |
| |
| @param args: Must be empty (present as a check against misspelled |
| arguments on the command line) |
| """ |
| assert not args, "Unexpected arguments %s" % args |
| |
| self.miniomaha_port = ( |
| int(miniomaha_port) if miniomaha_port else utils.get_unused_port()) |
| |
| if self.parse_boolean(debug_make_factory_package): |
| self._make_factory_package(factory_test_image, test_image) |
| self._start_miniomaha() |
| shim_image = self._prepare_factory_install_shim(factory_install_image) |
| self.run_factory_install(shim_image) |
| self._run_factory_tests_and_prepare_wipe() |
| self.reboot_for_wipe() |
| self._complete_install() |
| |
| def cleanup(self): |
| for task in self.cleanup_tasks: |
| try: |
| task() |
| except: |
| logging.info("Exception in cleanup task:") |
| traceback.print_exc(file=sys.stdout) |