Create Zygote, a configurable container.
Create a new subclass of Container called Zygote. Zygote adds support
for a host directory, which is a shared bind-mount that enables the host
system to bind/copy additional files/directories into a Container after
it has been started.
BUG=chromium:720219
TEST=sudo python lxc_functional_test.py -v
2017-06-29 15:07:03,348 All tests passed.
TEST=sudo python zygote_unittest.py -v
Ran 8 tests in 153.343s
OK (skipped=1)
Change-Id: I5fbcd31edd1e6fc34b6531640cbf58a2f34b9c18
Reviewed-on: https://chromium-review.googlesource.com/557061
Commit-Ready: Ben Kwa <kenobi@chromium.org>
Tested-by: Ben Kwa <kenobi@chromium.org>
Reviewed-by: Dan Shi <dshi@google.com>
diff --git a/global_config.ini b/global_config.ini
index 32bc64c..426e4dd 100644
--- a/global_config.ini
+++ b/global_config.ini
@@ -74,6 +74,8 @@
# Directory stores LXC containers
container_path: /usr/local/autotest/containers
+# Shared mount point for host mounts for LXC containers.
+container_shared_host_path: /usr/local/autotest/containers/host
# `container_base` is replaced by `container_base_folder_url` and `container_base_name`
# The setting is kept for backwards compatibility reason.
diff --git a/site_utils/lxc/__init__.py b/site_utils/lxc/__init__.py
index 8ee0426..d260302 100644
--- a/site_utils/lxc/__init__.py
+++ b/site_utils/lxc/__init__.py
@@ -16,3 +16,4 @@
from lxc import install_package
from lxc import install_packages
from lxc import install_python_package
+from zygote import Zygote
diff --git a/site_utils/lxc/constants.py b/site_utils/lxc/constants.py
index 910bf8e..9fbe485 100644
--- a/site_utils/lxc/constants.py
+++ b/site_utils/lxc/constants.py
@@ -38,6 +38,10 @@
# Default directory used to store LXC containers.
DEFAULT_CONTAINER_PATH = global_config.get_config_value('AUTOSERV',
'container_path')
+# Default directory for host mounts
+DEFAULT_SHARED_HOST_PATH = global_config.get_config_value(
+ 'AUTOSERV',
+ 'container_shared_host_path')
# Path to drone_temp folder in the container, which stores the control file for
# test job to run.
diff --git a/site_utils/lxc/container.py b/site_utils/lxc/container.py
index a4003e2..d1044d6 100644
--- a/site_utils/lxc/container.py
+++ b/site_utils/lxc/container.py
@@ -13,6 +13,7 @@
from autotest_lib.site_utils.lxc import config as lxc_config
from autotest_lib.site_utils.lxc import constants
from autotest_lib.site_utils.lxc import lxc
+from autotest_lib.site_utils.lxc import utils as lxc_utils
try:
from chromite.lib import metrics
@@ -44,20 +45,73 @@
The attributes available are defined in ATTRIBUTES constant.
"""
- def __init__(self, container_path, attribute_values):
+ def __init__(self, container_path, name, attribute_values, src=None,
+ snapshot=False):
"""Initialize an object of LXC container with given attribute values.
@param container_path: Directory that stores the container.
+ @param name: Name of the container.
@param attribute_values: A dictionary of attribute values for the
container.
+ @param src: An optional source container. If provided, the source
+ continer is cloned, and the new container will point to the
+ clone.
+ @param snapshot: If a source container was specified, this argument
+ specifies whether or not to create a snapshot clone.
+ The default is to attempt to create a snapshot.
+ If a snapshot is requested and creating the snapshot
+ fails, a full clone will be attempted.
"""
self.container_path = os.path.realpath(container_path)
# Path to the rootfs of the container. This will be initialized when
# property rootfs is retrieved.
self._rootfs = None
+ self.name = name
for attribute, value in attribute_values.iteritems():
setattr(self, attribute, value)
+ # Clone the container
+ if src is not None:
+ # Clone the source container to initialize this one.
+ lxc_utils.clone(src.container_path, src.name, self.container_path,
+ self.name, snapshot)
+
+
+ @classmethod
+ def createFromExistingDir(cls, lxc_path, name, **kwargs):
+ """Creates a new container instance for an lxc container that already
+ exists on disk.
+
+ @param lxc_path: The LXC path for the container.
+ @param name: The container name.
+
+ @raise error.ContainerError: If the container doesn't already exist.
+
+ @return: The new container.
+ """
+ container = cls(lxc_path, name, kwargs)
+ container.refresh_status()
+ return container
+
+
+ @classmethod
+ def clone(cls, src, new_name, new_path=None, snapshot=False):
+ """Creates a clone of this container.
+
+ @param src: The original container.
+ @param new_name: Name for the cloned container.
+ @param new_path: LXC path for the cloned container (optional; if not
+ specified, the new container is created in the same directory as
+ this container).
+ @param snapshot: Whether to snapshot, or create a full clone.
+ @param cleanup: If a container with the given name and path already
+ exist, clean it up first.
+ """
+ if new_path is None:
+ new_path = src.container_path
+
+ return cls(new_path, new_name, {}, src, snapshot)
+
def refresh_status(self):
"""Refresh the status information of the container.
@@ -66,7 +120,7 @@
if not containers:
raise error.ContainerError(
'No container found in directory %s with name of %s.' %
- self.container_path, self.name)
+ (self.container_path, self.name))
attribute_values = containers[0]
for attribute, value in attribute_values.iteritems():
setattr(self, attribute, value)
@@ -107,8 +161,8 @@
'in the container config file is %s' %
(self.name, lxc_rootfs_config))
lxc_rootfs = match.group(1)
- self.clone_from_snapshot = ':' in lxc_rootfs
- if self.clone_from_snapshot:
+ cloned_from_snapshot = ':' in lxc_rootfs
+ if cloned_from_snapshot:
self._rootfs = lxc_rootfs.split(':')[-1]
else:
self._rootfs = lxc_rootfs
@@ -160,8 +214,7 @@
"""
cmd = 'sudo lxc-start -P %s -n %s -d' % (self.container_path, self.name)
output = utils.run(cmd).stdout
- self.refresh_status()
- if self.state != 'RUNNING':
+ if not self.is_running():
raise error.ContainerError(
'Container %s failed to start. lxc command output:\n%s' %
(os.path.join(self.container_path, self.name),
@@ -295,3 +348,9 @@
"\"local\/lib\",\\n/g' %s" % site_module)
self.attach_run('sed -i "s/lib_placeholder/lib/g" %s' %
site_module)
+
+
+ def is_running(self):
+ """Returns whether or not this container is currently running."""
+ self.refresh_status()
+ return self.state == 'RUNNING'
diff --git a/site_utils/lxc/container_bucket.py b/site_utils/lxc/container_bucket.py
index 6f7d73f..4dc4d83 100644
--- a/site_utils/lxc/container_bucket.py
+++ b/site_utils/lxc/container_bucket.py
@@ -1,4 +1,4 @@
-# Copyright 2015 The Chromium Authors. All rights reserved.
+# 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.
@@ -28,7 +28,9 @@
"""A wrapper class to interact with containers in a specific container path.
"""
- def __init__(self, container_path=constants.DEFAULT_CONTAINER_PATH):
+ def __init__(self,
+ container_path=constants.DEFAULT_CONTAINER_PATH,
+ shared_host_path = constants.DEFAULT_SHARED_HOST_PATH):
"""Initialize a ContainerBucket.
@param container_path: Path to the directory used to store containers.
@@ -36,6 +38,13 @@
global config.
"""
self.container_path = os.path.realpath(container_path)
+ self.shared_host_path = os.path.realpath(shared_host_path)
+ # Try to create the base container.
+ try:
+ self.base_container = Container.createFromExistingDir(
+ container_path, constants.BASE);
+ except error.ContainerError:
+ self.base_container = None
def get_all(self):
@@ -47,7 +56,8 @@
info_collection = lxc.get_container_info(self.container_path)
containers = {}
for info in info_collection:
- container = Container(self.container_path, info)
+ container = Container.createFromExistingDir(self.container_path,
+ **info)
containers[container.name] = container
return containers
@@ -82,6 +92,7 @@
containers, key=lambda n: 1 if n.name == constants.BASE else 0):
logging.info('Destroy container %s.', container.name)
container.destroy()
+ self._cleanup_shared_host_path()
@metrics.SecondsTimerDecorator(
@@ -158,7 +169,7 @@
if not cleanup:
raise error.ContainerError('Container %s already exists.' %
new_name)
- container = Container(new_path, {'name': name})
+ container = Container.createFromExistingDir(new_path, new_name)
try:
container.destroy()
except error.CmdError as e:
@@ -168,15 +179,7 @@
name, e)
utils.run('sudo rm -rf "%s"' % container_folder)
- snapshot_arg = '-s' if snapshot else ''
- # overlayfs is the default clone backend storage. However it is not
- # supported in Ganeti yet. Use aufs as the alternative.
- aufs_arg = '-B aufs' if utils.is_vm() and snapshot else ''
- cmd = (('sudo lxc-clone --lxcpath %s --newpath %s '
- '--orig %s --new %s %s %s') %
- (path, new_path, name, new_name, snapshot_arg, aufs_arg))
-
- utils.run(cmd)
+ lxc_utils.clone(path, name, new_path, new_name, snapshot)
return self.get(new_name)
@@ -238,6 +241,41 @@
utils.run('sudo sed -i "s|container_dir|%s|g" "%s"' %
(self.container_path, config_path))
+ self.base_container = Container.createFromExistingDir(
+ self.container_path, constants.BASE)
+
+ self._setup_shared_host_path()
+
+
+ def _setup_shared_host_path(self):
+ """Sets up the shared host directory."""
+ # First, clear out the old shared host dir if it exists.
+ if lxc_utils.path_exists(self.shared_host_path):
+ self._cleanup_shared_host_path()
+ # Create the dir and set it up as a shared mount point.
+ utils.run(('sudo mkdir "{path}" && '
+ 'sudo mount --bind "{path}" "{path}" && '
+ 'sudo mount --make-unbindable "{path}" && '
+ 'sudo mount --make-shared "{path}"')
+ .format(path=self.shared_host_path))
+
+
+ def _cleanup_shared_host_path(self):
+ """Removes the shared host directory.
+
+ This should only be called after all containers have been destroyed
+ (i.e. all host mounts have been disconnected and removed, so the shared
+ host directory should be empty).
+ """
+ if not os.path.exists(self.shared_host_path):
+ return
+
+ if len(os.listdir(self.shared_host_path)) > 0:
+ raise RuntimeError('Attempting to clean up host dir before all '
+ 'hosts have been disconnected')
+ utils.run('sudo umount "{path}" && sudo rmdir "{path}"'
+ .format(path=self.shared_host_path))
+
@metrics.SecondsTimerDecorator(
'%s/setup_test_duration' % constants.STATS_KEY)
diff --git a/site_utils/lxc/container_bucket_unittest.py b/site_utils/lxc/container_bucket_unittest.py
new file mode 100644
index 0000000..4d6361e
--- /dev/null
+++ b/site_utils/lxc/container_bucket_unittest.py
@@ -0,0 +1,91 @@
+#!/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 argparse
+import logging
+import os
+import shutil
+import tempfile
+import unittest
+
+import common
+from autotest_lib.site_utils import lxc
+from autotest_lib.site_utils.lxc import unittest_logging
+
+
+options = None
+
+class ContainerBucketTests(unittest.TestCase):
+ """Unit tests for the ContainerBucket class."""
+
+ @classmethod
+ def setUpClass(cls):
+ cls.container_path = tempfile.mkdtemp(
+ dir=lxc.DEFAULT_CONTAINER_PATH,
+ prefix='container_bucket_unittest_')
+
+
+ @classmethod
+ def tearDownClass(cls):
+ shutil.rmtree(cls.container_path)
+
+
+ def setUp(self):
+ self.tmpdir = tempfile.mkdtemp()
+
+
+ def tearDown(self):
+ shutil.rmtree(self.tmpdir)
+
+
+ def testHostDirCreationAndCleanup(self):
+ """Verifies that the host dir is properly created and cleaned up when
+ the container bucket is set up and destroyed.
+ """
+ shared_host_path = os.path.realpath(os.path.join(self.tmpdir, 'host'))
+ bucket = lxc.ContainerBucket(self.container_path, shared_host_path)
+
+ # Verify the host path in the container bucket.
+ self.assertEqual(os.path.realpath(bucket.shared_host_path),
+ shared_host_path)
+
+ # Set up, verify that the path is created.
+ bucket.setup_base()
+ self.assertTrue(os.path.isdir(shared_host_path))
+
+ # Clean up, verify that the path is removed.
+ bucket.destroy_all()
+ self.assertFalse(os.path.isdir(shared_host_path))
+
+
+ def testHostDirMissing(self):
+ """Verifies that a missing host dir does not cause container bucket
+ destruction to crash.
+ """
+ shared_host_path = os.path.realpath(os.path.join(self.tmpdir, 'host'))
+ bucket = lxc.ContainerBucket(self.container_path, shared_host_path)
+
+ # Verify that the host path does not exist.
+ self.assertFalse(os.path.exists(shared_host_path))
+ # Do not call startup, just call destroy. This should not throw.
+ bucket.destroy_all()
+
+
+def parse_options():
+ """Parse command line inputs."""
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-v', '--verbose', action='store_true',
+ help='Print out ALL entries.')
+ args, _unused = parser.parse_known_args()
+ return args
+
+
+if __name__ == '__main__':
+ options = parse_options()
+
+ log_level=(logging.DEBUG if options.verbose else logging.INFO)
+ unittest_logging.setup(log_level)
+
+ unittest.main()
diff --git a/site_utils/lxc/lxc_functional_test.py b/site_utils/lxc/lxc_functional_test.py
index 9f5a623..080aebb 100644
--- a/site_utils/lxc/lxc_functional_test.py
+++ b/site_utils/lxc/lxc_functional_test.py
@@ -15,13 +15,13 @@
import argparse
import logging
import os
-import sys
import tempfile
import time
import common
from autotest_lib.client.bin import utils
from autotest_lib.site_utils import lxc
+from autotest_lib.site_utils.lxc import unittest_logging
TEST_JOB_ID = 123
@@ -175,21 +175,6 @@
"""
-def setup_logging(log_level=logging.INFO):
- """Direct logging to stdout.
-
- @param log_level: Level of logging to redirect to stdout, default to INFO.
- """
- logger = logging.getLogger()
- logger.setLevel(log_level)
- handler = logging.StreamHandler(sys.stdout)
- handler.setLevel(log_level)
- formatter = logging.Formatter('%(asctime)s %(message)s')
- handler.setFormatter(formatter)
- logger.handlers = []
- logger.addHandler(handler)
-
-
def setup_base(bucket):
"""Test setup base container works.
@@ -352,8 +337,8 @@
'grant root access to this process.')
utils.run('sudo true')
- setup_logging(log_level=(logging.DEBUG if options.verbose
- else logging.INFO))
+ log_level=(logging.DEBUG if options.verbose else logging.INFO)
+ unittest_logging.setup(log_level)
bucket = lxc.ContainerBucket(TEMP_DIR)
diff --git a/site_utils/lxc/unittest_cleanup.py b/site_utils/lxc/unittest_cleanup.py
new file mode 100644
index 0000000..fe5e5d4
--- /dev/null
+++ b/site_utils/lxc/unittest_cleanup.py
@@ -0,0 +1,36 @@
+#!/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 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 utils as lxc_utils
+
+
+TEST_CONTAINER_PATH = os.path.join(lxc.DEFAULT_CONTAINER_PATH, 'test')
+TEST_HOST_PATH = os.path.join(TEST_CONTAINER_PATH, 'host')
+
+def main():
+ """Clean up the remnants from any old aborted unit tests."""
+ # Manually clean out the host dir.
+ if lxc_utils.path_exists(TEST_HOST_PATH):
+ for host_dir in os.listdir(TEST_HOST_PATH):
+ host_dir = os.path.realpath(os.path.join(TEST_HOST_PATH, host_dir))
+ try:
+ utils.run('sudo umount %s' % host_dir)
+ except error.CmdError:
+ pass
+ utils.run('sudo rm -r %s' % host_dir)
+
+ # Utilize the container_bucket to clear out old test containers.
+ bucket = lxc.ContainerBucket(TEST_CONTAINER_PATH, TEST_HOST_PATH)
+ bucket.destroy_all()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/site_utils/lxc/unittest_logging.py b/site_utils/lxc/unittest_logging.py
new file mode 100644
index 0000000..b19750b
--- /dev/null
+++ b/site_utils/lxc/unittest_logging.py
@@ -0,0 +1,21 @@
+# 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 logging
+import sys
+
+
+def setup(log_level=logging.INFO):
+ """Direct logging to stdout.
+
+ @param log_level: Level of logging to redirect to stdout, default to INFO.
+ """
+ logger = logging.getLogger()
+ logger.setLevel(log_level)
+ handler = logging.StreamHandler(sys.stdout)
+ handler.setLevel(log_level)
+ formatter = logging.Formatter('%(asctime)s %(message)s')
+ handler.setFormatter(formatter)
+ logger.handlers = []
+ logger.addHandler(handler)
diff --git a/site_utils/lxc/utils.py b/site_utils/lxc/utils.py
index 29ad241..310b121 100644
--- a/site_utils/lxc/utils.py
+++ b/site_utils/lxc/utils.py
@@ -53,3 +53,42 @@
interface_names)
netif = interface.Interface(lxc_network)
return netif.ipv4_address
+
+def clone(lxc_path, src_name, new_path, dst_name, snapshot):
+ """Clones a container.
+
+ @param lxc_path: The LXC path of the source container.
+ @param src_name: The name of the source container.
+ @param new_path: The LXC path of the destination container.
+ @param dst_name: The name of the destination container.
+ @param snapshot: Whether or not to create a snapshot clone.
+ """
+ snapshot_arg = '-s' if snapshot else ''
+ # overlayfs is the default clone backend storage. However it is not
+ # supported in Ganeti yet. Use aufs as the alternative.
+ aufs_arg = '-B aufs' if utils.is_vm() and snapshot else ''
+ cmd = (('sudo lxc-clone --lxcpath {lxcpath} --newpath {newpath} '
+ '--orig {orig} --new {new} {snapshot} {backing}')
+ .format(
+ lxcpath = lxc_path,
+ newpath = new_path,
+ orig = src_name,
+ new = dst_name,
+ snapshot = snapshot_arg,
+ backing = aufs_arg
+ ))
+ utils.run(cmd)
+
+
+def cleanup_host_mount(host_dir):
+ """Unmounts and removes the given host dir.
+
+ @param host_dir: The host dir to unmount and remove.
+ """
+ try:
+ utils.run('sudo umount "%s"' % host_dir)
+ except error.CmdError:
+ # Ignore errors. Most likely this occurred because the host dir
+ # was already unmounted.
+ pass
+ utils.run('sudo rm -r "%s"' % host_dir)
diff --git a/site_utils/lxc/zygote.py b/site_utils/lxc/zygote.py
new file mode 100644
index 0000000..fbb9ee7
--- /dev/null
+++ b/site_utils/lxc/zygote.py
@@ -0,0 +1,84 @@
+# Copyright 2017 The Chromium 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 common
+from autotest_lib.client.bin import utils
+from autotest_lib.site_utils.lxc import Container
+from autotest_lib.site_utils.lxc import config as lxc_config
+from autotest_lib.site_utils.lxc import constants
+from autotest_lib.site_utils.lxc import utils as lxc_utils
+
+
+class Zygote(Container):
+ """A Container that implements post-bringup configuration.
+ """
+
+ def __init__(self, container_path, name, attribute_values, src=None,
+ snapshot=False, host_path=None):
+ """Initialize an object of LXC container with given attribute values.
+
+ @param container_path: Directory that stores the container.
+ @param name: Name of the container.
+ @param attribute_values: A dictionary of attribute values for the
+ container.
+ @param src: An optional source container. If provided, the source
+ continer is cloned, and the new container will point to the
+ clone.
+ @param snapshot: Whether or not to create a snapshot clone. By default,
+ this is false. If a snapshot is requested and creating
+ a snapshot clone fails, a full clone will be attempted.
+ @param host_path: If set to None (the default), a host path will be
+ generated based on constants.DEFAULT_SHARED_HOST_PATH.
+ Otherwise, this can be used to override the host path
+ of the new container, for testing purposes.
+ """
+ super(Zygote, self).__init__(container_path, name, attribute_values,
+ src, snapshot)
+
+ # Initialize host dir and mount
+ if host_path is None:
+ self.host_path = os.path.join(
+ os.path.realpath(constants.DEFAULT_SHARED_HOST_PATH),
+ self.name)
+ else:
+ self.host_path = host_path
+
+ if src is not None:
+ # If creating a new zygote, initialize the host dir.
+ if not lxc_utils.path_exists(self.host_path):
+ utils.run('sudo mkdir %s' % self.host_path)
+ self.mount_dir(self.host_path, lxc_config.CONTAINER_AUTOTEST_DIR)
+
+
+ def destroy(self, force=True):
+ super(Zygote, self).destroy(force)
+ if lxc_utils.path_exists(self.host_path):
+ self._cleanup_host_mount()
+
+
+ def set_hostname(self, hostname):
+ """Sets the hostname within the container.
+
+ @param hostname The new container hostname.
+ """
+ if self.is_running():
+ self.attach_run('hostname %s' % (hostname))
+ self.attach_run(constants.APPEND_CMD_FMT % {
+ 'content': '127.0.0.1 %s' % (hostname),
+ 'file': '/etc/hosts'})
+ else:
+ config_file = os.path.join(self.container_path, self.name, 'config')
+ lxc_utsname_setting = (
+ 'lxc.utsname = ' +
+ constants.CONTAINER_UTSNAME_FORMAT % hostname)
+ utils.run(
+ constants.APPEND_CMD_FMT % {'content': lxc_utsname_setting,
+ 'file': config_file})
+
+
+ def _cleanup_host_mount(self):
+ """Unmount and remove the host dir for this container."""
+ lxc_utils.cleanup_host_mount(self.host_path);
diff --git a/site_utils/lxc/zygote_unittest.py b/site_utils/lxc/zygote_unittest.py
new file mode 100644
index 0000000..d4ffdc0
--- /dev/null
+++ b/site_utils/lxc/zygote_unittest.py
@@ -0,0 +1,250 @@
+#!/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 argparse
+import logging
+import os
+import tempfile
+import shutil
+import sys
+import unittest
+from contextlib import contextmanager
+
+import common
+from autotest_lib.client.bin import utils
+from autotest_lib.site_utils import lxc
+from autotest_lib.site_utils.lxc import config as lxc_config
+from autotest_lib.site_utils.lxc import unittest_logging
+from autotest_lib.site_utils.lxc import utils as lxc_utils
+
+
+options = None
+
+class ZygoteTests(unittest.TestCase):
+ """Unit tests for the Zygote class."""
+
+ @classmethod
+ def setUpClass(cls):
+ cls.test_dir = tempfile.mkdtemp(dir=lxc.DEFAULT_CONTAINER_PATH,
+ prefix='zygote_unittest_')
+ cls.shared_host_path = os.path.join(cls.test_dir, 'host')
+
+ # Use a container bucket just to download and set up the base image.
+ cls.bucket = lxc.ContainerBucket(cls.test_dir, cls.shared_host_path)
+
+ if cls.bucket.base_container is None:
+ logging.debug('Base container not found - reinitializing')
+ cls.bucket.setup_base()
+
+ cls.base_container = cls.bucket.base_container
+ assert(cls.base_container is not None)
+
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.base_container = None
+ if not options.skip_cleanup:
+ cls.bucket.destroy_all()
+ shutil.rmtree(cls.test_dir)
+
+ def tearDown(self):
+ # Ensure host dirs from each test are completely destroyed.
+ for host_dir in os.listdir(self.shared_host_path):
+ host_dir = os.path.realpath(os.path.join(self.shared_host_path,
+ host_dir))
+ lxc_utils.cleanup_host_mount(host_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 testHostname(self):
+ """Verifies that the zygote starts up with a default hostname that is
+ the lxc container name."""
+ test_name = 'testHostname'
+ with self.createZygote(name=test_name) as zygote:
+ zygote.start(wait_for_network=True)
+ hostname = zygote.attach_run('hostname -f').stdout.strip()
+ self.assertEqual(test_name, hostname)
+
+
+ @unittest.skip('Setting the container hostname using lxc.utsname does not'
+ 'work on goobuntu.')
+ def testSetHostnameNotRunning(self):
+ """Verifies that the hostname can be set on a stopped container."""
+ with self.createZygote() as zygote:
+ expected_hostname = 'my-new-hostname'
+ zygote.set_hostname(expected_hostname)
+ zygote.start(wait_for_network=True)
+ hostname = zygote.attach_run('hostname -f').stdout.strip()
+ self.assertEqual(expected_hostname, hostname)
+
+
+ def testSetHostnameRunning(self):
+ """Verifies that the hostname can be set on a running container."""
+ with self.createZygote() as zygote:
+ expected_hostname = 'my-new-hostname'
+ zygote.start(wait_for_network=True)
+ zygote.set_hostname(expected_hostname)
+ hostname = zygote.attach_run('hostname -f').stdout.strip()
+ self.assertEqual(expected_hostname, hostname)
+
+
+ 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_config.CONTAINER_AUTOTEST_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_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.mkdir(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_config.CONTAINER_AUTOTEST_DIR,
+ host_path=zygote.host_path)
+
+ # Verify that the old directory contents was preserved.
+ cmd = 'cat %s' % os.path.join(lxc_config.CONTAINER_AUTOTEST_DIR,
+ test_filename)
+ test_output = zygote.attach_run(cmd).stdout.strip()
+ self.assertEqual(test_string, test_output)
+
+
+ @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_path, name)
+ if attribute_values is None:
+ attribute_values = {}
+ zygote = lxc.Zygote(self.test_dir,
+ name,
+ attribute_values,
+ self.base_container,
+ snapshot,
+ host_path)
+ yield zygote
+ if not options.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)
+
+
+def parse_options():
+ """Parse command line inputs.
+ """
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-v', '--verbose', action='store_true',
+ help='Print out ALL entries.')
+ parser.add_argument('--skip_cleanup', action='store_true',
+ help='Skip deleting test containers.')
+ args, argv = parser.parse_known_args()
+
+ # Hack: python unittest also processes args. Construct an argv to pass to
+ # it, that filters out the options it won't recognize.
+ if args.verbose:
+ argv.append('-v')
+ argv.insert(0, sys.argv[0])
+
+ return args, argv
+
+
+if __name__ == '__main__':
+ options, unittest_argv = parse_options()
+
+
+ log_level=(logging.DEBUG if options.verbose else logging.INFO)
+ unittest_logging.setup(log_level)
+
+ unittest.main(argv=unittest_argv)
diff --git a/utils/unittest_suite.py b/utils/unittest_suite.py
index cdeea33..76631b2 100755
--- a/utils/unittest_suite.py
+++ b/utils/unittest_suite.py
@@ -106,8 +106,10 @@
# crbug.com/432621 These files are not tests, and will disappear soon.
'des_01_test.py',
'des_02_test.py',
- # Rquire lxc to be installed
+ # Require lxc to be installed
+ 'container_bucket_unittest.py',
'lxc_functional_test.py',
+ 'zygote_unittest.py',
# Require sponge utils installed in site-packages
'sponge_utils_functional_test.py',
))