Home | History | Annotate | Download | only in cros
      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 """
      6 Factory install tests.
      7 
      8 FactoryInstallTest is an abstract superclass; factory_InstallVM and
      9 factory_InstallServo are two concrete implementations.
     10 
     11 Subclasses 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 
     22 import glob, logging, os, re, shutil, socket, sys, thread, time, traceback
     23 from abc import abstractmethod
     24 from StringIO import StringIO
     25 
     26 from autotest_lib.client.bin import utils as client_utils
     27 from autotest_lib.client.common_lib import error
     28 from 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 = """
     47 echo Running hwid_updater "$@" >&2
     48 set -ex
     49 MOUNT_DIR=$(mktemp -d --tmpdir)
     50 mount "$1" "$MOUNT_DIR"
     51 ls -l "$MOUNT_DIR"
     52 mkdir -p "$MOUNT_DIR/dev_image/share/chromeos-hwid"
     53 echo %s > "$MOUNT_DIR/dev_image/share/chromeos-hwid/cfg"
     54 umount "$MOUNT_DIR"
     55 """
     56 
     57 
     58 class 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