[autotest] Refactor ContainerBucket.

Remove base container management code from ContainerBucket.  This change:

- simplifies the ContainerBucket,

- makes it clearer that the base image setup is something that is
  performed separately from normal ContainerBucket operation,

- starts the process of decoupling container creation from the
  ContainerBucket, which will be necesary to move to a container pool.

BUG=chromium:720219

TEST=sudo python base_image_unittest.py
TEST=sudo python container_bucket_unittest.py
TEST=sudo python container_unittest.py
TEST=sudo python lxc_config_unittest.py
TEST=sudo python lxc_functional_test.py
TEST=sudo python zygote_unittest.py

Change-Id: I719f7b113729c7387ddacaec77c4c63154d52ea0
Reviewed-on: https://chromium-review.googlesource.com/636033
Commit-Ready: Ben Kwa <[email protected]>
Tested-by: Ben Kwa <[email protected]>
Reviewed-by: Ben Kwa <[email protected]>
diff --git a/site_utils/lxc.py b/site_utils/lxc.py
index ed1033c..8595163 100755
--- a/site_utils/lxc.py
+++ b/site_utils/lxc.py
@@ -57,11 +57,11 @@
         utils.run('sudo true')
 
     options = parse_options()
-    bucket = lxc.ContainerBucket(container_path=options.path)
+    image = lxc.BaseImage(container_path=options.path)
     if options.setup:
-        bucket.setup_base(name=options.name, force_delete=options.force_delete)
+        image.setup(name=options.name, force_delete=options.force_delete)
     elif options.force_delete:
-        bucket.destroy_all()
+        image.cleanup()
 
 
 if __name__ == '__main__':
diff --git a/site_utils/lxc/__init__.py b/site_utils/lxc/__init__.py
index d260302..1fcc7e7 100644
--- a/site_utils/lxc/__init__.py
+++ b/site_utils/lxc/__init__.py
@@ -10,6 +10,7 @@
   5. Cleanup, e.g., destroy the container.
 """
 
+from base_image import BaseImage
 from constants import *
 from container import Container
 from container_bucket import ContainerBucket
diff --git a/site_utils/lxc/base_image.py b/site_utils/lxc/base_image.py
new file mode 100644
index 0000000..e1ae695
--- /dev/null
+++ b/site_utils/lxc/base_image.py
@@ -0,0 +1,193 @@
+# 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 os
+import sys
+
+import common
+from autotest_lib.client.bin import utils
+from autotest_lib.client.common_lib import error
+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
+from autotest_lib.site_utils.lxc.container import Container
+
+
+class BaseImage(object):
+    """A class that manages a base container.
+
+    Instantiating this class will cause it to search for a base container under
+    the given path and name.  If one is found, the class adopts it.  If not, the
+    setup() method needs to be called, to download and install a new base
+    container.
+
+    The actual base container can be obtained by calling the get() method.
+
+    Calling cleanup() will delete the base container along with all of its
+    associated snapshot clones.
+    """
+
+    def __init__(self,
+                 container_path=constants.DEFAULT_CONTAINER_PATH,
+                 base_name=constants.BASE):
+        """Creates a new BaseImage.
+
+        If a valid base container already exists on this machine, the BaseImage
+        adopts it.  Otherwise, setup needs to be called to download a base and
+        install a base container.
+
+        @param container_path: The LXC path for the base container.
+        @param base_name: The base container name.
+        """
+        self.container_path = container_path
+        self.base_name = base_name
+        try:
+            base_container = Container.create_from_existing_dir(
+                    container_path, base_name);
+            base_container.refresh_status()
+            self.base_container = base_container
+        except error.ContainerError:
+            self.base_container = None
+
+
+    def setup(self, name=None, force_delete=False):
+        """Download and setup the base container.
+
+        @param name: Name of the base container, defaults to the name passed to
+                     the constructor.  If a different name is provided, that
+                     name overrides the name originally passed to the
+                     constructor.
+        @param force_delete: True to force to delete existing base container.
+                             This action will destroy all running test
+                             containers. Default is set to False.
+        """
+        if name is not None:
+            self.base_name = name
+
+        if not self.container_path:
+            raise error.ContainerError(
+                    'You must set a valid directory to store containers in '
+                    'global config "AUTOSERV/ container_path".')
+
+        if not os.path.exists(self.container_path):
+            os.makedirs(self.container_path)
+
+        if self.base_container and not force_delete:
+            logging.error(
+                    'Base container already exists. Set force_delete to True '
+                    'to force to re-stage base container. Note that this '
+                    'action will destroy all running test containers')
+            # Set proper file permission. base container in moblab may have
+            # owner of not being root. Force to update the folder's owner.
+            self._set_root_owner()
+            return
+
+        # Destroy existing base container if exists.
+        if self.base_container:
+            self.cleanup()
+
+        try:
+            self._download_and_install_base_container()
+            self._set_root_owner()
+        except:
+            # Clean up if something went wrong.
+            base_path = os.path.join(self.container_path, self.base_name)
+            if lxc_utils.path_exists(base_path):
+                exc_info = sys.exc_info()
+                container = Container.create_from_existing_dir(
+                        self.container_path, self.base_name)
+                # Attempt destroy.  Log but otherwise ignore errors.
+                try:
+                    container.destroy()
+                except error.CmdError as e:
+                    logging.error(e)
+            # Raise the cached exception with original backtrace.
+            raise exc_info[0], exc_info[1], exc_info[2]
+        else:
+            self.base_container = Container.create_from_existing_dir(
+                    self.container_path, self.base_name)
+
+
+    def cleanup(self):
+        """Destroys the base container.
+
+        This operation will also destroy all snapshot clones of the base
+        container.
+        """
+        # Find and delete clones first.
+        for clone in self._find_clones():
+            clone.destroy()
+        base = Container.create_from_existing_dir(self.container_path,
+                                                  self.base_name)
+        base.destroy()
+
+
+    def get(self):
+        """Returns the base container.
+
+        @raise ContainerError: If the base image is invalid or missing.
+        """
+        if self.base_container is None:
+            raise error.ContainerError('Invalid base container.')
+        else:
+            return self.base_container
+
+
+    def _download_and_install_base_container(self):
+        """Downloads the base image, untars and configures it."""
+        base_path = os.path.join(self.container_path, self.base_name)
+        tar_path = os.path.join(self.container_path,
+                                '%s.tar.xz' % self.base_name)
+
+        # Force cleanup of any previously downloaded/installed base containers.
+        # This ensures a clean setup of the new base container.
+        #
+        # TODO(kenobi): Add a check to ensure that the base container doesn't
+        # get deleted while snapshot clones exist (otherwise running tests might
+        # get disrupted).
+        path_to_cleanup = [tar_path, base_path]
+        for path in path_to_cleanup:
+            if os.path.exists(path):
+                utils.run('sudo rm -rf "%s"' % path)
+        container_url = constants.CONTAINER_BASE_URL_FMT % self.base_name
+        lxc.download_extract(container_url, tar_path, self.container_path)
+        # Remove the downloaded container tar file.
+        utils.run('sudo rm "%s"' % tar_path)
+
+        # Update container config with container_path from global config.
+        config_path = os.path.join(base_path, 'config')
+        rootfs_path = os.path.join(base_path, 'rootfs')
+        utils.run(('sudo sed '
+                   '-i "s|\(lxc\.rootfs[[:space:]]*=\).*$|\\1 {rootfs}|" '
+                   '"{config}"').format(rootfs=rootfs_path,
+                                        config=config_path))
+
+    def _set_root_owner(self):
+        """Changes the container group and owner to root.
+
+        This is necessary because we currently run privileged containers.
+        """
+        # TODO(dshi): Change root to current user when test container can be
+        # unprivileged container.
+        base_path = os.path.join(self.container_path, self.base_name)
+        utils.run('sudo chown -R root "%s"' % base_path)
+        utils.run('sudo chgrp -R root "%s"' % base_path)
+
+
+    def _find_clones(self):
+        """Finds snapshot clones of the current base container."""
+        snapshot_file = os.path.join(self.container_path,
+                                     self.base_name,
+                                     'lxc_snapshots')
+        if not lxc_utils.path_exists(snapshot_file):
+            return
+        cmd = 'sudo cat %s' % snapshot_file
+        clone_info = [line.strip()
+                      for line in utils.run(cmd).stdout.splitlines()]
+        # lxc_snapshots contains pairs of lines (lxc_path, container_name).
+        for i in range(0, len(clone_info), 2):
+            lxc_path = clone_info[i]
+            name = clone_info[i+1]
+            yield Container.create_from_existing_dir(lxc_path, name)
diff --git a/site_utils/lxc/base_image_unittest.py b/site_utils/lxc/base_image_unittest.py
new file mode 100644
index 0000000..37c803f
--- /dev/null
+++ b/site_utils/lxc/base_image_unittest.py
@@ -0,0 +1,223 @@
+#!/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 sys
+import tempfile
+import unittest
+from contextlib import contextmanager
+
+import common
+from autotest_lib.client.common_lib import error
+from autotest_lib.site_utils import lxc
+from autotest_lib.site_utils.lxc import BaseImage
+from autotest_lib.site_utils.lxc import constants
+from autotest_lib.site_utils.lxc import unittest_logging
+from autotest_lib.site_utils.lxc import utils as lxc_utils
+
+
+# Namespace object for parsing cmd line options.
+options = None
+test_dir = None
+# A reference to an existing base container that can be copied for tests that
+# need a base container.  This is an optimization.
+reference_container = None
+# The reference container can either be a reference to an existing container, or
+# to a container that was downloaded by this test.  If the latter, then it needs
+# to be cleaned up when the tests are complete.
+cleanup_ref_container = False
+
+
+class BaseImageTests(unittest.TestCase):
+    """Unit tests to verify the BaseImage class."""
+
+    def testCreate_existing(self):
+        """Verifies that BaseImage works with existing base containers."""
+        with TestBaseContainer() as control:
+            manager = BaseImage(control.container_path,
+                                control.name)
+            self.assertIsNotNone(manager.base_container)
+            self.assertEquals(control.container_path,
+                              manager.base_container.container_path)
+            self.assertEquals(control.name, manager.base_container.name)
+            try:
+                manager.base_container.refresh_status()
+            except error.ContainerError:
+                self.fail('Base container was not valid.\n%s' %
+                          error.format_error())
+
+
+    def testCleanup_noClones(self):
+        """Verifies that cleanup cleans up the base image."""
+        base = lxc.Container.clone(src=reference_container,
+                                   new_name=constants.BASE,
+                                   new_path=test_dir,
+                                   snapshot=True)
+
+        manager = BaseImage(base.container_path, base.name)
+        # Precondition: ensure base exists and is a valid container.
+        base.refresh_status()
+
+        manager.cleanup()
+
+        # Verify that the base container was cleaned up.
+        self.assertFalse(lxc_utils.path_exists(
+                os.path.join(base.container_path, base.name)))
+
+
+    def testCleanup_withClones(self):
+        """Verifies that cleanup cleans up the base image.
+
+        Ensure that it works even when clones of the base image exist.
+        """
+        # Do not snapshot, as snapshots of snapshots behave differently than
+        # snapshots of full container clones.  BaseImage cleanup code assumes
+        # that the base container is not a snapshot.
+        base = lxc.Container.clone(src=reference_container,
+                                   new_name=constants.BASE,
+                                   new_path=test_dir,
+                                   snapshot=False)
+        manager = BaseImage(base.container_path, base.name)
+        clones = []
+        for i in range(3):
+            clones.append(lxc.Container.clone(src=base,
+                                              new_name='clone_%d' % i,
+                                              snapshot=True))
+
+
+        # Precondition: all containers are valid.
+        base.refresh_status()
+        for container in clones:
+            container.refresh_status()
+
+        manager.cleanup()
+
+        # Verify that all containers were cleaned up
+        self.assertFalse(lxc_utils.path_exists(
+                os.path.join(base.container_path, base.name)))
+        for container in clones:
+            self.assertFalse(lxc_utils.path_exists(
+                    os.path.join(container.container_path, container.name)))
+
+
+class BaseImageSetupTests(unittest.TestCase):
+    """Unit tests to verify the setup of specific images.
+
+    Some images differ in layout from others.  These tests test specific images
+    to make sure the setup code works for all of them.
+    """
+
+    def setUp(self):
+        self.manager = BaseImage(container_path=test_dir)
+
+
+    def tearDown(self):
+        self.manager.cleanup()
+
+
+    def testSetupBase05(self):
+        """Verifies that setup works for moblab base container.
+
+        Verifies that the code for installing the rootfs location into the
+        lxc config, is working correctly.
+        """
+        # Set up the bucket, then start the base container, and verify it works.
+        self.manager.setup('base_05')
+        container = self.manager.base_container
+
+        container.start(wait_for_network=False)
+        self.assertTrue(container.is_running())
+
+
+    def testSetupBase09(self):
+        """Verifies that setup works for base container.
+
+        Verifies that the code for installing the rootfs location into the
+        lxc config, is working correctly.
+        """
+        self.manager.setup('base_09')
+        container = self.manager.base_container
+
+        container.start(wait_for_network=False)
+        self.assertTrue(container.is_running())
+
+
+@contextmanager
+def TestBaseContainer(name=constants.BASE):
+    """Context manager for creating a scoped base container for testing.
+
+    @param name: (optional) Name of the base container.  If this is not
+                 provided, the default base container name is used.
+    """
+    container = lxc.Container.clone(src=reference_container,
+                                    new_name=name,
+                                    new_path=test_dir,
+                                    snapshot=True,
+                                    cleanup=False)
+    try:
+        yield container
+    finally:
+        if not options.skip_cleanup:
+            container.destroy()
+
+
+def setUpModule():
+    """Module setup for base image unittests.
+
+    Sets up a test directory along with a reference container that is used by
+    tests that need an existing base container.
+    """
+    global test_dir
+    global reference_container
+    global cleanup_ref_container
+
+    test_dir = tempfile.mkdtemp(dir=lxc.DEFAULT_CONTAINER_PATH,
+                                prefix='base_container_manager_unittest_')
+    # Unfortunately, aside from duping the BaseImage code completely, there
+    # isn't an easy way to download and configure a base container.  So even
+    # though this is the BaseImage unittest, we use a BaseImage to set it up.
+    bcm = BaseImage()
+    if bcm.base_container is None:
+        bcm.setup()
+        cleanup_ref_container = True
+    reference_container = bcm.base_container
+
+
+def tearDownModule():
+    """Deletes the test dir and reference container."""
+    if not options.skip_cleanup:
+        if cleanup_ref_container:
+            reference_container.destroy()
+        shutil.rmtree(test_dir)
+
+
+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.insert(0, '-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/site_utils/lxc/container.py b/site_utils/lxc/container.py
index f6e8aef..4b514ea 100644
--- a/site_utils/lxc/container.py
+++ b/site_utils/lxc/container.py
@@ -77,7 +77,7 @@
 
 
     @classmethod
-    def createFromExistingDir(cls, lxc_path, name, **kwargs):
+    def create_from_existing_dir(cls, lxc_path, name, **kwargs):
         """Creates a new container instance for an lxc container that already
         exists on disk.
 
@@ -113,7 +113,7 @@
             if not cleanup:
                 raise error.ContainerError('Container %s already exists.' %
                                            new_name)
-            container = Container.createFromExistingDir(new_path, new_name)
+            container = Container.create_from_existing_dir(new_path, new_name)
             try:
                 container.destroy()
             except error.CmdError as e:
@@ -265,6 +265,9 @@
         @raise ContainerError: If container does not exist or failed to destroy
                                the container.
         """
+        logging.debug('Destroying container %s/%s',
+                      self.container_path,
+                      self.name)
         cmd = 'sudo lxc-destroy -P %s -n %s' % (self.container_path,
                                                 self.name)
         if force:
diff --git a/site_utils/lxc/container_bucket.py b/site_utils/lxc/container_bucket.py
index ad062ab..f813672 100644
--- a/site_utils/lxc/container_bucket.py
+++ b/site_utils/lxc/container_bucket.py
@@ -9,12 +9,13 @@
 import common
 from autotest_lib.client.bin import utils
 from autotest_lib.client.common_lib import error
-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 lxc
 from autotest_lib.site_utils.lxc import utils as lxc_utils
 from autotest_lib.site_utils.lxc.cleanup_if_fail import cleanup_if_fail
+from autotest_lib.site_utils.lxc.base_image import BaseImage
+from autotest_lib.site_utils.lxc.container import Container
 
 try:
     from chromite.lib import metrics
@@ -37,14 +38,9 @@
         """
         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:
-            base_container = Container.createFromExistingDir(
-                    container_path, constants.BASE);
-            base_container.refresh_status()
-            self.base_container = base_container
-        except error.ContainerError:
-            self.base_container = None
+        # Try to create the base container.  Use the default path and image
+        # name.
+        self.base_container = BaseImage().get()
 
 
     def get_all(self):
@@ -56,8 +52,8 @@
         info_collection = lxc.get_container_info(self.container_path)
         containers = {}
         for info in info_collection:
-            container = Container.createFromExistingDir(self.container_path,
-                                                        **info)
+            container = Container.create_from_existing_dir(self.container_path,
+                                                           **info)
             containers[container.name] = container
         return containers
 
@@ -138,74 +134,7 @@
                 return container
 
 
-    @cleanup_if_fail()
-    def setup_base(self, name=constants.BASE, force_delete=False):
-        """Setup base container.
-
-        @param name: Name of the base container, default to base.
-        @param force_delete: True to force to delete existing base container.
-                             This action will destroy all running test
-                             containers. Default is set to False.
-        """
-        if not self.container_path:
-            raise error.ContainerError(
-                    'You must set a valid directory to store containers in '
-                    'global config "AUTOSERV/ container_path".')
-
-        if not os.path.exists(self.container_path):
-            os.makedirs(self.container_path)
-
-        base_path = os.path.join(self.container_path, name)
-        if self.exist(name) and not force_delete:
-            logging.error(
-                    'Base container already exists. Set force_delete to True '
-                    'to force to re-stage base container. Note that this '
-                    'action will destroy all running test containers')
-            # Set proper file permission. base container in moblab may have
-            # owner of not being root. Force to update the folder's owner.
-            # TODO(dshi): Change root to current user when test container can be
-            # unprivileged container.
-            utils.run('sudo chown -R root "%s"' % base_path)
-            utils.run('sudo chgrp -R root "%s"' % base_path)
-            return
-
-        # Destroy existing base container if exists.
-        if self.exist(name):
-            # TODO: We may need to destroy all snapshots created from this base
-            # container, not all container.
-            self.destroy_all()
-
-        # Download and untar the base container.
-        tar_path = os.path.join(self.container_path, '%s.tar.xz' % name)
-        path_to_cleanup = [tar_path, base_path]
-        for path in path_to_cleanup:
-            if os.path.exists(path):
-                utils.run('sudo rm -rf "%s"' % path)
-        container_url = constants.CONTAINER_BASE_URL_FMT % name
-        lxc.download_extract(container_url, tar_path, self.container_path)
-        # Remove the downloaded container tar file.
-        utils.run('sudo rm "%s"' % tar_path)
-        # Set proper file permission.
-        # TODO(dshi): Change root to current user when test container can be
-        # unprivileged container.
-        utils.run('sudo chown -R root "%s"' % base_path)
-        utils.run('sudo chgrp -R root "%s"' % base_path)
-
-        # Update container config with container_path from global config.
-        config_path = os.path.join(base_path, 'config')
-        rootfs_path = os.path.join(base_path, 'rootfs')
-        utils.run(('sudo sed '
-                   '-i "s|\(lxc\.rootfs[[:space:]]*=\).*$|\\1 {rootfs}|" '
-                   '"{config}"').format(rootfs=rootfs_path,
-                                        config=config_path))
-
-        self.base_container = Container.createFromExistingDir(
-                self.container_path, name)
-
-        self._setup_shared_host_path(force_delete)
-
-
-    def _setup_shared_host_path(self, force_delete=False):
+    def setup_shared_host_path(self, force_delete=False):
         """Sets up the shared host directory.
 
         @param force_delete: If True, the host dir will be cleared and
diff --git a/site_utils/lxc/container_bucket_unittest.py b/site_utils/lxc/container_bucket_unittest.py
index ec6dfa0..727e24d 100644
--- a/site_utils/lxc/container_bucket_unittest.py
+++ b/site_utils/lxc/container_bucket_unittest.py
@@ -55,7 +55,7 @@
                          self.shared_host_path)
 
         # Set up, verify that the path is created.
-        bucket.setup_base()
+        bucket.setup_shared_host_path()
         self.assertTrue(os.path.isdir(self.shared_host_path))
 
         # Clean up, verify that the path is removed.
@@ -84,56 +84,10 @@
         bucket = lxc.ContainerBucket(container_path, self.shared_host_path)
 
         # Setup then destroy the bucket.  This should not emit any exceptions.
-        bucket.setup_base()
+        bucket.setup_shared_host_path()
         bucket.destroy_all()
 
 
-class ContainerBucketSetupBaseTests(unittest.TestCase):
-    """Unit tests to verify the ContainerBucket setup_base method."""
-
-    def setUp(self):
-        self.tmpdir = tempfile.mkdtemp()
-        self.shared_host_path = os.path.realpath(os.path.join(self.tmpdir,
-                                                              'host'))
-        self.bucket = lxc.ContainerBucket(container_path,
-                                          self.shared_host_path)
-
-
-    def tearDown(self):
-        for container in self.bucket.get_all().values():
-            container.stop()
-        self.bucket.destroy_all()
-        shutil.rmtree(self.tmpdir)
-
-
-    # TODO(kenobi): Read moblab_config.ini to get the correct base version
-    # instead of hard-coding it.
-    def testSetupBase05(self):
-        """Verifies that the code for installing the rootfs location into the
-        lxc config, is working correctly.
-        """
-        # Set up the bucket, then start the base container, and verify it works.
-        self.downloadAndStart('base_05')
-
-
-    # TODO(kenobi): Read shadow_config.ini to get the correct base version
-    # instead of hard-coding it.
-    def testSetupBase09(self):
-        """Verifies that the setup_base code works with the base_09 image. """
-        self.downloadAndStart('base_09')
-
-
-    def downloadAndStart(self, name):
-        """Calls setup_base with the given base image name, then starts the
-        container and verifies that it is running.
-
-        @param name: The name of the base image to download and test with.
-        """
-        self.bucket.setup_base(name=name)
-        base_container = self.bucket.get(name)
-        base_container.start()
-        self.assertTrue(base_container.is_running())
-
 def parse_options():
     """Parse command line inputs."""
     parser = argparse.ArgumentParser()
diff --git a/site_utils/lxc/container_unittest.py b/site_utils/lxc/container_unittest.py
index 24451c8..7dcc87a 100644
--- a/site_utils/lxc/container_unittest.py
+++ b/site_utils/lxc/container_unittest.py
@@ -58,7 +58,7 @@
     def testInit(self):
         """Verifies that containers initialize correctly."""
         # Make a container that just points to the base container.
-        container = lxc.Container.createFromExistingDir(
+        container = lxc.Container.create_from_existing_dir(
             self.base_container.container_path,
             self.base_container.name)
         # Calling is_running triggers an lxc-ls call, which should verify that
@@ -72,7 +72,8 @@
         """
         with tempfile.NamedTemporaryFile(dir=self.test_dir) as tmpfile:
             name = os.path.basename(tmpfile.name)
-            container = lxc.Container.createFromExistingDir(self.test_dir, name)
+            container = lxc.Container.create_from_existing_dir(self.test_dir,
+                                                               name)
             with self.assertRaises(error.ContainerError):
                 container.refresh_status()
 
@@ -122,6 +123,7 @@
         """Verifies that cloning a container works as expected."""
         clone = lxc.Container.clone(src=self.base_container,
                                     new_name="testClone",
+                                    new_path=self.test_dir,
                                     snapshot=True)
         try:
             # Throws an exception if the container is not valid.
@@ -136,10 +138,12 @@
         """
         lxc.Container.clone(src=self.base_container,
                             new_name="testCloneWithoutCleanup",
+                            new_path=self.test_dir,
                             snapshot=True)
         with self.assertRaises(error.ContainerError):
             lxc.Container.clone(src=self.base_container,
                                 new_name="testCloneWithoutCleanup",
+                                new_path=self.test_dir,
                                 snapshot=True)
 
 
@@ -147,6 +151,7 @@
         """Verifies that cloning a container with cleanup works properly."""
         clone0 = lxc.Container.clone(src=self.base_container,
                                      new_name="testClone",
+                                     new_path=self.test_dir,
                                      snapshot=True)
         clone0.start(wait_for_network=False)
         tmpfile = clone0.attach_run('mktemp').stdout
@@ -156,6 +161,7 @@
         # Clone another container in place of the existing container.
         clone1 = lxc.Container.clone(src=self.base_container,
                                      new_name="testClone",
+                                     new_path=self.test_dir,
                                      snapshot=True,
                                      cleanup=True)
         with self.assertRaises(error.CmdError):
@@ -283,7 +289,10 @@
         """
         if name is None:
             name = self.id().split('.')[-1]
-        container = self.bucket.create_from_base(name)
+        container = lxc.Container.clone(src=self.base_container,
+                                        new_name=name,
+                                        new_path=self.test_dir,
+                                        snapshot=True)
         try:
             yield container
         finally:
diff --git a/site_utils/lxc/lxc_functional_test.py b/site_utils/lxc/lxc_functional_test.py
index efdc6fc..d353ebe 100644
--- a/site_utils/lxc/lxc_functional_test.py
+++ b/site_utils/lxc/lxc_functional_test.py
@@ -182,7 +182,8 @@
     @param bucket: ContainerBucket to interact with containers.
     """
     logging.info('Rebuild base container in folder %s.', bucket.container_path)
-    bucket.setup_base()
+    lxc.BaseImage().setup()
+    bucket.setup_shared_host_path()
     containers = bucket.get_all()
     logging.info('Containers created: %s', containers.keys())
 
diff --git a/site_utils/lxc/unittest_container_bucket.py b/site_utils/lxc/unittest_container_bucket.py
index 1200876..976b233 100644
--- a/site_utils/lxc/unittest_container_bucket.py
+++ b/site_utils/lxc/unittest_container_bucket.py
@@ -29,7 +29,7 @@
 
                 # Clone the base container (snapshot for speed) to make a base
                 # container for the unit test.
-                base = lxc.Container.createFromExistingDir(
+                base = lxc.Container.create_from_existing_dir(
                         constants.DEFAULT_CONTAINER_PATH, constants.BASE)
                 lxc.Container.clone(src=base,
                                     new_name=constants.BASE,
@@ -40,7 +40,7 @@
         finally:
             super(FastContainerBucket, self).__init__(lxc_path, host_path)
             if self.base_container is not None:
-                self._setup_shared_host_path()
+                self.setup_shared_host_path()
 
 
     def setup_base(self, *args, **kwargs):
diff --git a/utils/unittest_suite.py b/utils/unittest_suite.py
index f15e94c..67ca62f 100755
--- a/utils/unittest_suite.py
+++ b/utils/unittest_suite.py
@@ -108,6 +108,7 @@
     'des_01_test.py',
     'des_02_test.py',
     # Require lxc to be installed
+    'base_image_unittest.py',
     'container_bucket_unittest.py',
     'container_unittest.py',
     'lxc_functional_test.py',