#!/usr/bin/python # Copyright 2017 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. import os import tempfile import shutil import unittest from contextlib import contextmanager import common from autotest_lib.client.bin import utils from autotest_lib.client.common_lib import error from autotest_lib.site_utils import lxc from autotest_lib.site_utils.lxc import constants from autotest_lib.site_utils.lxc import unittest_http from autotest_lib.site_utils.lxc import unittest_setup from autotest_lib.site_utils.lxc import utils as lxc_utils @unittest.skipIf(lxc.IS_MOBLAB, 'Zygotes are not supported on moblab.') class ZygoteTests(lxc_utils.LXCTests): """Unit tests for the Zygote class.""" @classmethod def setUpClass(cls): super(ZygoteTests, cls).setUpClass() cls.test_dir = tempfile.mkdtemp(dir=lxc.DEFAULT_CONTAINER_PATH, prefix='zygote_unittest_') # Check if a base container exists on this machine and download one if # necessary. image = lxc.BaseImage() try: cls.base_container = image.get() cls.cleanup_base_container = False except error.ContainerError: image.setup() cls.base_container = image.get() cls.cleanup_base_container = True assert(cls.base_container is not None) # Set up the zygote host path. cls.shared_host_dir = lxc.SharedHostDir( os.path.join(cls.test_dir, 'host')) @classmethod def tearDownClass(cls): cls.base_container = None if not unittest_setup.config.skip_cleanup: if cls.cleanup_base_container: lxc.BaseImage().cleanup() cls.shared_host_dir.cleanup() shutil.rmtree(cls.test_dir) def testCleanup(self): """Verifies that the zygote cleans up after itself.""" with self.createZygote() as zygote: host_path = zygote.host_path self.assertTrue(os.path.isdir(host_path)) # Start/stop the zygote to exercise the host mounts. zygote.start(wait_for_network=False) zygote.stop() # After the zygote is destroyed, verify that the host path is cleaned # up. self.assertFalse(os.path.isdir(host_path)) def testCleanupWithUnboundHostDir(self): """Verifies that cleanup works when the host dir is unbound.""" with self.createZygote() as zygote: host_path = zygote.host_path self.assertTrue(os.path.isdir(host_path)) # Don't start the zygote, so the host mount is not bound. # After the zygote is destroyed, verify that the host path is cleaned # up. self.assertFalse(os.path.isdir(host_path)) def testCleanupWithNoHostDir(self): """Verifies that cleanup works when the host dir is missing.""" with self.createZygote() as zygote: host_path = zygote.host_path utils.run('sudo rmdir %s' % zygote.host_path) self.assertFalse(os.path.isdir(host_path)) # Zygote destruction should yield no errors if the host path is # missing. def testHostDir(self): """Verifies that the host dir on the container is created, and correctly bind-mounted.""" with self.createZygote() as zygote: self.assertIsNotNone(zygote.host_path) self.assertTrue(os.path.isdir(zygote.host_path)) zygote.start(wait_for_network=False) self.verifyBindMount( zygote, container_path=lxc.CONTAINER_HOST_DIR, host_path=zygote.host_path) def testHostDirExists(self): """Verifies that the host dir is just mounted if it already exists.""" # Pre-create the host dir and put a file in it. test_host_path = os.path.join(self.shared_host_dir.path, 'testHostDirExists') test_filename = 'test_file' test_host_file = os.path.join(test_host_path, test_filename) test_string = 'jackdaws love my big sphinx of quartz.' os.makedirs(test_host_path) with open(test_host_file, 'w') as f: f.write(test_string) # Sanity check self.assertTrue(lxc_utils.path_exists(test_host_file)) with self.createZygote(host_path=test_host_path) as zygote: zygote.start(wait_for_network=False) self.verifyBindMount( zygote, container_path=lxc.CONTAINER_HOST_DIR, host_path=zygote.host_path) # Verify that the old directory contents was preserved. cmd = 'cat %s' % os.path.join(lxc.CONTAINER_HOST_DIR, test_filename) test_output = zygote.attach_run(cmd).stdout.strip() self.assertEqual(test_string, test_output) def testInstallSsp(self): """Verifies that installing the ssp in the container works.""" # Hard-coded path to some golden data for this test. test_ssp = os.path.join( common.autotest_dir, 'site_utils', 'lxc', 'test', 'test_ssp.tar.bz2') # Create a container, install the self-served ssp, then check that it is # installed into the container correctly. with self.createZygote() as zygote: # Note: start the zygote first, then install the SSP. This mimics # the way things would work in the production environment. zygote.start(wait_for_network=False) with unittest_http.serve_locally(test_ssp) as url: zygote.install_ssp(url) # The test ssp just contains a couple of text files, in known # locations. Verify the location and content of those files in the # container. cat = lambda path: zygote.attach_run('cat %s' % path).stdout test0 = cat(os.path.join(constants.CONTAINER_AUTOTEST_DIR, 'test.0')) test1 = cat(os.path.join(constants.CONTAINER_AUTOTEST_DIR, 'dir0', 'test.1')) self.assertEquals('the five boxing wizards jumped quickly', test0) self.assertEquals('the quick brown fox jumps over the lazy dog', test1) def testInstallControlFile(self): """Verifies that installing a control file in the container works.""" _unused, tmpfile = tempfile.mkstemp() with self.createZygote() as zygote: # Note: start the zygote first. This mimics the way things would # work in the production environment. zygote.start(wait_for_network=False) zygote.install_control_file(tmpfile) # Verify that the file is found in the zygote. zygote.attach_run( 'test -f %s' % os.path.join(lxc.CONTROL_TEMP_PATH, os.path.basename(tmpfile))) def testCopyFile(self): """Verifies that files are correctly copied into the container.""" control_string = 'amazingly few discotheques provide jukeboxes' with tempfile.NamedTemporaryFile() as tmpfile: tmpfile.write(control_string) tmpfile.flush() with self.createZygote() as zygote: dst = os.path.join(constants.CONTAINER_AUTOTEST_DIR, os.path.basename(tmpfile.name)) zygote.start(wait_for_network=False) zygote.copy(tmpfile.name, dst) # Verify the file content. test_string = zygote.attach_run('cat %s' % dst).stdout self.assertEquals(control_string, test_string) def testCopyDirectory(self): """Verifies that directories are correctly copied into the container.""" control_string = 'pack my box with five dozen liquor jugs' with lxc_utils.TempDir() as tmpdir: fd, tmpfile = tempfile.mkstemp(dir=tmpdir) f = os.fdopen(fd, 'w') f.write(control_string) f.close() with self.createZygote() as zygote: dst = os.path.join(constants.CONTAINER_AUTOTEST_DIR, os.path.basename(tmpdir)) zygote.start(wait_for_network=False) zygote.copy(tmpdir, dst) # Verify the file content. test_file = os.path.join(dst, os.path.basename(tmpfile)) test_string = zygote.attach_run('cat %s' % test_file).stdout self.assertEquals(control_string, test_string) def testFindHostMount(self): """Verifies that zygotes pick up the correct host dirs.""" with self.createZygote() as zygote0: # Not a clone, this just instantiates zygote1 on top of the LXC # container created by zygote0. zygote1 = lxc.Zygote(container_path=zygote0.container_path, name=zygote0.name, attribute_values={}) # Verify that the new zygote picked up the correct host path # from the existing LXC container. self.assertEquals(zygote0.host_path, zygote1.host_path) self.assertEquals(zygote0.host_path_ro, zygote1.host_path_ro) def testDetectExistingMounts(self): """Verifies that host mounts are properly reconstructed. When a Zygote is instantiated on top of an already-running container, any previously-created bind mounts have to be detected. This enables proper cleanup later. """ with lxc_utils.TempDir() as tmpdir, self.createZygote() as zygote0: zygote0.start(wait_for_network=False) # Create a bind mounted directory. zygote0.mount_dir(tmpdir, 'foo') # Create another zygote on top of the existing container. zygote1 = lxc.Zygote(container_path=zygote0.container_path, name=zygote0.name, attribute_values={}) # Verify that the new zygote contains the same bind mounts. self.assertEqual(zygote0.mounts, zygote1.mounts) def testMountDirectory(self): """Verifies that read-write mounts work.""" with lxc_utils.TempDir() as tmpdir, self.createZygote() as zygote: dst = '/testMountDirectory/testMount' zygote.start(wait_for_network=False) zygote.mount_dir(tmpdir, dst, readonly=False) # Verify that the mount point is correctly bound, and is read-write. self.verifyBindMount(zygote, dst, tmpdir) zygote.attach_run('test -r {0} -a -w {0}'.format(dst)) def testMountDirectoryReadOnly(self): """Verifies that read-only mounts are mounted, and read-only.""" with lxc_utils.TempDir() as tmpdir, self.createZygote() as zygote: dst = '/testMountDirectoryReadOnly/testMount' zygote.start(wait_for_network=False) zygote.mount_dir(tmpdir, dst, readonly=True) # Verify that the mount point is correctly bound, and is read-only. self.verifyBindMount(zygote, dst, tmpdir) try: zygote.attach_run('test -r {0} -a ! -w {0}'.format(dst)) except error.CmdError: self.fail('Bind mount is not read-only') def testMountDirectoryRelativePath(self): """Verifies that relative-path mounts work.""" with lxc_utils.TempDir() as tmpdir, self.createZygote() as zygote: dst = 'testMountDirectoryRelativePath/testMount' zygote.start(wait_for_network=False) zygote.mount_dir(tmpdir, dst, readonly=True) # Verify that the mount points is correctly bound.. self.verifyBindMount(zygote, dst, tmpdir) @contextmanager def createZygote(self, name = None, attribute_values = None, snapshot = True, host_path = None): """Clones a zygote from the test base container. Use this to ensure that zygotes got properly cleaned up after each test. @param container_path: The LXC path for the new container. @param host_path: The host path for the new container. @param name: The name of the new container. @param attribute_values: Any attribute values for the new container. @param snapshot: Whether to create a snapshot clone. """ if name is None: name = self.id().split('.')[-1] if host_path is None: host_path = os.path.join(self.shared_host_dir.path, name) if attribute_values is None: attribute_values = {} zygote = lxc.Zygote(self.test_dir, name, attribute_values, self.base_container, snapshot, host_path) try: yield zygote finally: if not unittest_setup.config.skip_cleanup: zygote.destroy() def verifyBindMount(self, container, container_path, host_path): """Verifies that a given path in a container is bind-mounted to a given path in the host system. @param container: The Container instance to be tested. @param container_path: The path in the container to compare. @param host_path: The path in the host system to compare. """ container_inode = (container.attach_run('ls -id %s' % container_path) .stdout.split()[0]) host_inode = utils.run('ls -id %s' % host_path).stdout.split()[0] # Compare the container and host inodes - they should match. self.assertEqual(container_inode, host_inode) if __name__ == '__main__': unittest.main()